PHPNW15: James Mallison - Dependency Injection and Dependency Inversion in PHP

By: PHPNW

91   5   6733

Uploaded on 12/16/2015

Dependency Injection and Dependency Inversion are important tools for writing testable and reusable code. They are available in any object oriented language and PHP is no exception. In this talk we will look at both Dependency Injection and the Dependency Inversion Principle, how they fit in with SOLID, and why they should be used when writing object oriented code.

How are objects wired together? What is an object graph? Is a Dependency Injection Container the right way forward? Can we do this automatically, and are there any patterns or reusable components available to help us achieve reusable and decoupled code? These are some of the topics covered in this talk from both a theoretical and a practical standpoint.

Walking out of the room you should understand why dependency injection is so heavily advocated in programming and how you can use it to write awesome, decoupled code in PHP.

Comments (13):

By anonymous    2017-09-20

First question:

The first problem happens because you are loading and rendering the view files IN the action context/scope of the controller. That's why your url in the point 3. is "incorrect", e.g. site-test/tasks/page/tasks, e.g '///`. So, other said, because you are actually seeing the rendered html content from inside the controller folder, where it was the view file rendered.

In order to resolve this you have to options:

Option 1) Use absolute paths in all the anchors in your view/template files, like for example:

<a href="http://site-test/tasks/">tasks</a>
<a href="http://site-test/tasks/page/2">next page</a>

Here you can of course use a variable in your view/template files instead, like:

<a href="<?php echo $host; ?>/tasks/">tasks</a>
<a href="<?php echo $host; ?>/tasks/page/2">next page</a>

I showed you just pure php code, but you can use syntaxes provided by a diversity of template engines, if you decide to use one, one then PHP engine.

Option 2) Use relative paths in all the anchors in your view/template files, like for example:

<a href="/tasks/">tasks</a>
<a href="/tasks/page/2">next page</a>

The key is of course the slash (/) at the beginning of each anchor's href attribute. It ensures that all the links are "joined" to the host http://site-test of your MVC website.

Personally I use the second option in my own MVC framework and it works smooth and is... pretty.

Second question:

When url is: site-test or site-test/tasks all files loading from root_directory/images or root_directory/css, when url is: site-test/tasks/page/2 he tries to find these files in root_directory/tasks/page/images

Well, here is the need of a discussion:

When url is site-test, then the default action of the default controller is called, e.g. Controller_add_task::action_index(). You didn't show the code for that controller, so I can't say nothing about from where and how are the asset files (css, images, js) loaded in the views.

Same goes for the url site-test/tasks/page/...: you showed the code for the controller, but I can't say nothing about how and where are the asset files (css, images, js) loaded in the views. Are the asset paths loaded from inside the Controller (parent class) methods? Are they written - as normal - in the view/template files (using <script src=...>, <link href=...>, <img src=...>)? Are they loaded from inside the View::generate?

In principle, they can be passed to the view/template files as variables, or they can be written - as normal - in the view/template files.

The context/scope theory of the first question applies here too. And the key here is the same as in the first question: the asset paths should be passed/written as absolute paths or relative paths as shown above in the options 1) and 2).

Recommendations:

Note: Seeing that you want to learn clean MVC, I thought you would allow me to give you some tips:

*) Don't use any static classes/functions, singletons or so called service locators. Trust me, you don't need them at all if you are writing a clean, good testable MVC.

*) Use a dependency injection container like Auryn, which you will implement only on the entry point of your app, e.g. bootstrap.php or index.php. The next point tells you why.

*) Inject your dependencies, don't create them in the places where they are needed. So not like this:

function __construct()
    {
        $this->model = new Model_tasks();
        $this->view = new View();
    }

but like this:

function __construct(Model_tasks $model, View $view)
    {
        $this->model = $model;
        $this->view = $view;
    }

Read:

*) Use namespaces! Read PSR-4: Autoloader (yes, it's correct).

*) Read all the first 8 recommendations in PHP Standards Recommendations. They are very important if you want to correctly learn how to develop a PHP MVC.

*) Learn the five SOLID principles in OOP (just search for "solid principles" in web).

*) Use a corresponding view class for each controller class (1:1). For example: if you have TasksController, then also create a class TasksView. And what you are using there as view files - tasks_view.php, template_view.php, etc - are actually template files. And you should implement a Template class, which is to be injected in the corresponding view class. The template class is actually responsible for rendering/loading the template files. The view class should be responsible to output the rendered content (or to pass it to a Response class to be outputed).

Read: Understanding MVC Views in PHP

*) Don't use names like Controller_tasks for the class names. Use the syntax like this - I forgot the name: TasksController, TasksModel, TasksView.

*) Don't use names like action_index for the class methods or members. Use the camelCase syntax. And don't use action in the controller methods, because you already know that. So write the methods just like this: index(), page(), listBooks. If you use ajax you can name them like ajaxGetUsers(), etc. Just make sure that your routing system make the correct syntax-mapping between the controller/action names in url and the actual controller/action names.

Resources:

Good luck.

Original Thread

By anonymous    2017-09-20

From a general perspective, a web application based on the MVC concept is composed of two layers: the model layer and the presentation layer. Their implementation achieves the - goal of - separation of concerns.

The model layer consists of three sublayers:

  • The domain layer (domain model), consisting of the domain objects (the in-memory objects) - also known as models. They are the entities that encapsulate the business logic. So, through their structure and interdependence with each other, they are an abstraction of the real world (business) objects/entities. This layer could also contain structures like collections of domain objects.
  • The storage layer, composed from the classes responsible with the transfer of the domain objects into/from the underlying storage system (may it be RDBMS, session, file system etc): repositories, (data) mappers, adapters, data-access abstraction classes (PDO, MySQLi - and their wrappers), etc. The use of these structures also achieve the purpose of making the domain objects (completely) agnostic, unknowledgeable to the storage type and the way in which it is addressed.
  • The service layer is built from classes (e.g. services) that execute operations involving the structures from the upper two sublayers. For example, a service fetches a domain object from the storage system, make some validations based on it's state (properties) and returns corresponding results.

The presentation layer consists of:

  • Views.
  • Controllers.
  • [View-models]

Notice that I didn't complete the description of this layer. I did it on purpose, because I think it's better for you to follow this links, in order to gain the correct perspective on this subject:


About your second question: Indeed, ORMs automatize the mapping between domain layer and database. They are useful, but also come with disadvantages, because they are forcing you to think in terms of business logic PLUS database structure. "One class per table" as in (Table Data Gateway), "protected $tableName;" as in the parent class Mapper of DMM, "class User extends ActiveRecord" as in Active Record, etc, are signs of flexibility limitations. For example, as I saw in the DMM code, it forces you to provide a $tableName and $identityFields in Mapper constructor. That's a big limitation.

Anyway, if you want to be really flexible in tasks involving (complex) querying of database, then keep it simple:

  • Keep the domain objects completely unaware of storage system.
  • Implement the data mapper pattern without inheriting the specific mappers from any parent mapper! In the methods of the specific data mapppers (save, update, insert, delete, find, findByXXX, etc) you can then use pure SQL, infinitely complex. Read PHP MVC: Data Mapper pattern: class design. Of course, this way you'll write a bit more sql... and become an SQL-virtuoso! :-) Please notice that any other "solution" will reduce the sql flexibility.
  • If you really need to abstract from the sql language (SQL, T-SQL, PL/SQL, etc), you can even implement your own query builder class and use it's instance inside the data mapper methods, instead of the sql statements. Read PHP MVC: Query builder class for Data Mapper layer.
  • Implement an Adapter class, 90% similar to the one in DMM. Things like tableName should not appear there.
  • Create a PDO connection and inject it in the constructor of the Adapter object. NB: Don't create PDO inside Adapter, as DMM does, because their adapter class is then tight coupled to PDO. Yours should be - correctly - loosely coupled. You're achieving this through dependency injection - see The Clean Code Talks - Don't Look For Things!.
  • Try to use a dependency injection container. Like Auryn. It will take care of the instantiation and sharing of all classes at the entry point of your application (index.php, or bootstrap.php) only. Best example is the PDO connection, shared through the complete cycle of a web MVC. It's very powerful, very easy to learn and use, and will make your MVC really slim. Watch Dependency Injection and Dependency Inversion in PHP first.

