geekcell / container-facade

一个用于为PSR-11兼容容器服务创建静态外观的简单库。

1.0.0 2023-02-18 14:58 UTC

This package is auto-updated.

Last update: 2024-09-19 15:37:46 UTC


README

Unit tests workflow status Coverage Bugs Maintainability Rating Quality Gate Status

一个受Laravel的外观实现启发的独立PHP库,可以与任何PSR-11兼容的依赖注入容器(DIC)一起使用,例如(由PHP-DISymfonyPimpleSlim使用的)依赖注入容器。

安装

要使用此包,请使用Composer要求。

composer install geekcell/container-facade

动机

尽管罕见,但有时你希望在不需要依赖注入的情况下获取容器服务。一个例子是AggregateRoot模式,它允许从聚合体中直接分发领域事件,这通常是通过直接创建而不是通过DIC创建的。在这种情况下,相应的(静态)服务外观可以提供与单例相当的便利性,但不会带来单例模式的固有缺点

用法

让我们假设你有一个在所选DIC中的Logger服务,该服务将消息记录到文件中。

<?php

namespace App\Service;

// ...

class Logger
{
    public function __construct(
        private readonly FileWriter $writer,
    ) {
    }

    public function log(string $message, LogLevel $level = LogLevel::INFO): void
    {
        $line = sprintf(
            '%s (%s): %s', 
            (new \DateTime)->format('c'),
            $level->value,
            $message,
        );

        $this->writer->writeLine($line);
    }
}

如果你想“外观”此服务,只需创建一个扩展GeekCell\Facade\Facade的类。

<?php

namespace App\Support\Facade;

use App\Service\Logger as LoggerRoot;
use GeekCell\Facade\Facade;

class Logger extends Facade
{
    protected static function getFacadeAccessor(): string
    {
        return 'app.logger';
    }
}

你必须实现getFacadeAccessor()方法,该方法返回DIC中服务的标识符。

此外,你必须将你的DIC“介绍”给外观。如何做到这一点完全取决于你使用的框架。在Symfony中,在src/Kernel.php中覆盖boot()方法是一个很好的机会。

<?php

namespace App;

use GeekCell\Facade\Facade;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;

class Kernel extends BaseKernel
{
    use MicroKernelTrait;

    public function boot()
    {
        parent::boot();

        // This is where the magic happens!
        Facade::setContainer($this->container);
    }
}

要在应用程序的任何部分中使用外观,只需像调用静态方法一样调用服务。在幕后,调用通过__callStatic委派给实际的容器服务。

<?php

// ...

use App\Support\Facade\Logger;

class SomeClass
{
    public function doStuff()
    {
        Logger::log('Calling ' __CLASS__ . '::doStuff()', LogLevel::DEBUG);

        // The acutal method logic ...
    }
}

测试

虽然上面的内容看起来像反模式,但实际上它非常适合测试。在单元测试期间,你可以使用swapMock()方法将实际的服务与一个Mockery模拟直接交换。

<?php

// ...

use App\Support\Facade\Logger;
use PHPUnit\Framework\TestCase;

class SomeClassTest extends TestCase
{
    public function tearDown(): void
    {
        Logger::clear();
    }

    // ...

    public function testDoStuff(): void
    {
        // Swap real service with mock
        $loggerMock = Logger::swapMock();

        // Set expectations for mock
        $loggerMock->shouldReceive('log')->once();

        $out = new SomeClass();
        $result = $out->doStuff(); // This will now call the mock!

        // Test assertions ...
    }
}

提示:你必须调用clear()方法来清除内部缓存的模拟实例。对于PHPUnit,你可以使用tearDown()方法来完成此操作。

注意事项

力量越大,责任越大。

尽管有有效的用例,并且尽管服务外观提供了高级便利性,但你仍然应该仅有限地使用它们,并在可能的情况下返回到标准依赖注入,因为所有外观都内部依赖于PHP的__callStatic魔术方法,这可能会使调试更加繁琐/困难。

示例

请参阅examples目录中的各种示例项目,其中包含此包的最小集成。