affinity4/slimphp-support

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

0.10.1 2024-03-11 00:27 UTC

This package is auto-updated.

Last update: 2024-09-11 01:34:23 UTC


README

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

安装

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');
});

特质

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兼容方法,即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 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

向任何类添加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
  ]
}

注意:如果您想追加额外的转储数据,也可以像平常一样向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 来使用 AppgetContainer(或其他任何公共方法/属性)

$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();
});

这样我们的控制器保持清洁、可读,并且每个责任都被分离到自己的类中,以便长期维护更容易。这也会使测试更容易,因为你可以测试单个类,也可以测试整体管道结果,而不需要测试控制器本身。

中心点

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