Skip to content

Commit

Permalink
Merge pull request #276 from saloonphp/feature/last-middleware
Browse files Browse the repository at this point in the history
Feature | V3 - Allow Middleware To Be Reordered
  • Loading branch information
Sammyjo20 authored Aug 26, 2023
2 parents 72d444b + bea0386 commit 2aa53a2
Show file tree
Hide file tree
Showing 10 changed files with 220 additions and 50 deletions.
6 changes: 4 additions & 2 deletions src/Contracts/MiddlewarePipeline.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

namespace Saloon\Contracts;

use Saloon\Data\PipeOrder;

interface MiddlewarePipeline
{
/**
Expand All @@ -12,15 +14,15 @@ interface MiddlewarePipeline
* @param callable(\Saloon\Contracts\PendingRequest): (\Saloon\Contracts\PendingRequest|\Saloon\Contracts\FakeResponse|void) $callable
* @return $this
*/
public function onRequest(callable $callable, bool $prepend = false, ?string $name = null): static;
public function onRequest(callable $callable, ?string $name = null, ?PipeOrder $order = null): static;

/**
* Add a middleware after the request is sent
*
* @param callable(\Saloon\Contracts\Response): (\Saloon\Contracts\Response|void) $callable
* @return $this
*/
public function onResponse(callable $callable, bool $prepend = false, ?string $name = null): static;
public function onResponse(callable $callable, ?string $name = null, ?PipeOrder $order = null): static;

/**
* Process the request pipeline.
Expand Down
4 changes: 3 additions & 1 deletion src/Contracts/Pipeline.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

namespace Saloon\Contracts;

use Saloon\Data\PipeOrder;

interface Pipeline
{
/**
Expand All @@ -13,7 +15,7 @@ interface Pipeline
* @return $this
* @throws \Saloon\Exceptions\DuplicatePipeNameException
*/
public function pipe(callable $callable, bool $prepend = false, ?string $name = null): static;
public function pipe(callable $callable, ?string $name = null, ?PipeOrder $order = null): static;

/**
* Process the pipeline.
Expand Down
1 change: 1 addition & 0 deletions src/Data/Pipe.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class Pipe
public function __construct(
callable $callable,
readonly public ?string $name = null,
readonly public ?PipeOrder $order = null,
) {
$this->callable = $callable(...);
}
Expand Down
35 changes: 35 additions & 0 deletions src/Data/PipeOrder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace Saloon\Data;

use Saloon\Enums\Order;

class PipeOrder
{
/**
* Constructor
*/
public function __construct(
public readonly Order $type,
) {
//
}

/**
* Run the middleware first
*/
public static function first(): self
{
return new self(Order::FIRST);
}

/**
* Run the middleware last
*/
public static function last(): self
{
return new self(Order::LAST);
}
}
11 changes: 11 additions & 0 deletions src/Enums/Order.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Saloon\Enums;

enum Order: string
{
case FIRST = 'first';
case LAST = 'last';
}
9 changes: 5 additions & 4 deletions src/Helpers/MiddlewarePipeline.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Saloon\Helpers;

