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 (10):

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

Recommended Books

    Submit Your Video

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