Later you'll want to create repositories and services too.

So, to close your first question: there is a very good explained article series, exactly about what you're interested in. After you'll read them, you'll have no doubt anymore about how all model layer components work together. NB: You'll see there the same $tableName as property, but now you know from which perspective to consider it. So:

And here is a version of a mapper, inspired from the above articles. Notice the absence of inheritance from a parent/abstract class. To find out the reasons, read the great answer from PHP MVC: Data Mapper pattern: class design.

Data mapper class:

<?php

/*
 * User mapper.
 * 
 * Copyright © 2017 SitePoint
 * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 
 * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 
 * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

namespace App\Modules\Connects\Models\Mappers;

use App\Modules\Connects\Models\Models\User;
use App\Modules\Connects\Models\Models\UserInterface;
use App\Modules\Connects\Models\Mappers\UserMapperInterface;
use App\Modules\Connects\Models\Collections\UserCollectionInterface;
use App\Core\Model\Storage\Adapter\Database\DatabaseAdapterInterface;

/**
 * User mapper.
 */
class UserMapper implements UserMapperInterface {

    /**
     * Adapter.
     * 
     * @var DatabaseAdapterInterface
     */
    private $adapter;

    /**
     * User collection.
     * 
     * @var UserCollectionInterface
     */
    private $userCollection;

    /**
     * 
     * @param DatabaseAdapterInterface $adapter Adapter.
     * @param UserCollectionInterface $userCollection User collection.
     */
    public function __construct(DatabaseAdapterInterface $adapter, UserCollectionInterface $userCollection) {
        $this
                ->setAdapter($adapter)
                ->setUserCollection($userCollection)
        ;
    }

    /**
     * Find user by id.
     * 
     * @param int $id User id.
     * @return UserInterface User.
     */
    public function findById($id) {
        $sql = "SELECT * FROM users WHERE id=:id";
        $bindings = [
            'id' => $id
        ];

        $row = $this->getAdapter()->selectOne($sql, $bindings);

        return $this->createUser($row);
    }

    /**
     * Find users by criteria.
     * 
     * @param array $filter [optional] WHERE conditions.
     * @return UserCollectionInterface User collection.
     */
    public function find(array $filter = array()) {
        $conditions = array();
        foreach ($filter as $key => $value) {
            $conditions[] = $key . '=:' . $key;
        }
        $whereClause = implode(' AND ', $conditions);

        $sql = sprintf('SELECT * FROM users %s'
                , !empty($filter) ? 'WHERE ' . $whereClause : ''
        );
        $bindings = $filter;

        $rows = $this->getAdapter()->select($sql, $bindings);

        return $this->createUserCollection($rows);
    }

    /**
     * Insert user.
     * 
     * @param UserInterface $user User.
     * @return UserInterface Inserted user (saved data may differ from initial user data).
     */
    public function insert(UserInterface $user) {
        $properties = get_object_vars($user);

        $columnsClause = implode(',', array_keys($properties));

        $values = array();
        foreach (array_keys($properties) as $column) {
            $values[] = ':' . $column;
        }
        $valuesClause = implode(',', $values);

        $sql = sprintf('INSERT INTO users (%s) VALUES (%s)'
                , $columnsClause
                , $valuesClause
        );
        $bindings = $properties;

        $this->getAdapter()->insert($sql, $bindings);

        $lastInsertId = $this->getAdapter()->getLastInsertId();

        return $this->findById($lastInsertId);
    }

    /**
     * Update user.
     * 
     * @param UserInterface $user User.
     * @return UserInterface Updated user (saved data may differ from initial user data).
     */
    public function update(UserInterface $user) {
        $properties = get_object_vars($user);

        $columns = array();
        foreach (array_keys($properties) as $column) {
            if ($column !== 'id') {
                $columns[] = $column . '=:' . $column;
            }
        }
        $columnsClause = implode(',', $columns);

        $sql = sprintf('UPDATE users SET %s WHERE id = :id'
                , $columnsClause
        );
        $bindings = $properties;

        $this->getAdapter()->update($sql, $bindings);

        return $this->findById($user->id);
    }

    /**
     * Delete user.
     * 
     * @param UserInterface $user User.
     * @return bool TRUE if user successfully deleted, FALSE otherwise.
     */
    public function delete(UserInterface $user) {
        $sql = 'DELETE FROM users WHERE id=:id';
        $bindings = array(
            'id' => $user->id
        );

        $rowCount = $this->getAdapter()->delete($sql, $bindings);

        return $rowCount > 0;
    }

    /**
     * Create user.
     * 
     * @param array $row Table row.
     * @return UserInterface User.
     */
    public function createUser(array $row) {
        $user = new User();
        foreach ($row as $key => $value) {
            $user->$key = $value;
        }
        return $user;
    }

    /**
     * Create user collection.
     * 
     * @param array $rows Table rows.
     * @return UserCollectionInterface User collection.
     */
    public function createUserCollection(array $rows) {
        $this->getUserCollection()->clear();
        foreach ($rows as $row) {
            $user = $this->createUser($row);
            $this->getUserCollection()->add($user);
        }
        return $this->getUserCollection()->toArray();
    }

    /**
     * Get adapter.
     * 
     * @return DatabaseAdapterInterface
     */
    public function getAdapter() {
        return $this->adapter;
    }

    /**
     * Set adapter.
     * 
     * @param DatabaseAdapterInterface $adapter Adapter.
     * @return $this
     */
    public function setAdapter(DatabaseAdapterInterface $adapter) {
        $this->adapter = $adapter;
        return $this;
    }

    /**
     * Get user collection.
     * 
     * @return UserCollectionInterface
     */
    public function getUserCollection() {
        return $this->userCollection;
    }

    /**
     * Set user collection.
     * 
     * @param UserCollectionInterface $userCollection User collection.
     * @return $this
     */
    public function setUserCollection(UserCollectionInterface $userCollection) {
        $this->userCollection = $userCollection;
        return $this;
    }

}

Data mapper interface:

<?php

/*
 * User mapper interface.
 */

namespace App\Modules\Connects\Models\Mappers;

use App\Modules\Connects\Models\Models\UserInterface;

/**
 * User mapper interface.
 */
interface UserMapperInterface {
    /**
     * Find user by id.
     * 
     * @param int $id User id.
     * @return UserInterface User.
     */
    public function findById($id);

    /**
     * Find users by criteria.
     * 
     * @param array $filter [optional] WHERE conditions.
     * @param string $operator [optional] WHERE conditions concatenation operator.
     * @return UserCollectionInterface User collection.
     */
    public function find(array $filter = array(), $operator = 'AND');

    /**
     * Insert user.
     * 
     * @param UserInterface $user User.
     * @return UserInterface Inserted user (saved data may differ from initial user data).
     */
    public function insert(UserInterface $user);

    /**
     * Update user.
     * 
     * @param UserInterface $user User.
     * @return UserInterface Updated user (saved data may differ from initial user data).
     */
    public function update(UserInterface $user);

    /**
     * Delete user.
     * 
     * @param UserInterface $user User.
     * @return bool TRUE if user successfully deleted, FALSE otherwise.
     */
    public function delete(UserInterface $user);

    /**
     * Create user.
     * 
     * @param array $row Table row.
     * @return UserInterface User.
     */
    public function createUser(array $row);

    /**
     * Create user collection.
     * 
     * @param array $rows Table rows.
     * @return UserCollectionInterface User collection.
     */
    public function createUserCollection(array $rows);
}

Adapter class:

<?php

namespace App\Core\Model\Storage\Adapter\Database\Pdo;

use PDO;
use PDOStatement;
use PDOException as Php_PDOException;
use App\Core\Exception\PDO\PDOException;
use App\Core\Exception\SPL\UnexpectedValueException;
use App\Core\Model\Storage\Adapter\Database\DatabaseAdapterInterface;

abstract class AbstractPdoAdapter implements DatabaseAdapterInterface {

    /**
     * Database connection.
     * 
     * @var PDO
     */
    private $connection;

    /**
     * Fetch mode for a PDO statement. Must be one of the PDO::FETCH_* constants.
     * 
     * @var int
     */
    private $fetchMode = PDO::FETCH_ASSOC;

    /**
     * Fetch argument for a PDO statement.
     * 
     * @var mixed 
     */
    private $fetchArgument = NULL;

    /**
     * Constructor arguments for a PDO statement when fetch mode is PDO::FETCH_CLASS.
     * 
     * @var array 
     */
    private $fetchConstructorArguments = array();

    /**
     * For a PDOStatement object representing a scrollable cursor, this value determines<br/>
     * which row will be returned to the caller.
     * 
     * @var int 
     */
    private $fetchCursorOrientation = PDO::FETCH_ORI_NEXT;

    /**
     * The absolute number of the row in the result set, or the row relative to the cursor<br/>
     * position before PDOStatement::fetch() was called.
     * 
     * @var int 
     */
    private $fetchCursorOffset = 0;

    /**
     * @param PDO $connection Database connection.
     */
    public function __construct(PDO $connection) {
        $this->setConnection($connection);
    }

    /**
     * Fetch data by executing a SELECT sql statement.
     * 
     * @param string $sql Sql statement.
     * @param array $bindings [optional] Input parameters.
     * @return array An array containing the rows in the result set, or FALSE on failure.
     */
    public function select($sql, array $bindings = array()) {
        $statement = $this->execute($sql, $bindings);
        $fetchArgument = $this->getFetchArgument();
        if (isset($fetchArgument)) {
            return $statement->fetchAll(
                            $this->getFetchMode()
                            , $fetchArgument
                            , $this->getFetchConstructorArguments()
            );
        }
        return $statement->fetchAll($this->getFetchMode());
    }

    /**
     * Fetch the next row from the result set by executing a SELECT sql statement.<br/>
     * The fetch mode property determines how PDO returns the row.
     * 
     * @param string $sql Sql statement.
     * @param array $bindings [optional] Input parameters.
     * @return array An array containing the rows in the result set, or FALSE on failure.
     */
    public function selectOne($sql, array $bindings = array()) {
        $statement = $this->execute($sql, $bindings);
        return $statement->fetch(
                        $this->getFetchMode()
                        , $this->getFetchCursorOrientation()
                        , $this->getFetchCursorOffset()
        );
    }

    /**
     * Store data by executing an INSERT sql statement.
     * 
     * @param string $sql Sql statement.
     * @param array $bindings [optional] Input parameters.
     * @return int The number of the affected records.
     */
    public function insert($sql, array $bindings = array()) {
        $statement = $this->execute($sql, $bindings);
        return $statement->rowCount();
    }

    /**
     * Update data by executing an UPDATE sql statement.
     * 
     * @param string $sql Sql statement.
     * @param array $bindings [optional] Input parameters.
     * @return int The number of the affected records.
     */
    public function update($sql, array $bindings = array()) {
        $statement = $this->execute($sql, $bindings);
        return $statement->rowCount();
    }

    /**
     * Delete data by executing a DELETE sql statement.
     * 
     * @param string $sql Sql statement.
     * @param array $bindings [optional] Input parameters.
     * @return int The number of the affected records.
     */
    public function delete($sql, array $bindings = array()) {
        $statement = $this->execute($sql, $bindings);
        return $statement->rowCount();
    }

    /**
     * Prepare and execute an sql statement.
     * 
     * @todo I want to re-use the statement to execute several queries with the same SQL statement 
     * only with different parameters. So make a statement field and prepare only once!
     * See: https://www.sitepoint.com/integrating-the-data-mappers/
     * 
     * @param string $sql Sql statement.
     * @param array $bindings [optional] Input parameters.
     * @return PDOStatement The PDO statement after execution.
     */
    protected function execute($sql, array $bindings = array()) {
        // Prepare sql statement.
        $statement = $this->prepareStatement($sql);

        // Bind input parameters.
        $this->bindInputParameters($statement, $bindings);

        // Execute prepared sql statement.
        $this->executePreparedStatement($statement);

        return $statement;
    }

    /**
     * Prepare and validate an sql statement.<br/>
     * 
     * ---------------------------------------------------------------------------------
     * If the database server cannot successfully prepare the statement,
     * PDO::prepare() returns FALSE or emits PDOException (depending on error handling).
     * ---------------------------------------------------------------------------------
     * 
     * @param string $sql Sql statement.
     * @return PDOStatement If the database server successfully prepares the statement,
     * return a PDOStatement object. Otherwise return FALSE or emit PDOException 
     * (depending on error handling).
     * @throws Php_PDOException
     * @throws PDOException
     */
    private function prepareStatement($sql) {
        try {
            $statement = $this->getConnection()->prepare($sql);
            if (!$statement) {
                throw new PDOException('The sql statement can not be prepared!');
            }
        } catch (Php_PDOException $exc) {
            throw new PDOException('The sql statement can not be prepared!', 0, $exc);
        }
        return $statement;
    }

    /**
     * Bind the input parameters to a prepared PDO statement.
     * 
     * @param PDOStatement $statement PDO statement.
     * @param array $bindings Input parameters.
     * @return $this
     */
    private function bindInputParameters($statement, $bindings) {
        foreach ($bindings as $key => $value) {
            $statement->bindValue(
                    $this->getInputParameterName($key)
                    , $value
                    , $this->getInputParameterDataType($value)
            );
        }
        return $this;
    }

    /**
     * Get the name of an input parameter by its key in the bindings array.
     *  
     * @param int|string $key The key of the input parameter in the bindings array.
     * @return int|string The name of the input parameter.
     */
    private function getInputParameterName($key) {
        return is_int($key) ? ($key + 1) : (':' . ltrim($key, ':'));
    }

    /**
     * Get the PDO::PARAM_* constant, e.g the data type of an input parameter, by its value.
     *  
     * @param mixed $value Value of the input parameter.
     * @return int The PDO::PARAM_* constant.
     */
    private function getInputParameterDataType($value) {
        $dataType = PDO::PARAM_STR;
        if (is_int($value)) {
            $dataType = PDO::PARAM_INT;
        } elseif (is_bool($value)) {
            $dataType = PDO::PARAM_BOOL;
        }
        return $dataType;
    }

    /**
     * Execute a prepared PDO statement.
     * 
     * @param PDOStatement $statement PDO statement.
     * @return $this
     * @throws UnexpectedValueException
     */
    private function executePreparedStatement($statement) {
        if (!$statement->execute()) {
            throw new UnexpectedValueException('The statement can not be executed!');
        }
        return $this;
    }

    /**
     * Get the ID of the last inserted row or of the sequence value.
     * 
     * @param string $sequenceObjectName [optional] Name of the sequence object<br/>
     * from which the ID should be returned.
     * @return string The ID of the last row, or the last value retrieved from the specified<br/>
     * sequence object, or an error IM001 SQLSTATE If the PDO driver does not support this.
     */
    public function getLastInsertId($sequenceObjectName = NULL) {
        return $this->getConnection()->lastInsertId($sequenceObjectName);
    }

    public function getConnection() {
        return $this->connection;
    }

    public function setConnection(PDO $connection) {
        $this->connection = $connection;
        return $this;
    }

    public function getFetchMode() {
        return $this->fetchMode;
    }

    public function setFetchMode($fetchMode) {
        $this->fetchMode = $fetchMode;
        return $this;
    }

    public function getFetchArgument() {
        return $this->fetchArgument;
    }

    public function setFetchArgument($fetchArgument) {
        $this->fetchArgument = $fetchArgument;
        return $this;
    }

    public function getFetchConstructorArguments() {
        return $this->fetchConstructorArguments;
    }

    public function setFetchConstructorArguments($fetchConstructorArguments) {
        $this->fetchConstructorArguments = $fetchConstructorArguments;
        return $this;
    }

    public function getFetchCursorOrientation() {
        return $this->fetchCursorOrientation;
    }

    public function setFetchCursorOrientation($fetchCursorOrientation) {
        $this->fetchCursorOrientation = $fetchCursorOrientation;
        return $this;
    }

    public function getFetchCursorOffset() {
        return $this->fetchCursorOffset;
    }

    public function setFetchCursorOffset($fetchCursorOffset) {
        $this->fetchCursorOffset = $fetchCursorOffset;
        return $this;
    }

}

About your first question: There is no convention about where you should store your classes. Choose whatever file sytem structure you wish. But make sure that:

1) You're using an autoloader and namespaces, as recommended in the PSR-4 Autoloading Standard.

2) You can uniquely identify each component class at any time. You can achieve this in two ways: Either by applying a corresponding suffix to each class (UserController, UserMapper, UserView, etc), or by defining corresponding class aliases in the use statements, like:

namespace App\Controllers;

use App\Models\DomainObjects\User;
use App\Models\Mappers\User as UserMapper;
use App\Models\Repositories\User as UserRepository;

The file system structure could be something like the following - it's the one used in my project, so sorry if it's too complex at first sight:

In App/Core:

enter image description here

In App/:

enter image description here

Good luck!

Original Thread

By anonymous    2017-09-20

Give it a try.

  • You should create only one PDO instance, e.g. connection. It should be passed as argument to each function that uses it. Using global vars or even static functions in your code is a bad idea.
  • I wrote a function named fetchLeagues() instead of impressoras. Implement function carrefour() in a similar way.
  • I wrote a function fetchleagueById() too, in order to show you how to bind values to the prepared sql statement.
  • ... and I implemented three other example functions (insertUser(), updateUser() and deleteUser()) too, so that you can see the complete CRUD process.

Some recommendations:

  • Try to move to OOP.
  • Apply the so-called dependency injection instead of implementing globals and statics.
  • Apply exception handling and activate error reporting/handling. I gave you a general view by throwing only Exception. Normally you should throw and handle the SPL (Standard PHP Library) subtypes of it.
  • Always read in the PHP Manual what PHP functions are returning so that you can correctly apply handle cases (exceptions, bools, etc).

Resources:


Here is the code, spread on four PHP pages. index.php is the main page, the others are included in it.

Good luck!

PDO prepared statements with exception handling:

index.php

<?php

require_once 'configs.php';
require_once 'generalFunctions.php';
require_once 'testFunctions.php';

// Activate error reporting (only on development).
activateErrorReporting();

try {
    // Create db connection.
    $connection = createConnection(
            MYSQL_HOST
            , MYSQL_DATABASE
            , MYSQL_USERNAME
            , MYSQL_PASSWORD
            , MYSQL_PORT
            , MYSQL_CHARSET
    );

    // Fetch leagues.
    $leagues = fetchLeagues($connection);
    printData($leagues, TRUE);

    // OR...
    // Fetch league by id.
    // $leagueId = 1;
    // $league = fetchLeagueById($connection, $leagueId);
    // printData($league, TRUE);

    // Insert user.
    // $username = 'Sam';
    // $lastInsertId = insertUser($connection, $username);
    // printData($lastInsertId, TRUE);

    // Update user.
    // $userId = 6;
    // $username = 'Mikaela';
    // $userUpdated = updateUser($connection, $userId, $username);
    // printData($userUpdated, TRUE);

    // Delete user.
    // $userId = 6;
    // $userDeleted = deleteUser($connection, $userId);
    // printData($userDeleted, TRUE);

    // Close connection
    closeConnection($connection);
} catch (PDOException $pdoException) {
    // On development.
    printData($pdoException, TRUE);

    // On production.
    // echo $pdoException->getMessage();
    exit();
} catch (Exception $exception) {
    // On development.
    printData($exception, TRUE);

    // On production.
    // echo $exception->getMessage();
    exit();
}

testFunctions.php

<?php

/*
 * ---------------------
 * Test functions
 * ---------------------
 */

/**
 * Fetch leagues.
 * 
 * SELECT * FROM [table-name] WHERE [col1]=:[val1] [oper] [col2]=:[val2]
 * 
 * @param PDO $connection Connection instance.
 * @throws Exception
 */
function fetchLeagues($connection) {
    // Sql statement.
    $sql = 'SELECT * FROM league';

    // Prepare and check sql statement (returns PDO statement).
    $statement = $connection->prepare($sql);
    if (!$statement) {
        throw new Exception('The SQL statement can not be prepared!');
    }

    // Execute and check PDO statement.
    if (!$statement->execute()) {
        throw new Exception('The PDO statement can not be executed!');
    }

    // Fetch data.
    $fetchedData = $statement->fetchAll(PDO::FETCH_ASSOC);
    if ($fetchedData === FALSE) {
        throw new Exception('Fetching data failed!');
    }

    return $fetchedData;
}

/**
 * Fetch league by id.
 * 
 * SELECT * FROM [table-name] WHERE [col1]=:[val1] [oper] [col2]=:[val2]
 * 
 * @param PDO $connection Connection instance.
 * @param string $leagueId League ID.
 * @throws Exception
 */
function fetchLeagueById($connection, $leagueId) {
    if (!isset($leagueId)) {
        throw new Exception('League ID not provided!');
    }

    // Sql statement.
    $sql = 'SELECT * FROM league WHERE id = :id';

    // Prepare and check sql statement (returns PDO statement).
    $statement = $connection->prepare($sql);
    if (!$statement) {
        throw new Exception('The SQL statement can not be prepared!');
    }

    // Bind values to sql statement parameters.
    $statement->bindValue(':id', $leagueId, getInputParameterDataType($leagueId));

    // Execute and check PDO statement.
    if (!$statement->execute()) {
        throw new Exception('The PDO statement can not be executed!');
    }

    // Fetch data.
    $fetchedData = $statement->fetchAll(PDO::FETCH_ASSOC);
    if ($fetchedData === FALSE) {
        throw new Exception('Fetching data failed!');
    }

    return $fetchedData;
}

/**
 * Insert user.
 * 
 * INSERT INTO [table-name] ([col1],[col2],[col3]) VALUES (:[col1],:[col2],:[col3])
 * 
 * @param PDO $connection Connection instance.
 * @param string $username User name.
 * @return integer Last insert id.
 * @throws Exception
 */
