affinity4 / slimphp-support
将Laravel风格的门面、特质和辅助函数添加到任何SlimPHP应用程序
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
将Laravel风格的门面、特质和辅助函数添加到任何SlimPHP应用程序
安装
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'); });
特质
Tappable
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()
Macroable
宏允许您动态地向类添加方法(无需修改其代码)。
假设您厌倦了必须这样做
$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!'); });
Conditionable
允许有条件地链接功能。
例如,让我们想象我们有一个标准的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
ServiceFactory类。
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));
Dumpable
向任何类添加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 ] }
注意:如果您想追加额外的转储数据,也可以像平常一样向dd和dump方法传递...$args
。
ForwardsCalls
将当前类中缺失的方法的调用代理到另一个目标类。在您不能继承或修改类但希望向其中添加一些功能(当然不是覆盖任何方法)时很有用。
这是一个示例,我们有一个基本的 App
类,但它是一个最终类,所以我们不能继承它。因此,我们创建了一个 AppProxy
类,允许我们声明“在 AppProxy
上调用的任何方法,如果 AppProxy
中不存在,则使用 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(); });
这样我们的控制器保持清洁、可读,并且每个责任都被分离到自己的类中,以便长期维护更容易。这也会使测试更容易,因为你可以测试单个类,也可以测试整体管道结果,而不需要测试控制器本身。
中心点
Hub
类是一种将相似组管道存储在一起的方法,以便可以从同一个对象中检索和执行它们。
$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(); });