Last active
August 16, 2023 10:18
-
-
Save butschster/e957e10cf01bc39f4cceebe69200396f to your computer and use it in GitHub Desktop.
GrpcExceptionMapper
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
namespace Shared\gRPC\Service\Client; | |
use Shared\gRPC\Attribute\ServiceClient; | |
use Shared\gRPC\Service\ServiceClientTrait; | |
use Shared\gRPC\Services\Auth\v1\AuthServiceInterface; | |
use Shared\gRPC\Services\Auth\v1\Request\ChangePasswordRequest; | |
use Shared\gRPC\Services\Auth\v1\Request\ForgotPasswordRequest; | |
use Shared\gRPC\Services\Auth\v1\Request\GetUserByTokenRequest; | |
use Shared\gRPC\Services\Auth\v1\Request\LoginRequest; | |
use Shared\gRPC\Services\Auth\v1\Request\MeRequest; | |
use Shared\gRPC\Services\Auth\v1\Request\RegisterRequest; | |
use Shared\gRPC\Services\Auth\v1\Response\ChangePasswordResponse; | |
use Shared\gRPC\Services\Auth\v1\Response\ForgotPasswordResponse; | |
use Shared\gRPC\Services\Auth\v1\Response\GetUserByTokenResponse; | |
use Shared\gRPC\Services\Auth\v1\Response\LoginResponse; | |
use Shared\gRPC\Services\Auth\v1\Response\MeResponse; | |
use Shared\gRPC\Services\Auth\v1\Response\RegisterResponse; | |
use Spiral\RoadRunner\GRPC\ContextInterface; | |
#[ServiceClient(name: "auth.v1.AuthService")] | |
final class AuthServiceClient implements AuthServiceInterface | |
{ | |
use ServiceClientTrait; | |
public function Auth(ContextInterface $ctx, LoginRequest $in): LoginResponse | |
{ | |
return $this->callAction(__FUNCTION__, $ctx, $in); | |
} | |
public function Me(ContextInterface $ctx, MeRequest $in): MeResponse | |
{ | |
return $this->callAction(__FUNCTION__, $ctx, $in); | |
} | |
public function Register(ContextInterface $ctx, RegisterRequest $in): RegisterResponse | |
{ | |
return $this->callAction(__FUNCTION__, $ctx, $in); | |
} | |
public function ForgotPassword(ContextInterface $ctx, ForgotPasswordRequest $in): ForgotPasswordResponse | |
{ | |
return $this->callAction(__FUNCTION__, $ctx, $in); | |
} | |
public function ChangePassword(ContextInterface $ctx, ChangePasswordRequest $in): ChangePasswordResponse | |
{ | |
return $this->callAction(__FUNCTION__, $ctx, $in); | |
} | |
public function GetUserByToken(ContextInterface $ctx, GetUserByTokenRequest $in): GetUserByTokenResponse | |
{ | |
return $this->callAction(__FUNCTION__, $ctx, $in); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
namespace Shared\gRPC\Interceptor\Incoming; | |
use Shared\gRPC\Exception\GrpcExceptionMapper; | |
use Spiral\Core\CoreInterceptorInterface; | |
use Spiral\Core\CoreInterface; | |
use Spiral\Exceptions\ExceptionHandlerInterface; | |
use Spiral\RoadRunner\GRPC\Exception\GRPCExceptionInterface; | |
final readonly class ExceptionHandlerInterceptor implements CoreInterceptorInterface | |
{ | |
public function __construct( | |
private ExceptionHandlerInterface $errorHandler, | |
private GrpcExceptionMapper $mapper, | |
) { | |
} | |
/** | |
* Handle exceptions. | |
* @throws GRPCExceptionInterface | |
*/ | |
public function process(string $controller, string $action, array $parameters, CoreInterface $core): mixed | |
{ | |
try { | |
return $core->callAction($controller, $action, $parameters); | |
} catch (\Throwable $e) { | |
if (!$e instanceof \DomainException) { | |
$this->errorHandler->report($e); | |
} | |
throw $this->mapper->toGrpcException($e); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
namespace Shared\gRPC\Exception; | |
use Google\Rpc\Status; | |
use Shared\gRPC\Attribute\ErrorMapper; | |
use Shared\gRPC\Services\Common\v1\DTO\Exception; | |
use Shared\gRPC\Services\Common\v1\DTO\ValidationException; | |
use Spiral\Attributes\AttributeReader; | |
use Spiral\Attributes\ReaderInterface; | |
use Spiral\Core\Attribute\Singleton; | |
use Spiral\Core\Container; | |
use Spiral\Core\FactoryInterface; | |
use Spiral\RoadRunner\GRPC\Exception\GRPCException; | |
use Spiral\RoadRunner\GRPC\Exception\GRPCExceptionInterface; | |
use Spiral\Tokenizer\Attribute\TargetAttribute; | |
use Spiral\Tokenizer\TokenizationListenerInterface; | |
#[TargetAttribute(attribute: ErrorMapper::class)] | |
#[Singleton] | |
final class GrpcExceptionMapper implements TokenizationListenerInterface | |
{ | |
/** @var array<class-string, class-string<MapperInterface>> */ | |
private array $mappers = []; | |
/** @var array<class-string, MapperInterface> */ | |
private array $resolvedMappers = []; | |
public function __construct( | |
private readonly ReaderInterface $reader = new AttributeReader(), | |
private readonly FactoryInterface $factory = new Container(), | |
) { | |
new Exception(); | |
new ValidationException(); | |
} | |
public function listen(\ReflectionClass $class): void | |
{ | |
/** @var ErrorMapper $mapper */ | |
$mapper = $this->reader->firstClassMetadata($class, ErrorMapper::class); | |
$this->mappers[$mapper->type] = $class->getName(); | |
} | |
public function finalize(): void | |
{ | |
// do nothing | |
} | |
public function toGrpcException(\Throwable $e): GRPCExceptionInterface | |
{ | |
$type = $this->getExceptionKey($e); | |
if (isset($this->mappers[$type])) { | |
$mapper = $this->resolvedMappers[$type] ??= $this->factory->make($this->mappers[$type]); | |
return $mapper->toGrpcException($e); | |
} | |
$info = $this->makeExceptionMessageObject($e); | |
$previous = $e->getPrevious(); | |
while ($previous !== null) { | |
$info->setPrevious($this->makeExceptionMessageObject($previous)); | |
$previous = $previous->getPrevious(); | |
} | |
return new GRPCException( | |
message: $e->getMessage(), | |
code: $e->getCode(), | |
details: [$info], | |
previous: $e | |
); | |
} | |
public function fromError(object $error): \Throwable | |
{ | |
if (!isset($error->metadata['grpc-status-details-bin'])) { | |
return ResponseException::createFromStatus($error); | |
} | |
$exception = $this->parseException($error); | |
if (!isset($this->mappers[$exception->getType()])) { | |
return ResponseException::createFromStatus($error); | |
} | |
$mapper = $this->resolvedMappers[$exception->getType()] ??= $this->factory->make( | |
$this->mappers[$exception->getType()], | |
); | |
return $mapper->fromError($exception); | |
} | |
private function makeExceptionMessageObject(\Throwable $e): Exception | |
{ | |
return match (true) { | |
default => new Exception([ | |
'type' => $this->getExceptionKey($e), | |
'message' => $e->getMessage(), | |
'code' => $e->getCode(), | |
]) | |
}; | |
} | |
private function parseException(object $status): Exception|ValidationException | |
{ | |
$status = \array_map( | |
function (string $info) { | |
$status = new Status(); | |
$status->mergeFromString($info); | |
return $status; | |
}, | |
$status->metadata['grpc-status-details-bin'], | |
)[0]; | |
return $status->getDetails()[0]->unpack(); | |
} | |
private function getExceptionKey(\Throwable $e): string | |
{ | |
$className = (new \ReflectionClass($e))->getShortName(); | |
return \strtolower(\preg_replace('/(?<!^)[A-Z]/', '_$0', $className)); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
namespace Shared\gRPC\Service; | |
use Google\Protobuf\Internal\Message; | |
use Shared\gRPC\Exception\GrpcExceptionMapper; | |
use Spiral\Core\CoreInterface; | |
use Spiral\RoadRunner\GRPC\ContextInterface; | |
use Spiral\RoadRunner\GRPC\StatusCode; | |
use Spiral\RoadRunner\GRPC\ServiceInterface; | |
trait ServiceClientTrait | |
{ | |
private readonly array $registeredServices; | |
public function __construct( | |
private readonly CoreInterface $core, | |
private readonly GrpcExceptionMapper $mapper, | |
ServiceLocatorInterface $locator = new NullServiceLocator(), | |
) { | |
$this->registeredServices = \array_map( | |
static fn (ServiceInterface $service): string => $service::NAME, | |
$locator->getServices(), | |
); | |
} | |
private function isRegistered(): bool | |
{ | |
return \in_array($this::NAME, $this->registeredServices, true); | |
} | |
/** | |
* @throws \ReflectionException | |
* @throws \Throwable | |
*/ | |
private function callAction(string $action, ContextInterface $ctx, Message $in): Message | |
{ | |
if ($this->isRegistered()) { | |
throw new \LogicException(\sprintf( | |
'Infinite call of action "%s/%s" detected: Service is attempting to call itself, leading to a potential infinite loop.', | |
$this::NAME, | |
$action | |
)); | |
} | |
$method = new \ReflectionMethod($this, $action); | |
$returnType = $method->getReturnType()->getName(); | |
$uri = '/' . $this::NAME . '/' . $action; | |
[$response, $status] = $this->core->callAction($this::class, $uri, [ | |
'in' => $in, | |
'ctx' => $ctx, | |
'responseClass' => $returnType, | |
]); | |
$code = $status->code ?? StatusCode::UNKNOWN; | |
if ($code !== StatusCode::OK) { | |
throw $this->mapper->fromError($status); | |
} | |
\assert($response instanceof $returnType); | |
return $response; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment