DEV Community

Lawrence Cooke
Lawrence Cooke

Posted on

Dependency Injection with Fat Free Framework

What is Fat Free Framework?

Fat Free Framework is an easy to set up, lightweight framework for PHP.

Fat Free Framework is a great tool for building small to medium sized projects in. It has enough tools to get you going, without a daunting number of settings to wade through in order for you to start your project.

If you are new to Fat Free, check out their website for instructions on how to set it up, and watch these tutorial videos

Out of the box configuration

Out of the box, Fat Free Framework is geared towards an MVC setup. With good code separation between the Controller, Model(Mapper) and View.

A simple Controller with a call to a Mapper might look like

<?php
declare(strict_types=1);

namespace app\Controller;

use app\Mapper\User_Mapper;
use Base;

final class Index_Controller extends Base_Controller {

    public function indexAction(Base $f3, array $args = []): void {
        $user = (new User_Mapper($f3->DB))->findone(['id = ?', 1]);
        $this->render('homepage', ['email' => $user->email]);
    }
}


Enter fullscreen mode Exit fullscreen mode

(Note that $this->render is a custom method to handle the View)

The Mapper Class would look like this:

<?php

namespace app\Mapper;

use DB\SQL;
use DB\SQL\Mapper;

class User_Mapper extends Mapper {
    public function __construct(SQL $DB) {
        parent::__construct($DB, 'users');
    }
}
Enter fullscreen mode Exit fullscreen mode

In a really simple project, this is enough, however, in a larger project, separating the logic from the Controller is important. Unit testing is also an important aspect of any project.

With the configuration shown above, as the code base gets larger, lines of code like $user = (new User_Mapper($f3->DB))->findone(['id = ?', 1]); start to get hard to read, hard to diagnose bugs in and difficult to unit test.

Separating Logic from the Controller

Separating logic will give the project better organization, and makes unit testing the logic a little easier.

In this example, moving the mapper call into its own logic class is a good starting point, keeping the Controller logic free.

The Controller might end up looking like

<?php
declare(strict_types=1);

namespace app\Controller;

use app\Logic\Index_Logic;
use Base;

final class Index_Controller extends Base_Controller {

    public function indexAction(Base $f3, array $args = []): void {
        $email = (new Index_Logic($f3))->getEmail(1);
        $this->render('homepage', ['email' => $email]);
    }
} 
Enter fullscreen mode Exit fullscreen mode

and the new logic class would have a method to get the email address using the mapper.

<?php

declare(strict_types=1);

namespace app\Logic;

use app\Mapper\User_Mapper;

class Index_Logic extends Logic_Base {

    public function getEmail(int $id): string {
        $user = (new User_Mapper($f3->DB))->findone(['id = ?', $id]);
        return  $user->email;
    }
}
Enter fullscreen mode Exit fullscreen mode

This achieves the code separation but the code is still not clean code, nor is it unit test friendly.

Dependency Injection

The simplest way to clean up the code and build more unit test friendly code is by adding in some dependency injection.

Fat Free Framework comes with the tools needed to add in Dependency Injection, but not without a little help.

While you could choose to use any DI library in your project, I have chosen to add Dice.

Dice is a lightweight Dependency Injection library, easy to set up and well suited to Fat Free Framework.

Setting Up Dice

Import Dice into your project using composer:

composer require level-2/dice

Once it's installed, it's time to configure it to work with Fat Free Framework.

In the init for the framework, a little bit of code is needed to incorporate Dice into it.

$f3 = Base::instance();

$dice = new Dice();

$f3->set('CONTAINER', function ($class) use ($dice) {
    return $dice->create($class);
});


$f3->run();
Enter fullscreen mode Exit fullscreen mode

We can now update the Controller code to use Dependency Injection

final class Index_Controller extends Base_Controller {

    protected $Index_Logic;

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

    public function indexAction(Base $f3, array $args = []): void {
        $email = $this->Index_Logic->getEmail(1);
        $this->render('homepage', ['email' => $email]);
    }
}
Enter fullscreen mode Exit fullscreen mode

