geekcell / container-facade
一个用于为PSR-11兼容容器服务创建静态外观的简单库。
Requires
- psr/container: ^2.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.14
- mockery/mockery: ^1.5
- phpstan/phpstan: ^1.9
- phpstan/phpstan-mockery: ^1.1
- phpunit/phpunit: ^10.0
README
一个受Laravel的外观实现启发的独立PHP库,可以与任何PSR-11兼容的依赖注入容器(DIC)一起使用,例如(由PHP-DI、Symfony、Pimple或Slim使用的)依赖注入容器。
安装
要使用此包,请使用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
目录中的各种示例项目,其中包含此包的最小集成。