use Closure;
use Saloon\Data\PipeOrder;
use Saloon\Contracts\Response;
use Saloon\Contracts\FakeResponse;
use Saloon\Contracts\PendingRequest;
Expand Down Expand Up @@ -39,7 +40,7 @@ public function __construct()
* @return $this
* @throws \Saloon\Exceptions\DuplicatePipeNameException
*/
public function onRequest(callable $callable, bool $prepend = false, ?string $name = null): static
public function onRequest(callable $callable, ?string $name = null, ?PipeOrder $order = null): static
{
/**
* For some reason, PHP is not destructing non-static Closures, or 'things' using non-static Closures, correctly, keeping unused objects intact.
Expand All @@ -63,7 +64,7 @@ public function onRequest(callable $callable, bool $prepend = false, ?string $na
}

return $pendingRequest;
}, $prepend, $name);
}, $name, $order);

return $this;
}
Expand All @@ -75,7 +76,7 @@ public function onRequest(callable $callable, bool $prepend = false, ?string $na
* @return $this
* @throws \Saloon\Exceptions\DuplicatePipeNameException
*/
public function onResponse(callable $callable, bool $prepend = false, ?string $name = null): static
public function onResponse(callable $callable, ?string $name = null, ?PipeOrder $order = null): static
{
/**
* For some reason, PHP is not destructing non-static Closures, or 'things' using non-static Closures, correctly, keeping unused objects intact.
Expand All @@ -91,7 +92,7 @@ public function onResponse(callable $callable, bool $prepend = false, ?string $n
$result = $callable($response);

return $result instanceof Response ? $result : $response;
}, $prepend, $name);
}, $name, $order);

return $this;
}
Expand Down
47 changes: 40 additions & 7 deletions src/Helpers/Pipeline.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
namespace Saloon\Helpers;

use Saloon\Data\Pipe;
use Saloon\Enums\Order;
use Saloon\Data\PipeOrder;
use Saloon\Exceptions\DuplicatePipeNameException;
use Saloon\Contracts\Pipeline as PipelineContract;

Expand All @@ -24,17 +26,15 @@ class Pipeline implements PipelineContract
* @return $this
* @throws \Saloon\Exceptions\DuplicatePipeNameException
*/
public function pipe(callable $callable, bool $prepend = false, ?string $name = null): static
public function pipe(callable $callable, ?string $name = null, ?PipeOrder $order = null): static
{
$pipe = new Pipe($callable, $name);
$pipe = new Pipe($callable, $name, $order);

if (is_string($name) && $this->pipeExists($name)) {
throw new DuplicatePipeNameException($name);
}

$prepend === true
? array_unshift($this->pipes, $pipe)
: $this->pipes[] = $pipe;
$this->pipes[] = $pipe;

return $this;
}
Expand All @@ -44,13 +44,46 @@ public function pipe(callable $callable, bool $prepend = false, ?string $name =
*/
public function process(mixed $payload): mixed
{
foreach ($this->pipes as $pipe) {
foreach ($this->sortPipes() as $pipe) {
$payload = call_user_func($pipe->callable, $payload);
}

return $payload;
}

/**
* Sort the pipes based on the "order" classes
*/
protected function sortPipes(): array

Check failure on line 57 in src/Helpers/Pipeline.php

View workflow job for this annotation

GitHub Actions / phpstan

Method Saloon\Helpers\Pipeline::sortPipes() return type has no value type specified in iterable type array.
{
$pipes = $this->pipes;

/** @var array<\Saloon\Data\PipeOrder> $pipeNames */
$pipeOrders = array_map(static fn (Pipe $pipe) => $pipe->order, $pipes);

Check failure on line 62 in src/Helpers/Pipeline.php

View workflow job for this annotation

GitHub Actions / phpstan

Variable $pipeNames in PHPDoc tag @var does not match assigned variable $pipeOrders.

// Now we'll iterate through the pipe orders and if a specific pipe
// requests to be placed at the top - we will move the pipe to the
// top of the array. If it wants to be at the bottom we can put it
// there too.

foreach ($pipeOrders as $index => $order) {
if (is_null($order)) {
continue;
}

$pipe = $pipes[$index];

unset($pipes[$index]);

match (true) {
$order->type === Order::FIRST => array_unshift($pipes, $pipe),
$order->type === Order::LAST => $pipes[] = $pipe,
};
}

return $pipes;
}

/**
* Set the pipes on the pipeline.
*
Expand All @@ -66,7 +99,7 @@ public function setPipes(array $pipes): static
// so we can check if the name already exists.

foreach ($pipes as $pipe) {
$this->pipe($pipe->callable, false, $pipe->name);
$this->pipe($pipe->callable, $pipe->name, $pipe->order);
}

return $this;
Expand Down
4 changes: 3 additions & 1 deletion src/Http/Middleware/DetermineMockResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Saloon\Http\Middleware;

use Saloon\Data\PipeOrder;
use Saloon\Http\Faking\Fixture;
use Saloon\Contracts\PendingRequest;
use Saloon\Http\Faking\MockResponse;
Expand All @@ -15,6 +16,7 @@ class DetermineMockResponse implements RequestMiddleware
* Guess a mock response
*
* @throws \JsonException
* @throws \Saloon\Exceptions\FixtureException
* @throws \Saloon\Exceptions\FixtureMissingException
*/
public function __invoke(PendingRequest $pendingRequest): PendingRequest|MockResponse
Expand Down Expand Up @@ -49,7 +51,7 @@ public function __invoke(PendingRequest $pendingRequest): PendingRequest|MockRes
// middleware on the response to record the response.

if (is_null($mockResponse) && $mockObject instanceof Fixture) {
$pendingRequest->middleware()->onResponse(new RecordFixture($mockObject), true, 'recordFixture');
$pendingRequest->middleware()->onResponse(new RecordFixture($mockObject), 'recordFixture', PipeOrder::first());
}

return $pendingRequest;
Expand Down
55 changes: 34 additions & 21 deletions src/Http/PendingRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -152,39 +152,52 @@ protected function registerAndExecuteMiddleware(): void
{
$middleware = $this->middleware();

// New Middleware Order:
// We'll start with our core middleware like merging request properties, merging the
// body, delay and also running authenticators on the request.

// 1. Global (Laravel)
// 2. Plugin (Rate Limiter)
// 3. Deferred Authentication
// 4. Mock Response
// 5. User
// 6. Delay/Debugging/Event
$middleware
->onRequest(new MergeRequestProperties, 'mergeRequestProperties')
->onRequest(new MergeBody, 'mergeBody')
->onRequest(new MergeDelay, 'mergeDelay')
->onRequest(new AuthenticateRequest, 'authenticateRequest')
->onRequest(new DetermineMockResponse, 'determineMockResponse');

// Todo: Revisit middleware order
// Next, we'll merge in our "Global" middleware which can be middleware set by the
// user or set by Saloon's plugins like the Laravel Plugin. It's best that this
// middleware is run now because we want the user to still have an opportunity
// to overwrite anything applied by it.

$middleware->merge(Config::middleware());

// Now we'll queue te delay middleware and authenticator middleware

$middleware
->onRequest(new MergeRequestProperties, false, 'mergeRequestProperties')
->onRequest(new MergeBody, false, 'mergeBody')
->onRequest(new MergeDelay, false, 'mergeDelay')
->onRequest(new AuthenticateRequest, false, 'authenticateRequest')
->onRequest(new DetermineMockResponse, false, 'determineMockResponse');
// Now we'll "boot" the connector and request. This is a hook that can be run after
// the core middleware that allows you to add your own properties that are a higher
// priority than anything else.

$this->bootConnectorAndRequest();

// Now we'll merge the middleware added on the connector and the request. This
// middleware will have almost the final object to play with and overwrite if
// they desire.

$middleware
->merge($this->connector->middleware())
->merge($this->request->middleware())
->onRequest(new DelayMiddleware, false, 'delayMiddleware')
->onRequest(new DebugRequest, false, 'debugRequest')
->onResponse(new DebugResponse, false, 'debugResponse');
->merge($this->request->middleware());

// Next, we'll delay the request if we need to. This will run before the final
// middleware.

$middleware->onRequest(new DelayMiddleware, 'delayMiddleware');

// Finally, we'll apply our "final" middleware. This is a group of middleware
// that will run at the end, no matter what. This is useful for debugging and
// events where we can guarantee that the middleware will be run at the end.

$middleware
->onRequest(new DebugRequest, 'debugRequest')
->onResponse(new DebugResponse, 'debugResponse');

// Next, we will execute the request middleware pipeline which will
// process any middleware added on the connector or the request.
// process the middleware in the order we added it.

$middleware->executeRequestPipeline($this);
}
Expand Down
Loading

0 comments on commit 2aa53a2

Please sign in to comment.