While the Controller will work with this code, to have the Mapper and Logic classes work, a few additional changes are needed.

The Logic class needs $f3 injected into it for it to work and the Mapper needs to be given the database connection config.

To configure this, first update the init code to add a rule to Dice for the Database config and link it to the DB\SQL class built into Fat Free.

constructParams allows arguments that are needed for the class construct to be defined, and will be used when calling the class if the argument isn't defined in your call.

$f3 = Base::instance();

$dice = new Dice();

$dice = $dice->addRule(DB\SQL::class, [
    'constructParams' => [
        $dsn,
        $username,
        $password,
        [
            PDO::ATTR_EMULATE_PREPARES => false,
            PDO::ATTR_STRINGIFY_FETCHES => false,
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
        ]

    ],
    'shared' => true,
]);

$f3->set('CONTAINER', function ($class) use ($dice) {
    return $dice->create($class);
});


$f3->run();
Enter fullscreen mode Exit fullscreen mode

To pass $f3 into the Logic, we need to add a setter method to the Logic class and call the setter from the controller

We can use Fat Free Frameworks beforeRoute() method in the Controller to call the setter.

public function beforeroute(Base $f3) {
        $this->Index_Logic->set($f3);
}

Enter fullscreen mode Exit fullscreen mode

and update the Logic to include the Dependency Injection and the setter method

class Index_Logic extends Logic_Base {

    protected $User_Mapper;

    protected $f3;

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

    public function set($f3) {
        $this->f3 = $f3;
    }

    public function getEmail(int $id): string {
        $user = $this->User_Mapper->findone(['id = ?', $id]);
        return  $user->email;
    }
}
Enter fullscreen mode Exit fullscreen mode

It would be better to add the set() method to the Logic_Base class that the Index_Logic extends, so it could be called by any Logic classes without having to add the method into every class.

Now that the Dependency Injection is working and it's configured for the mapper, unit testing is now a much simpler task.

Unit Testing using PHP Unit

To unit test the code, first PHP Units bootstrap file needs to be configured for Fat Free.

After including the vendor autoload file, I've added a function that sets up Fat Free for unit testing. The function can then be called by the unit test file to define $f3 in the tests.

function setUpFatFree() {

    $f3 = Base::instance();

    $f3->config(PROJECT_ROOT_DIR . 'app/config/main_config.ini', true);

    $f3->set(
        'DB',
        new \DB\SQL(
            'mysql:host=127.0.0.1;port=3306;dbname=datbasename;',
            'username',
            'password',
            [
                \PDO::ATTR_EMULATE_PREPARES => false,
                \PDO::ATTR_STRINGIFY_FETCHES => false,
                \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION
            ]
        )
    );


    $dice = new Dice();

    $dsn = 'mysql:host=127.0.0.1;port=3306;dbname=databasename;';

    $dice = $dice->addRule(DB\SQL::class, [
        'constructParams' => [
            $dsn,
            'username',
            'password',
            [
                \PDO::ATTR_EMULATE_PREPARES => false,
                \PDO::ATTR_STRINGIFY_FETCHES => false,
                \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION
            ]
        ],
        'shared' => true,
    ]);

    $f3->set('CONTAINER', function ($class) use ($dice) {
        return $dice->create($class);
    });

    $f3->set('QUIET', true);

    return $f3;
}

Enter fullscreen mode Exit fullscreen mode

Unit Testing Logic Classes

To unit test the logic, call the setUpFatFree function from the unit test setup() method and add your test

final class Index_LogicTest extends TestCase {
    protected $f3;

    public function tearDown(): void {
    }

    public function setUp(): void {
        $this->f3 = setUpFatFree();
    }

    public function testGetEmail() {

        $User_Mapper = (new class($this->f3->DB) extends User_Mapper {
            function findone($filter = NULL, array $options = NULL, $ttl = 0) {
                $db_results = new stdClass;
                $db_results->email = 'test@test.com';
                return $db_results;
            }
        });

        $Index_Logic = new Index_Logic($User_Mapper);
        $Index_Logic->set($this->f3);
        $response = $Index_Logic->getEmail(1);
        $this->assertSame('test@test.com', $response);
    }
}
Enter fullscreen mode Exit fullscreen mode

