依赖注入与脂肪的框架
#php #dependencyinjection #fatfreeframework

什么是免费脂肪的框架?

Fat Free Framework是一个易于设置的,用于PHP的轻量级框架

无脂肪框架是建立中小型项目的绝佳工具。它具有足够的工具,可以使您继续前进,而无需涉足的设置涉水以便您开始项目。

如果您不熟悉脂肪,请访问他们的网站以获取有关如何设置的说明,watch these tutorial videos

开箱即用配置

开箱即用,无脂肪框架用于MVC设置。在控制器,模型(映射器)和视图之间进行良好的代码分离。

一个简单的控制器,呼叫映射器可能看起来像

<?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]);
    }
}


(请注意,$this->render是一种自定义方法来处理视图)

映射器类看起来像这样:

<?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');
    }
}

在一个非常简单的项目中,这已经足够了,但是,在一个较大的项目中,将逻辑与控制器分开很重要。单位测试也是任何项目的重要方面。

使用上面显示的配置,随着代码库的较大,$user = (new User_Mapper($f3->DB))->findone(['id = ?', 1]);之类的代码行开始很难阅读,很难诊断出错误,并且很难单位测试。

将逻辑与控制器分开

分开逻辑将使项目更好地组织,并使单元测试逻辑更加容易。

在此示例中,将映射器调用到自己的逻辑类中是一个很好的起点,可以使控制器逻辑免费。

控制器可能最终看起来像

<?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]);
    }
} 

和新的逻辑类将有一种使用映射器获取电子邮件地址的方法。

<?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;
    }
}

这实现了代码分离,但代码仍然不是干净的代码,也不是单位测试友好的。

依赖注入

清理代码并构建更多单元测试友好代码的最简单方法是添加一些依赖项注入。

不含脂肪的框架附带添加依赖注入所需的工具,但没有一点帮助。

虽然您可以选择在项目中使用任何DI库,但我选择添加 Dice

DICE是一个轻巧的依赖注入库,易于设置,非常适合无脂肪框架。

设置骰子

使用作曲家将骰子导入您的项目:

composer require level-2/dice

安装后,是时候配置它与无脂框架一起使用。

在框架的初始化中,需要一点代码才能将骰子纳入其中。

$f3 = Base::instance();

$dice = new Dice();

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


$f3->run();

我们现在可以更新控制器代码以使用依赖项注入

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]);
    }
}

虽然控制器将使用此代码,但要使映射器和逻辑类工作,但需要进行一些其他更改。

逻辑类需要$ f3注入它才能使其工作,并且需要给出映射器的数据库连接配置。

要配置此信息,请首先更新INIT代码,以将数据库配置骰子添加到DICE,然后将其链接到db \ sql类内置在Free中。

constructParams允许定义类构造所需的参数,如果在呼叫中未定义的参数(如果参数未定义)时,将在呼叫类时使用。

$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();

要将$ f3传递到逻辑中,我们需要在逻辑类中添加一个setter方法,然后从控制器

调用设置器

我们可以在控制器中使用无脂肪框架beforeroute()方法来调用设置器。

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

并更新逻辑以包括依赖项注入和设置器方法

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;
    }
}

最好将set()方法添加到index_logic扩展的logic_base类中,因此任何逻辑类都可以调用它,而无需将方法添加到每个类中。

既然依赖项注入正在工作,并且已为映射器配置,则单位测试现在是一个更简单的任务。

使用PHP单元进行单元测试

要进行单元测试代码,首先需要将Bootstrap文件配置为免费脂肪。

包含供应商自动加载文件后,我添加了一个功能,该功能可设置为单位测试的脂肪。然后可以由单元测试文件调用该函数以在测试中定义$ f3。

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;
}

单元测试逻辑类

要单元测试逻辑,请调用单元test Setup()方法的setUpfatfatree函数,然后添加您的测试

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);
    }
}

在此代码中,我们嘲笑映射器,因此我们不需要在单元测试中进行数据库调用,并将其注入要进行单位测试的类。

通过添加依赖项注入,嘲笑映射器已成为一个更简单的任务,现在设置单位测试并不复杂。

单元测试控制器

单位测试无脂肪中的控制器要困难得多。在无脂肪的情况下,控制器方法不返回可以测试的值。

我们可以设置类似于逻辑单元测试的单元测试

    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);
    }

在此测试中,映射器被模拟并注入逻辑中,并将逻辑注入到控制器方法中。

此测试存在两个问题,SpersertNull确实没有证明控制器正在工作。如果没有控制器返回值进行测试的值,AspertNull将始终是正确的。

另一个问题是,当单元测试运行时,单元测试将回应控制器$ this-> render()在屏幕上响应什么单位测试正在运行。

可以通过在setupfatree函数$f3->set('UNITTEST',1);中,然后在render方法中添加无脂肪变量来解决这一问题,并添加if语句检查是否unittest是TRUE还是FALSE。

不含脂肪的框架更适合集成测试,而不是模拟的测试。这避免了测试期间要发送到屏幕的文本问题,并通过能够从视图中检查响应来使测试有用。

为了正确测试控制器,它需要一个数据库来连接,并且需要运行控制器方法从。

调用的路由。

如果您使用流浪汉进行开发,则可以在Vagrant内部设置测试数据库并配置Vagrant以连接到它。另外,使用SQLite是单元测试的选项。

集成测试

创建无脂肪的集成测试是两个步骤的过程。模拟路线,断言$ this>渲染的回应。

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

在此测试中,没有模拟映射器,$f3->mock允许我们运行将运行以触发控制器方法的路由,我们可以断言反对路线的响应。

最终代码

映射器

<?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');
    }
}

控制器

<?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]);
    }
}

逻辑

<?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;
    }
}