function insertUser($connection, $username) {
    // Sql statement.
    $sql = 'INSERT INTO users (
                username
            ) VALUES (
                :username
            )';

    // Prepare and check sql statement (returns PDO statement).
    $statement = $connection->prepare($sql);
    if (!$statement) {
        throw new Exception('The SQL statement can not be prepared!');
    }

    // Bind values to sql statement parameters.
    $statement->bindValue(':username', $username, getInputParameterDataType($username));

    // Execute and check PDO statement.
    if (!$statement->execute()) {
        throw new Exception('The PDO statement can not be executed!');
    }

    // Get last insert id.
    return $connection->lastInsertId();
}

/**
 * Update user.
 * 
 * UPDATE [table-name] SET [col1]=:[col1],[col2]=:[col2] WHERE [PK-name]=:[PK-name]
 * 
 * @param PDO $connection Connection instance.
 * @param integer $userId User ID.
 * @param string $username User name.
 * @return bool TRUE if update successful, FALSE otherwise.
 * @throws Exception
 */
function updateUser($connection, $userId, $username) {
    if (!isset($userId)) {
        throw new Exception('User ID not provided!');
    }

    // Sql statement.
    $sql = 'UPDATE users 
            SET username = :username 
            WHERE id = :id';

    // Prepare and check sql statement (returns PDO statement).
    $statement = $connection->prepare($sql);
    if (!$statement) {
        throw new Exception('The SQL statement can not be prepared!');
    }

    // Bind values to sql statement parameters.
    $statement->bindValue(':id', $userId, getInputParameterDataType($userId));
    $statement->bindValue(':username', $username, getInputParameterDataType($username));

    // Execute and check PDO statement.
    if (!$statement->execute()) {
        throw new Exception('The PDO statement can not be executed!');
    }

    // Return TRUE if update successful, FALSE otherwise.
    return $statement->rowCount() > 0 ? TRUE : FALSE;
}

/**
 * Delete user.
 * 
 * DELETE FROM [table-name] WHERE [PK-name]=:[PK-name]
 * 
 * @param PDO $connection Connection instance.
 * @param integer $userId User ID.
 * @return bool TRUE if delete successful, FALSE otherwise.
 * @throws Exception
 */
function deleteUser($connection, $userId) {
    if (!isset($userId)) {
        throw new Exception('User ID not provided!');
    }

    // Sql statement.
    $sql = 'DELETE FROM users 
            WHERE id = :id';

    // Prepare and check sql statement (returns PDO statement).
    $statement = $connection->prepare($sql);
    if (!$statement) {
        throw new Exception('The SQL statement can not be prepared!');
    }

    // Bind values to sql statement parameters.
    $statement->bindValue(':id', $userId, getInputParameterDataType($userId));

    // Execute and check PDO statement.
    if (!$statement->execute()) {
        throw new Exception('The PDO statement can not be executed!');
    }

    // Return TRUE if delete successful, FALSE otherwise.
    return $statement->rowCount() > 0 ? TRUE : FALSE;
}

configs.php

<?php

/*
 * ----------------
 * Database configs
 * ----------------
 */

define('MYSQL_HOST', '...');
define('MYSQL_PORT', '3306');
define('MYSQL_DATABASE', '...');
define('MYSQL_CHARSET', 'utf8');
define('MYSQL_USERNAME', '...');
define('MYSQL_PASSWORD', '...');

define('MYSQL_HOST_2', '...');
define('MYSQL_PORT_2', '3306');
define('MYSQL_DATABASE_2', '...');
define('MYSQL_CHARSET_2', 'utf8');
define('MYSQL_USERNAME_2', '...');
define('MYSQL_PASSWORD_2', '...');

generalFunctions.php

<?php

/*
 * ---------------------
 * Data access functions
 * ---------------------
 */

/**
 * Create a new db connection.
 * 
 * @param string $host Host.
 * @param string $dbname Database name.
 * @param string $username Username.
 * @param string $password Password.
 * @param string $port [optional] Port.
 * @param array $charset [optional] Character set.
 * @param array $options [optional] Driver options.
 * @return PDO Db connection.
 */
function createConnection($host, $dbname, $username, $password, $port = '3306', $charset = 'utf8', $options = array(
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_EMULATE_PREPARES => false,
    PDO::ATTR_PERSISTENT => true,
)) {
    $dsn = getDsn($host, $dbname, $port, $charset);
    $connection = new PDO($dsn, $username, $password);
    foreach ($options as $key => $value) {
        $connection->setAttribute($key, $value);
    }
    return $connection;
}

/**
 * Create a mysql DSN string.
 * 
 * @param string $host Host.
 * @param string $dbname Database name.
 * @param string $port [optional] Port.
 * @param array $charset [optional] Character set.
 * @return string DSN string.
 */
function getDsn($host, $dbname, $port = '3306', $charset = 'utf8') {
    $dsn = sprintf('mysql:host=%s;port=%s;dbname=%s;charset=%s'
            , $host
            , $port
            , $dbname
            , $charset
    );
    return $dsn;
}

/**
 * Close a db connection.
 * 
 * @param PDO $connection Db connection.
 * @return void
 */
function closeConnection($connection) {
    $connection = NULL;
}

/**
 * Get the data type of a binding value.
 * 
 * @param mixed $value Binding value.
 * @return mixed Data type of the binding value.
 */
function getInputParameterDataType($value) {
    $dataType = PDO::PARAM_STR;
    if (is_int($value)) {
        $dataType = PDO::PARAM_INT;
    } elseif (is_bool($value)) {
        $dataType = PDO::PARAM_BOOL;
    }
    return $dataType;
}

/*
 * ---------------
 * Print functions
 * ---------------
 */

/**
 * Print data on screen.
 * 
 * @param mixed $data Data to print.
 * @param bool $preformatted Print preformatted if TRUE, print normal otherwise.
 * @return void
 */
function printData($data, $preformatted = FALSE) {
    if ($preformatted) {
        echo '<pre>' . print_r($data, true) . '</pre>';
    } else {
        echo $data;
    }
}

/*
 * -------------------------
 * Error reporting functions
 * -------------------------
 */

/**
 * Toggle error reporting.
 * 
 * @param integer $level Error level.
 * @param bool $displayErrors Display errors if TRUE, hide them otherwise.
 * @return void
 */
function activateErrorReporting($level = E_ALL, $displayErrors = TRUE) {
    error_reporting($level);
    ini_set('display_errors', ($displayErrors ? 1 : 0));
}

Original Thread

By anonymous    2017-10-22

