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]);
}
}
(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');
}
}
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]);
}
}
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;
}
}
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();
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]);
}
}
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();
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);
}
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;
}
}
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;
}
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);
}
}
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);
}
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'));
}
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');
}
}
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]);
}
}
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;
}
}
Top comments (1)
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!