In this code, we are mocking the Mapper so that we don't need to make a database call in the unit test, and injecting it into the class that is to be unit tested.

By adding the Dependency Injection, mocking the Mapper has become a much simpler task and the unit test is now not complicated to set up.

Unit Testing The Controller

Unit testing a Controller in Fat Free is a little more difficult. In Fat Free, Controller methods do not return values that can be tested.

We could set up a unit test similar to the Logic unit test

    public function testIndexAction() {
        $User_Mapper = (new class($this->f3->DB) extends User_Mapper {
            function findone($filter = NULL, array $options = NULL, $ttl = 0) {
                $db_results = new stdClass;
                $db_results->email = 'test@test.com';
                return $db_results;
            }
        });

        $Index_Logic = (new class($User_Mapper) extends Index_Logic {
        });

        $Index_Logic->set($this->f3);

        $Index_Controller = new Index_Controller($Index_Logic);
        $result = $Index_Controller->indexAction($this->f3, []);
        $this->assertNull($result);
    }
Enter fullscreen mode Exit fullscreen mode

In this test, the Mapper is mocked and injected into the Logic and the Logic injected into the Controller method.

There are a couple of problems with this test, the assertNull really doesn't prove that the controller is working. Without the Controller returning a value to test against, assertNull is always going to be true.

The other issue is that while the unit test is running, the unit test will echo what the controllers $this->render() responds with onto the screen, which will be messy of you need to see what is happening while the unit test is running.

This could be worked around by adding a Fat Free variable into the setUpFatFree Function $f3->set('UNITTEST',1); and then in the render method, adding an IF statement checking if UNITTEST is true or false.

Fat Free Framework is better suited to integrated testing rather than mocked testing. This avoids the issue of text being sent to the screen during the test and makes the test useful by being able to check the response from the view.

In order to unit test the Controller properly, it needs a database to connect to, and it needs to run the route that the controller method is called from.

If you use a vagrant box for development, a test database could be setup inside the vagrant and configure vagrant to connect to it. Alternatively, using SQLite is an option for the unit testing.

Integrated Testing

Creating an integrated test for Fat Free is a two step process. Mock the route, assert against what $this->render responds with.

   public function testIndexAction() {
        $this->f3->mock('GET /');
        $this->assertSame('test@test.com', $this->f3->get('RESPONSE'));
    }

Enter fullscreen mode Exit fullscreen mode

In this test, no Mappers are mocked, $f3->mock allows us to run the route that would be run to trigger the Controller method, and we can assert against the response from the route.

Final Code

Mapper

<?php

namespace app\Mapper;

use DB\SQL;
use DB\SQL\Mapper;

class User_Mapper extends Mapper {
    public function __construct(SQL $DB) {
        parent::__construct($DB, 'users');
    }
}
Enter fullscreen mode Exit fullscreen mode

Controller

<?php

declare(strict_types=1);

namespace app\Controller;

use app\Logic\Index_Logic;
use Base;

final class Index_Controller extends Base_Controller {

    protected $Index_Logic;

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

    public function beforeroute(Base $f3) {
        $this->Index_Logic->set($f3);
    }

    public function indexAction(Base $f3, array $args = []): void {
        $email = $this->Index_Logic->getEmail(1);
        $this->render('homepage', ['email' => $email]);
    }
}
Enter fullscreen mode Exit fullscreen mode

Logic

<?php

declare(strict_types=1);

namespace app\Logic;

use app\Mapper\User_Mapper;

class Index_Logic extends Logic_Base {

    protected $User_Mapper;

    protected $f3;

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

    public function set($f3) {
        $this->f3 = $f3;
    }

    public function getEmail(int $id): string {
        $user = $this->User_Mapper->findone(['id = ?', $id]);
        return  $user->email;
    }
}


Enter fullscreen mode Exit fullscreen mode

Top comments (1)

Collapse
 
n0nag0n profile image
n0nag0n

Fat-Free is an amazing framework! What a great write up on how to level up your code in F3 and utilize the CONTAINER variable!