affinity4 / 支持
向任何PSR-11容器应用程序添加Laravel风格的门面、特性和辅助函数
Requires
- php: ^8.1
- psr/http-factory: ^1.0
- symfony/var-dumper: ^6.4
Requires (Dev)
- guzzlehttp/psr7: ^2
- php-di/php-di: ^7.0
- php-di/slim-bridge: ^3.4
- phpunit/phpunit: ^10
- slim/slim: ^4.0
README
向任何SlimPHP应用程序添加Laravel风格的门面、特性和辅助函数
安装
composer require affinity4/slimphp-support
使用方法
在您的应用程序中设置门面
要使用SlimPHP门面,您首先需要像往常一样创建您的Slim应用程序,使用Slim\App\AppFactory
或DI\Container\Slim\Bridge
。然后,您需要调用Affinity4\SlimSupport\Support\Facade::setFacadeApplication($app)
use Slim\Factory\AppFactory; use Affinity4\SlimSupport\Support\Facade; $app = AppFactory::createFromContainer(); Facade::setFacadeApplication($app);
现在,您将能够访问所有门面以及辅助函数(例如response()
)
应用程序门面
Slim\App
的门面
use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Slim\Factory\AppFactory; use Affinity4\SlimSupport\Support\Facade; $app = AppFactory::createFromContainer(); Facade::setFacadeApplication($app); App::get('/', function(RequestInterface $request, ResponseInterface $response) { // return ... }); App::run();
容器
use Affinity4\SlimSupport\Facades\Container; Container::set('some-service', function () { return SomeService(); }); if (Container::has('some-service')) { $someService = Container::get('some-service'); }
响应
JSON响应
use Affinity4\SlimSupport\Facades\Container; App::get('/', function($request) { return Response::json(['test' => 'payload'])->get(); });
管道门面
注意:有关详细示例,请参阅管道支持类部分。
App::get('/', function ($request) { // 4. Define the pipeline $result = (new Pipeline(App::getContainer())) ->send($request) ->through([ PrepareRequest::class, ValidateRequest::class, TransformRequest::class, SaveRequest::class, ]) ->thenReturn(); // 5. Respond with the processed data return response()->json(['result' => $result])->get(); });
辅助函数
response()
标准应用程序/text响应
App::get('/', function ($request) { return response('Hello World')->get(); });
标准JSON响应
App::get('/', function ($request) { return response()->json(['data' => 'payload'])->get(); });
tap()
return tap(new Psr7Response(), function ($response) { $response->getBody()->write('foo'); });
特性
可触摸
use Affinity4\SlimSupport\Support\Traits\Tappable; class TappableClass { use Tappable; private $name; public static function make() { return new static; } public function setName($name) { $this->name = $name; } public function getName() { return $this->name; } } $name = TappableClass::make()->tap(function ($tappable) { $tappable->setName('MyName'); })->getName(); // Or, even though setName does not return this you can now just chain from it! $name = TappableClass::make()->tap()->setName('MyName')->getName()
可宏化
宏允许您动态地向类添加方法(而无需修改其代码)。
假设您厌倦了必须这样做
$app->get('/', function ($request, $response) { $response = new Response; $response->getBody()->write('Hello'); return $response; })
相反,您只想直接从$response
实例调用写方法。首先,我们需要扩展响应类以便我们可以使用Macroable
特性,但仍然保留所有我们的基本响应方法。
use GuzzleHttp\Psr7\Response; use Affinity4\SlimSupport\Support\Traits\Macroable; class MacroableResponse extends Response { use Macroable; }
然后,我们需要将MacroableResponse
添加到我们的容器中,这样我们总是处理相同的实例(不是所有实例都将具有“宏化”的方法)。
use Affinity4\SlimSupport\Facades\Container; // ... above code here Container::set('response', function () { return new MacroableResponse(); });
然后,我们可以从容器中获取我们的MacroableResponse
实例,并只需调用write
!
App::get('/', function () { return Container::get('response')->write('Macro!'); });
条件性
允许条件性地链式调用功能。
例如,让我们假设我们有一个标准的PSR-11容器,它具有最小的PSR-11兼容方法,即set
、get
和has
。set
方法向容器添加服务,get
返回服务,has
检查服务是否在容器中。
我们有一个Logger
想要添加到容器中,但它需要一个已经在容器中的FileDriver
,或者我们需要首先将FileDriver
类添加到容器中。
我们可能会有一些这样的引导逻辑
$container = new Container; if (!$container->has('FileDriver')) { $container->set('FileDriver', fn() => new FileDriver); } if (!$container->has('Logger')) { $container->set('Logger', function ($container) { $logger = new Logger; $logger->setDriver($container->get('FileDriver')); return $logger; }); }
但是,如果我们扩展我们的Container
类并添加Conditionable
特性,我们可以使用unless
方法以流畅的接口执行此检查
注意:要检查相反的情况,还有when
。
class ConditionableContainer extends Container { use Conditionable; } $container = new ConditionableContainer; $container ->unless( fn($container) => $container->has('FileDriver'), function ($container) { $container->set('FileDriver', fn() => new FileDriver); } )->unless( fn($container) => $container->has('Logger'), function ($container) { $container->set('Logger', function ($container) { $logger = new Logger; $logger->setDriver($container->get('FileDriver')); return $logger; }); } );
您可能认为这仍然相当冗长,因此为了清理它,您可以创建所有$container->set
逻辑的invokable
服务工厂类。
class FileDriverServiceFactory { public function __invoke($container) { $container->set('FileDriver', fn() => new FileDriver); } } class LoggerServiceFactory { public function __invoke($cotnainer) { $logger = new Logger; $logger->setDriver($container->get('FileDriver')); return $logger; } } $container = new ConditionableContainer; // or, using unless, instead of when $container ->unless(fn($container) => $container->has('FileDriver'), FileDriverServiceFactory($container)) ->unless(fn($container) => $container->has('Logger'), LoggerServiceFactory($container));
可转储
向任何类添加dump
和dd
方法
class Collection { use Dumpable; public function __constructor( protected array $collection = [] ) {} } $collection = new Collection([ "one" => 1, "two" => 2 ]); // Debug the collection... $collection->dump(); // Or $collection->dd();
结果将是
DumpableCollection {#69 ▼ #collection: array:1 [▼ "one" => 1, "two" => 2 ] }
注意:如果您想附加额外的转储数据,也可以像通常一样将...$args
传递给dd和dump方法。
转发调用
将当前类中缺失的方法的调用代理到另一个目标类。当您不能继承或修改一个类但想向它添加一些功能时非常有用(当然,除了覆盖它的任何方法之外)。
以下是一个示例,其中我们有一个基类App
,但它是一个最终类,所以我们不能继承它。因此,我们创建了一个AppProxy
类,允许我们说“任何在AppProxy
上调用而AppProxy
中不存在的App
方法,我们使用App
代替”
class AppProxy { use ForwardsCalls; public function __call($method, $parameters) { return $this->forwardCallTo(new App, $method, $parameters); } public function addSomeServiceDirectlyToContainer() { $this->getContainer()->set('some-service', function ($container) { return new SomeService($container->get('some-dependency-already-in-container')); }); } } final class App { public function __construct( protected ContainerInterface $container ) {} public function getContainer() { return $this->container; } }
然后,我们可以通过调用AppProxy
来使用App
中的getContainer
(或任何其他公共方法/属性)
$appProxy = new AppProxy; $app->addSomeServiceDirectlyToContainer(); $container = $appProxy->getContainer(); dd($congainer->get('some-service')); /* SomeService {# 46 # some_service_already_in_container: someServiceAlreadyInContainer {# 30 } ... } */
管道支持类
管道允许使用类似中间件的接口来链式处理任务。
管道处理每个任务,并将返回的值传递给链中的下一个处理过程。
它们适用于多步数据处理、HTTP中间件、数据库查询和验证任务。
以下是一个使用示例,演示如何使用它来验证、过滤、转换并保存传入的GET请求。
// 1. Prepare the request class PrepareRequest { public function handle($request, $next) { $uri = $request->getUri(); $query = $uri->getQuery(); // Get the query string (e.g., "param1=value1¶m2=value2") parse_str($query, $queryParams); // Parse the query string into an array return $next($queryParams); } } // 2. Validate the request class ValidateRequest { public function handle($data, $next) { // Validate parameters // (e.g. check if 'email' and 'password' exist, validate 'email' and 'password' etc) // If invalid then $data['valid'] = false, else $data['valid'] = true; return $next($data); } } // 2. Transform the request class TransformRequest { public function handle($data, $next) { $data['password'] = bcrypt($data['password']); return $next($data); } } // 3. Save the data, or log errors class SaveRequest { public function handle($data, $next) { if (!$data['valid']) { // Log errors... return $next($data); } $data['saved'] = true; return $next($data); } } App::get('/', function ($request) { // 4. Define the pipeline $result = (new Pipeline(App::getContainer())) ->send($request) ->through([ PrepareRequest::class, ValidateRequest::class, TransformRequest::class, SaveRequest::class, ]) ->thenReturn(); // 5. Respond with the processed data return response()->json(['result' => $result])->get(); });
这样,我们的控制器保持干净、易于阅读,并且每个责任都分配到它自己的类中,以便长期维护更简单。这也会使测试更容易,因为您可以测试单个类,也可以测试整个管道结果,而无需测试控制器本身。
中心
一个中心
类,是一种存储类似组管道的方法,以便可以从同一对象检索和执行它们。
$app = AppFactory::create(); $userWorkflows = new Hub($app->getContainer()); // By default register the user $userWorkflows->defaults(function ($pipeline, $passable) { return $pipeline->send($passable) ->through([ ValidateRequest::class, RegisterUser::class, SendRegistrationEmail::class ]) ->thenReturn(); }); $userWorkflows->pipeline('user-requested-reset-password', function ($pipeline, $passable) { return $pipeline->send($passable) ->through([ ValidateRequestData::class, ValidateUser::class, EmailResetPasswordLink::class ]) ->thenReturn(); }); $userWorkflows->pipeline('user-enabled-2fa', function ($pipeline, $passable) { return $pipeline->send($passable) ->through([ ValidateRequestData::class, ValidateUser::class, Handle2faSetup::class ]) ->thenReturn(); }); // Then we can call them easily like so App::post('/user/register', function($request) use ($userWorkflows) { $result = $userWorkflows->pipe($request); // Since our default is our register pipe we only need the first arg return response()->json(['data' => $result])->get(); }); App::post('/user/password-reset', function($request) use ($userWorkflows) { $result = $userWorkflows->pipe($request, 'user-requested-password-reset'); return response()->json(['data' => $result])->get(); }); App::post('/user/enable-2fa', function($request) use ($userWorkflows) { $result = $userWorkflows->pipe($request, 'user-enabled-2fa'); return response()->json(['data' => $result])->get(); });