Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions src/GapicClientTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ trait GapicClientTrait
private string $serviceName = '';
private array $agentHeader = [];
private array $descriptors = [];
/** @var array<callable> $prependMiddlewareCallables */
private array $prependMiddlewareCallables = [];
/** @var array<callable> $middlewareCallables */
private array $middlewareCallables = [];
private array $transportCallMethods = [
Expand Down Expand Up @@ -118,6 +120,42 @@ public function addMiddleware(callable $middlewareCallable): void
$this->middlewareCallables[] = $middlewareCallable;
}

/**
* Prepend a middleware to the call stack by providing a callable which will be
* invoked at the end of each call, and will return an instance of
* {@see MiddlewareInterface} when invoked.
*
* The callable must have the following method signature:
*
* callable(MiddlewareInterface): MiddlewareInterface
*
* An implementation may look something like this:
* ```
* $client->prependMiddleware(function (MiddlewareInterface $handler) {
* return new class ($handler) implements MiddlewareInterface {
* public function __construct(private MiddlewareInterface $handler) {
* }
*
* public function __invoke(Call $call, array $options) {
* // modify call and options (pre-request)
* $response = ($this->handler)($call, $options);
* // modify the response (post-request)
* return $response;
* }
* };
* });
* ```
*
* @param callable $middlewareCallable A callable which returns an instance
* of {@see MiddlewareInterface} when invoked with a
* MiddlewareInterface instance as its first argument.
* @return void
*/
public function prependMiddleware(callable $middlewareCallable): void
{
$this->prependMiddlewareCallables[] = $middlewareCallable;
}

/**
* Initiates an orderly shutdown in which preexisting calls continue but new
* calls are immediately cancelled.
Expand Down Expand Up @@ -663,6 +701,12 @@ private function createCallStack(array $callConstructionOptions)
$startCallMethod = $this->transportCallMethods[$call->getCallType()];
return $this->transport->$startCallMethod($call, $options);
};

foreach (\array_reverse($this->prependMiddlewareCallables) as $fn) {
/** @var MiddlewareInterface $callStack */
$callStack = $fn($callStack);
}

$callStack = new CredentialsWrapperMiddleware($callStack, $this->credentialsWrapper);
$callStack = new FixedHeaderMiddleware($callStack, $fixedHeaders, true);
$callStack = new RetryMiddleware($callStack, $callConstructionOptions['retrySettings']);
Expand Down
69 changes: 69 additions & 0 deletions tests/Unit/GapicClientTraitTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1394,6 +1394,75 @@ public function __invoke(Call $call, array $options)
$this->assertTrue($m2Called);
}

public function testPrependMiddleware()
{
list($client, $transport) = $this->buildClientToTestModifyCallMethods();

$callOrder = [];
$middleware1 = function (callable $handler) use (&$callOrder) {
return new class($handler, $callOrder) implements MiddlewareInterface {
private $handler;
private array $callOrder;
public function __construct(
callable $handler,
array &$callOrder
) {
$this->handler = $handler;
$this->callOrder = &$callOrder;
}
public function __invoke(Call $call, array $options)
{
$this->callOrder[] = 'middleware1';
return ($this->handler)($call, $options);
}
};
};
$middleware2 = function (callable $handler) use (&$callOrder) {
return new class($handler, $callOrder) implements MiddlewareInterface {
private $handler;
private array $callOrder;
public function __construct(
callable $handler,
array &$callOrder
) {
$this->handler = $handler;
$this->callOrder = &$callOrder;
}
public function __invoke(Call $call, array $options)
{
$this->callOrder[] = 'middleware2';
return ($this->handler)($call, $options);
}
};
};
$client->addMiddleware($middleware1);
$client->prependMiddleware($middleware2);

$transport->startUnaryCall(
Argument::type(Call::class),
[
'transportOptions' => [
'custom' => ['addModifyUnaryCallableOption' => true]
],
'headers' => AgentHeader::buildAgentHeader([]),
'credentialsWrapper' => CredentialsWrapper::build([
'keyFile' => __DIR__ . '/testdata/json-key-file.json'
])
]
)
->shouldBeCalledOnce()
->willReturn(new FulfilledPromise(new Operation()));

$client->startCall(
'simpleMethod',
'decodeType',
[],
new MockRequest(),
)->wait();

$this->assertEquals(['middleware1', 'middleware2'], $callOrder);
}

public function testInvalidClientOptionsTypeThrowsExceptionForV2SurfaceOnly()
{
// v1 client
Expand Down