affinity4/支持

向任何PSR-11容器应用程序添加Laravel风格的门面、特性和辅助函数

0.1.0 2024-03-12 16:19 UTC

This package is auto-updated.

Last update: 2024-09-12 17:30:50 UTC


README

向任何SlimPHP应用程序添加Laravel风格的门面、特性和辅助函数

安装

composer require affinity4/slimphp-support

使用方法

在您的应用程序中设置门面

要使用SlimPHP门面,您首先需要像往常一样创建您的Slim应用程序,使用Slim\App\AppFactoryDI\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兼容方法,即setgethasset方法向容器添加服务,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));

可转储

向任何类添加dumpdd方法

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&param2=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();
});