Here are some very good resources to study. They will give you the answer to the question "How can I make it right?". [The Clean Code Talks - Don't Look For Things!](https://www.youtube.com/watch?v=RlfLCWKxHJ0), [PHPNW15: James Mallison - Dependency Injection and Dependency Inversion in PHP](https://www.youtube.com/watch?v=Ojsn11XY0X8&feature=youtu.be), [Managing Class Dependencies: ..., Part 1](https://www.sitepoint.com/managing-class-dependencies-1/), [Managing Class Dependencies: ..., Part 2](https://www.sitepoint.com/managing-class-dependencies-2/),

Original Thread

By anonymous    2017-10-30

Ishegg is right. Here are two very good resources which sustain and explain what he says: [The Clean Code Talks - Don't Look For Things!](https://www.youtube.com/watch?v=RlfLCWKxHJ0) and [James Mallison - Dependency Injection and Dependency Inversion in PHP](https://www.youtube.com/watch?v=Ojsn11XY0X8&feature=youtu.be).

Original Thread

By anonymous    2017-10-30

A good answer! ;-) Just a note though: the "OK" version I would directly entitle as "BAD" (regarding config array and tight coupling): [The Clean Code Talks - Don't Look For Things!](https://www.youtube.com/watch?v=RlfLCWKxHJ0) and [James Mallison - Dependency Injection and Dependency Inversion in PHP](https://www.youtube.com/watch?v=Ojsn11XY0X8&feature=youtu.be).

Original Thread

By anonymous    2018-01-14

No matter how many web frameworks, routers, autoloaders, etc are there already: keep doing what you think it's right for you and suitable to your momentarily understanding level, in order to LEARN. Actually, by confronting yourself with problems arised along the process of implementing yourself different parts of your application, you will not only gain the opportunity to learn and discover new things, but also to learn how and what to study in the already existing frameworks' design.

Study the PHP Standard Recommendations (the ones marked as "accepted"). Especially PSR-1,2,4,7. They are used by many frameworks and PHP projects. Read FAQs to find out more about the project itself.

Autoloader:

The PSR-4 provides a link with examples at the document end.

@mike suggested, that you should use the Composer autoloader. I agree with him and I strongly recommend it to you too. BUT I suggest you to do this only after you correctly implement and make use of your own autoloader (PSR-4 conform). Why? You definitely need to learn how the autoloading process works. And in some future situations you will still need your own autoloader implementation, even after Composer is installed and running.

Also be aware that you must not raise any exceptions from autoloader itself!

Router:

Btw, your class should be called "Router".

The router should not be responsible for validating the controller class/file and the action, nor for calling the action. These tasks are part of the "front controller" responsibilities. Your router should just return the components resulted after parsing, e.g. "exploding" the request URI ($_GET['furl']), in some form (as a Route object (with them as properties), as array, etc). These components are the controller name, the action name, the action parameters list (NB: the action parameters are not the query string parameters). The front controller uses them further to validate/access the controller class/file and its action and to call the action.

But please note that a router works actually in other way. In short: it matches (e.g. compares) the request method (GET, POST, etc) and the request URI against an existing (e.g. predefined by you) list of route definitions. A route definition contains the infos related to a specific controller, action, etc. If the HTTP method and the request URI "correspond" to one of the route definitions, then the router returns the matched definition's components to the front controller (in some form: as object, as array, etc).

For more details describing this principle see:

Front controller:

It can be a class, but it can also be just vanilla code in the entry point of your app (index.php, bootstrap.php, etc). In the latter case, the front controller code should reside in a file outside of the document root of the app. For example in a bootstrap.php file, which is to be just included in index.php - whereas index.php resides inside the document root.

"controller/action not found" specific handling:

If a controller, or an action is not found/valid, then call a predefined action (for example displayError) of a predefined Error controller, which informs the user that, for a specific part of his request (actually of his provided request URI), no resource was found.

For example, consider that the user provided the request URI www.example.com/books/show/12. Conform to your app workflow the controller is Book, the action (e.g. the controller's method) is show and the action parameter is 12 (the value is passed as argument to the show method and defined as $bookId parameter in it). But, if the controller class is not defined, or no controller file exists, then the front controller should call the action displayError of Error controller, which should display a message like No resource found for your 'book' request. A similar info should be displayed when the show method is not yet defined in the Book controller.

Note that, if the Error controller or its action is not found/valid, then the PHP engine raises a corresponding error/exception. If you follow the next links I provided, you'll end up implementing three custom error/exception handling functions (referenced by set_error_handler, set_exception_handler and register_shutdown_function, respectively). They will catch and handle the described situation properly.

To read: Manage the errors of a framework

General error/exception handling in MVC:

Here are some good resources:

Other MVC related resources:

P.S: Avoid the use of statics, globals, singletons. Why? Read here and here, for example.

Good luck.

Original Thread

By anonymous    2018-02-05

@bnoeafk ;-) There are "_dependency injection_" and "_dependency inversion_" in OOP. Check [this](https://www.youtube.com/watch?v=Ojsn11XY0X8&feature=youtu.be) and [this](https://www.youtube.com/watch?v=RlfLCWKxHJ0) out. So, you must avoid tight coupling in your classes design. E.g. don't create an object inside of a class, but pass it as dependency (e.g. as constructor or setter argument). Also, as a little (but important) note: the **variables** defined in a function definition are called "_parameters_". The **values** passed when the function is called are named "_arguments_".

Original Thread

By anonymous    2018-02-18

Static, singleton, global

I am sorry to disappoint you, because I am sure you invested a lot of effort in your code,... but you could or should take in consideration to start by not using any static methods (nor singletons or global variables). Here and here are some reasons presented.

Dependency injection (DI)

Inside a class, don't use methods of another class to create instances. If you need an object of any type, inject it - in constructor, or in any other method that needs it. This is called dependency injection. See this and this.

So, you should have this (further below I will change it):

class LoginController {

    private $template;

    __construct(Template $template) {
        $this->template = $template;
    }

    public function getView() {
        $this->template->renderTemplate('index.html');
    }

}

Dependency injection container (DIC)

The structure that has the responsibility of injecting dependencies (e.g. wiring) is called dependency injection container (DIC). Examples: PHP-DI, or Auryn). It is constructed and used only at the entry point of your app (like in index.php or bootstrap.php). In it you will register your app dependencies, and it will automatically take care of injecting them all over. Your App class would then be redundant. And your application will "concentrate" itself on its actual purpose, not on the creation and/or implementation of the structures that it requires in order to be able to complete its tasks. The related term is Inversion of Control (IoC).

Service locator

As a note: don't pass the DIC as dependency. If you do that you are "creating" a so called service locator. It would mean that your class would depend on the container to fetch ("locate") some of its dependencies, or to run some of its processes through it.

Tight coupling

In analogy to the previous point: don't create objects using the new keyword inside a class, because you are making the class dependent of another - tight coupling. See this. The solution is the same as above: the dependency injection.

Btw, in your code you are coupling each controller to the App class as well. As mentioned, this should be avoided.

So, in the Template class you should simply have:

class Template {

    private $environment;

    public function __construct(Twig_Environment $environment) {
        $this->environment = $environment;
    }

    public function render($template, array $vars) {
        return $this->environment->render($template, $vars);
    }

}

and let the creation of the Twig environment to be performed in some other place in your code (e.g. by the DIC). Note the return statement, instead of echo: let the output of the rendered content happen on a higher level, e.g. at the bootstrap level (like index.php, or bootstrap.php) - see last code below.

Library for the main classes

Having said that, you could have a folder (like library or framework) on the project root niveau, in which all core classses such as the following could reside: Template, View, model layer constructs (like data mappers, repositories, db adapters, services, etc), Config, Session, HTTP related classes (Request, Response, Stream, Uri, etc). See this and this.

Routing

I see a routes.php file in your structure. So I am assuming that you are already familiar with the fact that the "translation" (parsing) of an URI - passed into the client (a browser) - into a route object containing the informations about a specific controller action (including the action parameters) is the responsibility of a router (most likely composed of more classes: Route, Dispatcher, Router, etc). Example of implementations: FastRoute, Aura.Router.

Front controller (request dispatching)

The task of dispaching the request, e.g. of finding - and, eventually, creating - a route object in a predefined routes collection (based on the provided URI) and calling the controller action (with the action parameters as arguments), is of the so called front controller. It will receive the router and a dispatcher as dependencies. In short, this front controller grabs a route object from the router and passes its informations to the dispatcher, which is responsible for calling the action. An example:

class FrontController {

    private $router;
    private $dispatcher;

    public function __construct(Router $router, Dispatcher $dispatcher) {
        $this->router = $router;
        $this->dispatcher = $dispatcher;
    }

    public function routeAndDispatch(ServerRequestInterface $request, ResponseInterface $response) {
        $route = $this->router->route($request->getMethod(), $request->getUri()->getPath());
        $this->dispatcher->dispatch($route, $request, $response);
        return $this;
    }

}

A good tutorial on this theme is presented in here, and the continuation here.

HTTP message abstraction (PSR-7)

Having in mind the nature of the web applications, based on request and response, you should familiarize yourself a bit with the PSR-7 recommendation. It provides an abstraction of the HTTP message - composed of a HTTP request and a HTTP response. A good standalone library is Zend-Diactoros.

Namespaces vs. file system structure (PSR-4)

Regarding the file system/namespaces structure: As per PSR-4 recommendation, a file with a class in it should have the same name as the class. Also, if you use imports (use keyword), then you don't have to use the fully qualified class name in other places of the code too. So, correct is like:

namespace App\Controllers\Frontend\Guest;

use App\App;

App::getProvider(...)->...;

Controllers and views - 1:1 relation

Please note that a view-controller relation could be 1:1. Their action methods would be then called separately (in the front controller):

// Call the controller action.
call_user_func_array([$controller, "login"], <action-params>);

// Call the view action.    
call_user_func_array([$view, "login"], <action-params>);

// Call the output method of the view and print.
echo call_user_func_array([$view, "output"]);

In this constellation both, controller and view, can share different objects - like services, or domain objects (by many called "models"), or data mappers, etc. Let's say, that a controller and a view share a domain object - for example a "User" object. That means that both receive a certain domain object as constructor argument. This object will be, of course, automatically injected by the DIC.

First separation of concerns

By doing so, a first separation of concerns takes place:

  • The controller changes the state of the domain object (e.g. the value of its properties) and no more than that. No more "presentation"-related tasks...
  • All these "presentation"-related tasks remain the duty of the view - as it should, isn't it? And because the view receives the same "User" object as argument, it has access to the state values already set/changed by the controller. In the situation here the domain object is responsible with the db interaction too - not good though. Anyway, the view puts the template instance to work, requests the domain object to query the db and to return the results, and, in the end, returns the rendered content to be presented on screen with a simple echo.

Like:

class PdoAdapter implements AdapterInterface {

    private $connection;

    public function __construct(PDO $connection) {
        $this->connection = $connection;
    }

    public function fetchColumn(string $sql, array $bindings = [], int $columnNumber = 0) {
        $statement = $this->connection->prepare($sql);
        $statement->execute($bindings);
        return $statement->fetchColumn($columnNumber);
    }

}

class LoginController {

    private $user;

    public function __construct(UserInterface $user) {
        $this->user = $user;
    }

    public function login($name, $password) {
        $this->user->setName($name);
        $this->user->setPassword($password);
    }

}

class User implements UserInterface {

    private $adapter;
    private $name;
    private $password;

    public function __construct(AdapterInterface $adapter) {
        $this->adapter = $adapter;
    }

    public function getName() {
        return $this->name;
    }

    public function setName($name) {
        $this->name = $name;
    }

    public function getPassword() {
        return $this->password;
    }

    public function setPassword($password) {
        $this->password = $password;
    }

    public function checkLoggedIn($name, $password) {
        $sql = 'SELECT COUNT(*) FROM users WHERE name=:name AND password=:password LIMIT 1';
        $bindings = [
            ':name' => $name,
            ':password' => $password,
        ];

        return $this->adapter->fetchColumn($sql, $bindings) > 0;
    }

}

class LoginView {

    private $user;
    private $template;

    public function __construct(UserInterface $user, Template $template) {
        $this->user = $user;
        $this->template = $template;
    }

    public function login() {
        //...
    }

    public function output() {
        return $this->template->render('index.html', [
            'loggedIn' => $this->user->checkLoggedIn(
                $this->user->getName()
                , $this->user->getPassword()
            ),
        ]);
    }

}

Second separation of concerns - the MVC goal

In order to further separate the tasks, use data mappers, as intermediaries between the domain objects and the db (or the persistence layers, in general). E.g. in order to transfer data between the domain objects and the db.

In this step, the business logic, represented in and by the domain objects, become completely separated from any other app components. With this step, the actual goal of the MVC pattern is achieved: the decoupling of business logic from any other app structures/processes.

This can be seen in the following example: the User entity contains now only its properties and the methods related to them. The db functionality belongs now to the data mapper.

<?php

class PdoAdapter implements AdapterInterface {
    // ... The same ...
}

class UserMapper implements UserMapperInterface {

    private $adapter;

    public function __construct(AdapterInterface $adapter) {
        $this->adapter = $adapter;
    }

    public function checkLoggedIn($name, $password) {
        $sql = 'SELECT COUNT(*) FROM users WHERE name=:name AND password=:password LIMIT 1';
        $bindings = [
            ':name' => $name,
            ':password' => $password,
        ];

        return $this->adapter->fetchColumn($sql, $bindings) > 0;
    }

}

class LoginController {

    private $user;
    private $mapper;

    public function __construct(UserInterface $user, UserMapperInterface $mapper) {
        $this->user = $user;
        $this->mapper = $mapper;
    }

    public function login($name, $password) {
        $this->user->setName($name);
        $this->user->setPassword($password);
    }

}

class User implements UserInterface {

    private $name;
    private $password;

    public function getName() {
        return $this->name;
    }

    public function setName($name) {
        $this->name = $name;
    }

    public function getPassword() {
        return $this->password;
    }

    public function setPassword($password) {
        $this->password = $password;
    }

}

class LoginView {

    private $user;
    private $mapper;
    private $template;

    public function __construct(UserInterface $user, UserMapperInterface $mapper, Template $template) {
        $this->user = $user;
        $this->mapper = $mapper;
        $this->template = $template;
    }

    public function login() {
        //...
    }

    public function output() {
        $loggedIn = $this->mapper->checkLoggedIn(
                $this->user->getName()
                , $this->user->getPassword()
        );

        return $this->template->render('index.html', [
                    'loggedIn' => $loggedIn
        ]);
    }

}

Further separations/optimizations

  • (Optional) Use repositories to intermediate between the domain objects and the data mappers.
  • Use services to interact with the domain objects and the data mappers/repositories.
  • Share the services between the controllers and the views.

This great answer describes this part in detail.

And as last thing: all these dependencies in the examples are managed by the dependency injection container.


How to wire all together (steps):

• Define all "config" definitions, which will be registered into the dic. I speak about configuration values only, which will be further read by other "general" definitions of the dic, in order to create objects. The "config" definitions will probably reside in configuration files (e.g. definition files). So, for example, the definition file for the twig components could look like this:

config/configs/twig.php:

<?php

return [
    'twig' => [
        'options' => [
            'debug' => TRUE,
            'charset' => 'UTF-8',
            'base_template_class' => 'Twig_Template',
            'strict_variables' => FALSE,
            'autoescape' => 'html',
            'cache' => __DIR__ . '/../../storage/cache/twig',
            'auto_reload' => TRUE,
            'optimizations' => -1,
        ],
        'extensions' => [/* Indexed array with Twig_Extension_xxx instances */
            new Twig_Extension_Debug(),
        ],
    ],
];

• Define all "general" definitions, which will be registered into the dic and are used to create objects. But I don't speak here about the "specific" definitions, used to create specific objects like controllers, views, domain ojects, data mappers, etc. The "general" definitions will probably reside in one file. For example:

config/generals.php:

<?php

// Here come the imports...

return [
    'router' => function (ContainerInterface $c) {
        $dispatcher = 'Here create the route dispatcher...';
        return new Router($dispatcher);
    },
    'request' => factory([ServerRequestFactory::class, 'build']), /* Built with defaults */
    'response' => function (ContainerInterface $c) {
        return new Response(new Stream() /* defaults */);
    },
    'twigEnvironment' => function (ContainerInterface $c) {
        $loader = new Twig_Loader_Filesystem();
        $options = $c->get('twig')['options'];
        $environment = new Twig_Environment($loader, $options);

        $extensions = $c->get('twig')['extensions'];
        foreach ($extensions as $extension) {
            $environment->addExtension($extension);
        }

        return $environment;
    },
];

• Define all "specific" definitions, which will be registered into the dic and are used to create objects. I speak here about the definitions used to create controllers, views, domain ojects, data mappers, etc, but only for the dependencies type-hinted with interfaces or not type-hinted at all ("skalar" parameters). These "specific" definitions will probably reside in one file:

config/specifics.php:

<?php

use function DI\get;
use function DI\object;
use Mymvc\Models\User\User;
use Mymvc\Views\Login\LoginView;
use Mymvc\Models\User\UserMapper;
use Mymvc\Controllers\Login\LoginController;

return [
    LoginController::class => object()
            ->constructorParameter('user', get(User::class))
            ->constructorParameter('mapper', get(UserMapper::class)),
    LoginView::class => object()
            ->constructorParameter('user', get(User::class))
            ->constructorParameter('mapper', get(UserMapper::class)),
];

These "specific" definitions are based on the following LoginController:

<?php

namespace Mymvc\Controllers\Login;

use Mymvc\Models\User\UserInterface;
use Mymvc\Models\User\UserMapperInterface;

class LoginController {
    public function __construct(UserInterface $user, UserMapperInterface $mapper) {
        //...
    }
}

and on the following LoginView:

<?php

namespace Mymvc\Views\Login;

use Mylib\Presentation\Template;
use Mymvc\Models\User\UserInterface;
use Mymvc\Models\User\UserMapperInterface;

class LoginView {

    public function __construct(UserInterface $user, UserMapperInterface $mapper, Template $template) {
        //...
    }

}

Important: The Template dependency of the LoginView must not be defined in the "config/specifics.php" file, because for all concrete type-hints the DIC automatically creates instances! In this so-called autowiring process lies the real power of any DIC.

Note that get & object in "config/specifics.php" are functions of the DIC.

• Define all routes, which will be registered into the router. They could reside in config/routes.php

• In index.php only include the bootstrap file. So, index.php contains only one line:

<?php require_once '../bootstrap.php'; ?>

From here on the work is made in bootstrap.php, which resides directly in the project root.

• Load Composer autoloader.

• Create an instance of the DIC builder.

• Register all previously defined definitions into the DIC.

• Compile the container, e.g. the DIC.

• Get the router from the DIC.

• Register all previously defined routes into the router.

• Read the uri from the browser.

• Pass it to the router and call the dispatch method of the router. The router compares the uri with each registered route and, if it finds a match, it will return an array with the informations contained in the registered route ("route infos" array).

• The route infos are: the controller name, the action name and the action parameters list.

• Get the request object from the DIC by reading the 'request' entry from it and get the response object from the DIC by reading the 'response' entry from it. The generated response object will be probably used along the way before the controller action is called. For example, if you want to create a controller instance, but no controller class exists, then you'll use the response object to output an error message or a customized error page.

• Save the route infos into the request object as "attributes", e.g. with the withAttribute() method of the PSR-7 recommendation. From hear on read them only from the request object.

• Register the request object into the DIC by setting an entry ServerRequestInterface::class into it with the value of the request object. This way, because of autowiring, each time a dependency ServerRequestInterface is needed everywhere the assigned request object containing the route infos will be automatically injected.

$container->set(ServerRequestInterface::class, $request);

• If you don't use the response object for some operations before calling controller/view action anymore, then you can already register it into the DIC by setting an entry ResponseInterface::class. This way, because of autowiring, each time a dependency ResponseInterface is needed everywhere the assigned response will be automatically injected.

$container->set(ResponseInterface::class, $response);

• If an object of a certain type don't need to perform operations before the controller/view action is called, then you can register it as entry of DIC from the beginning. Otherwise, similar with request/response objects, create an instance of that type, use it, then register it afterwords. For example, a session object is needed to perform some session functions. After these ops are finished, a 'Session::class` entry can be registered in DIC.

• Based on the route infos and on the paths/namespaces registered as configs (e.g. "config" definitions in DIC) build the fully qualified class name (FQN) of the controller and of the view.

• Call the controller action. If not, no problem: the view action of the view is called. Make also proper validations (file/class exist, method exists). But use the call() method of the container (e.g. DIC), so that the automatic injection takes place!

• Here is the most awaited moment: create a Template object (by using the configs/paths/namespaces registered in DIC inclusive the 'twigEnvironment' entry) and register it into DIC. For example:

In config/configs/app.php:

<?php

return [
    'paths' => [
        // This folder contains the templates and the layouts of the application, used by your view classes if no "specific" layouts/templates are used.
        'appTemplates' => __DIR__ . '/../../resources/templates/app'
        // This folder contains the templates and the layouts loaded/rendered by the specific view classes (LoginView, HomeView, AboutUsVIew, etc).
        'customTemplates' => __DIR__ . '/../../resources/templates/app'
    ],
];

And now in bootsptrap.php:

$appTemplatesPath = $container->get('appTemplates');
$customTemplatesPath = $container->get('customTemplates');
$twigEnvironment = $container->get('twigEnvironment');

$template = new Template(
    $twigEnvironment
    , $appTemplatesPath
    , $customTemplatesPath
);

$container->set(Template::class, $template);

• Call the view action if exists. If not, no problem: the 'output' method of the view is called. Make also proper validations (file/class exist, method exists). And use the call() method of the container (e.g. DIC), so that the automatic injection takes place!

• Call the 'output' method of the view and print the result with echo. If none exists then throw an exception: the program is broken. Make also all proper validations (file/class exist, method exists). And use the call() method of the container (e.g. DIC), so that the automatic injection takes place!

• The end...

Good luck.

Original Thread

By anonymous    2018-03-18

Make the database property protected.

Also: $this->$database is wrong. Correct is $this->database.

Be aware that your class design is based on the service locator antipattern. E.g. you are passing the whole container to your controllers in order to get the database object from it. Don't do this! Inject only the resources that a class needs. E.g. your controller should receive only the database instance as dependency. Take a look at this, this, this and, especially, this.

You don't actually want to inject the database instance either. You should inject it into proper data mappers. And the data mappers should be injected into the controllers. In principle, the process of accessing data from a persistence layer is the responsibility of more MVC components. Read this and this. And this serie for code examples involving domain objects, database adapters, data mappers, repositories and services: Part 1, Part 2, Part 3 and Part 4.

A note: in Slim you can use the Eloquent ORM. See this.

Suggestion: You should learn OOP before starting (using) an MVC and familiarize yourself with the SOLID principles too.

class Controller {

    private $container;
    protected $database;

    public function __construct($container) {
        $this->container = $container;
        $this->database = $this->container->get('db');

        $lieux = $this->database->query('SELECT * FROM Lieu');
        var_dump($lieux);
    }

    public function render(ResponseInterface $response, $file) {
        $this->container->view->render($response, $file);
    }

}

class PagesController extends Controller {

    public function getLieu(RequestInterface $request, ResponseInterface $response) {
        $lieux = $this->database->query('SELECT * FROM Lieu');
        var_dump($lieux);

        $this->render($response, 'pages/lieu.twig');
    }

}

Original Thread

Submit Your Video

If you have some great dev videos to share, please fill out this form.