Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(laravel): laravel component #5882

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Documentation/Action/DocumentationAction.php
Expand Up @@ -80,6 +80,7 @@ private function getOpenApiDocumentation(array $context, string $format, Request
$context['request'] = $request;
$operation = new Get(class: OpenApi::class, read: true, serialize: true, provider: fn () => $this->openApiFactory->__invoke($context), normalizationContext: [ApiGatewayNormalizer::API_GATEWAY => $context['api_gateway'] ?? null], outputFormats: $this->documentationFormats);
if ('html' === $format) {
// TODO: support laravel this bounds Documentation with Symfony so it's not perfect
$operation = $operation->withProcessor('api_platform.swagger_ui.processor')->withWrite(true);
}
if ('json' === $format) {
Expand Down
4 changes: 4 additions & 0 deletions src/JsonLd/Action/ContextAction.php
Expand Up @@ -56,6 +56,10 @@ public function __construct(
*/
public function __invoke(string $shortName = 'Entrypoint', Request $request = null): array|Response
{
if (!$shortName) {
$shortName = 'Entrypoint';
}

if (null !== $request && $this->provider && $this->processor && $this->serializer) {
$operation = new Get(
outputFormats: ['jsonld' => ['application/ld+json']],
Expand Down
3 changes: 3 additions & 0 deletions src/Laravel/.gitignore
@@ -0,0 +1,3 @@
/composer.lock
/vendor
/.phpunit.result.cache
40 changes: 40 additions & 0 deletions src/Laravel/ApiPlatformMiddleware.php
@@ -0,0 +1,40 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Laravel;

use ApiPlatform\Metadata\Operation\Factory\OperationMetadataFactory;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class ApiPlatformMiddleware
{
public function __construct(
protected OperationMetadataFactory $operationMetadataFactory,
) {
}

/**
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, \Closure $next, string $operationName = null): Response

Check failure on line 30 in src/Laravel/ApiPlatformMiddleware.php

View workflow job for this annotation

GitHub Actions / PHPStan (PHP 8.2)

Parameter $next of method ApiPlatform\Laravel\ApiPlatformMiddleware::handle() has invalid type Illuminate\Http\Request.

Check failure on line 30 in src/Laravel/ApiPlatformMiddleware.php

View workflow job for this annotation

GitHub Actions / PHPStan (PHP 8.2)

Parameter $request of method ApiPlatform\Laravel\ApiPlatformMiddleware::handle() has invalid type Illuminate\Http\Request.
{
if ($operationName) {
$request->attributes->set('_api_operation', $this->operationMetadataFactory->create($operationName));

Check failure on line 33 in src/Laravel/ApiPlatformMiddleware.php

View workflow job for this annotation

GitHub Actions / PHPStan (PHP 8.2)

Access to property $attributes on an unknown class Illuminate\Http\Request.
}

$request->attributes->set('_format', str_replace('.', '', $request->route('_format') ?? ''));

Check failure on line 36 in src/Laravel/ApiPlatformMiddleware.php

View workflow job for this annotation

GitHub Actions / PHPStan (PHP 8.2)

Access to property $attributes on an unknown class Illuminate\Http\Request.

Check failure on line 36 in src/Laravel/ApiPlatformMiddleware.php

View workflow job for this annotation

GitHub Actions / PHPStan (PHP 8.2)

Call to method route() on an unknown class Illuminate\Http\Request.

return $next($request);
}
}
640 changes: 640 additions & 0 deletions src/Laravel/ApiPlatformProvider.php

Large diffs are not rendered by default.

165 changes: 165 additions & 0 deletions src/Laravel/ApiResource/Error.php
@@ -0,0 +1,165 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Laravel\ApiResource;

use ApiPlatform\JsonLd\ContextBuilderInterface;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\Error as Operation;
use ApiPlatform\Metadata\ErrorResource;
use ApiPlatform\Metadata\Exception\HttpExceptionInterface;
use ApiPlatform\Metadata\Exception\ProblemExceptionInterface;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface as SymfonyHttpExceptionInterface;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\Ignore;
use Symfony\Component\Serializer\Annotation\SerializedName;
use Symfony\Component\WebLink\Link;

#[ErrorResource(
types: ['hydra:Error'],
openapi: false,
operations: [
new Operation(
name: '_api_errors_problem',
outputFormats: ['json' => ['application/problem+json']],
normalizationContext: [
'groups' => ['jsonproblem'],
'skip_null_values' => true,
],
uriTemplate: '/errors/{status}'
),
new Operation(
name: '_api_errors_hydra',
outputFormats: ['jsonld' => ['application/problem+json']],
normalizationContext: [
'groups' => ['jsonld'],
'skip_null_values' => true,
],
links: [new Link(rel: ContextBuilderInterface::JSONLD_NS.'error', href: 'http://www.w3.org/ns/hydra/error')],
uriTemplate: '/hydra_errors/{status}'
),
new Operation(
name: '_api_errors_jsonapi',
outputFormats: ['jsonapi' => ['application/vnd.api+json']],
normalizationContext: ['groups' => ['jsonapi'], 'skip_null_values' => true],
uriTemplate: '/jsonapi_errors/{status}'
),
],
graphQlOperations: []
)]
class Error extends \Exception implements ProblemExceptionInterface, HttpExceptionInterface
{
public function __construct(
private readonly string $title,
private readonly string $detail,
#[ApiProperty(identifier: true)] private int $status,
private readonly array $originalTrace,
private ?string $instance = null,
private string $type = 'about:blank',
private array $headers = []
) {
parent::__construct();
}

#[SerializedName('hydra:title')]
#[Groups(['jsonld'])]
public function getHydraTitle(): string
{
return $this->title;
}

#[SerializedName('trace')]
#[Groups(['trace'])]
public function getOriginalTrace(): array
{
return $this->originalTrace;
}

#[SerializedName('hydra:description')]
#[Groups(['jsonld'])]
public function getHydraDescription(): string
{
return $this->detail;
}

#[SerializedName('description')]
#[Groups(['jsonapi'])]
public function getDescription(): string
{
return $this->detail;
}

public static function createFromException(\Exception|\Throwable $exception, int $status): self
{
$headers = ($exception instanceof SymfonyHttpExceptionInterface || $exception instanceof HttpExceptionInterface) ? $exception->getHeaders() : [];

return new self('An error occurred', $exception->getMessage(), $status, $exception->getTrace(), type: '/errors/'.$status, headers: $headers);
}

#[Ignore]
public function getHeaders(): array
{
return $this->headers;
}

#[Ignore]
public function getStatusCode(): int
{
return $this->status;
}

public function setHeaders(array $headers): void
{
$this->headers = $headers;
}

#[Groups(['jsonld', 'jsonproblem'])]
public function getType(): string
{
return $this->type;
}

#[Groups(['jsonld', 'jsonproblem', 'jsonapi'])]
public function getTitle(): ?string
{
return $this->title;
}

public function setType(string $type): void
{
$this->type = $type;
}

#[Groups(['jsonld', 'jsonproblem'])]
public function getStatus(): ?int
{
return $this->status;
}

public function setStatus(int $status): void
{
$this->status = $status;
}

#[Groups(['jsonld', 'jsonproblem'])]
public function getDetail(): ?string
{
return $this->detail;
}

#[Groups(['jsonld', 'jsonproblem'])]
public function getInstance(): ?string
{
return $this->instance;
}
}
105 changes: 105 additions & 0 deletions src/Laravel/Controller/ApiPlatformController.php
@@ -0,0 +1,105 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Laravel\Controller;

use ApiPlatform\Metadata\HttpOperation;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\Operation\Factory\OperationMetadataFactory;
use ApiPlatform\State\ProcessorInterface;
use ApiPlatform\State\ProviderInterface;
use Illuminate\Foundation\Application;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;

class ApiPlatformController extends Controller
{
public function __construct(
protected OperationMetadataFactory $operationMetadataFactory,
protected ProviderInterface $provider,
protected ProcessorInterface $processor,
protected Application $app,
) {
}

/**
* Display a listing of the resource.
*/
public function __invoke(Request $request)
{
$operation = $request->attributes->get('_api_operation');

if (!$operation) {
throw new \RuntimeException('Operation not found.');
}

$uriVariables = $this->getUriVariables($request, $operation);
// at some point we could introduce that back
// if ($this->uriVariablesConverter) {
// $context = ['operation' => $operation, 'uri_variables_map' => $uriVariablesMap];
// $identifiers = $this->uriVariablesConverter->convert($identifiers, $operation->getClass() ?? $resourceClass, $context);
// }

$context = [
'request' => $request,
'uri_variables' => $uriVariables,
'resource_class' => $operation->getClass(),
];

if (null === $operation->canValidate()) {
$operation = $operation->withValidate(!$request->isMethodSafe() && !$request->isMethod('DELETE'));
}

if (null === $operation->canRead() && $operation instanceof HttpOperation) {
$operation = $operation->withRead($operation->getUriVariables() || $request->isMethodSafe());
}

if (null === $operation->canDeserialize() && $operation instanceof HttpOperation) {
$operation = $operation->withDeserialize(\in_array($operation->getMethod(), ['POST', 'PUT', 'PATCH'], true));
}

$body = $this->provider->provide($operation, $uriVariables, $context);

// The provider can change the Operation, extract it again from the Request attributes
if ($request->attributes->get('_api_operation') !== $operation) {
$operation = $request->attributes->get('_api_operation');
$uriVariables = $this->getUriVariables($request, $operation);
}

$context['previous_data'] = $request->attributes->get('previous_data');
$context['data'] = $request->attributes->get('data');

if (null === $operation->canWrite()) {
$operation = $operation->withWrite(!$request->isMethodSafe());
}

if (null === $operation->canSerialize()) {
$operation = $operation->withSerialize(true);
}

return $this->processor->process($body, $operation, $uriVariables, $context);
}

/**
* @return array<string, mixed>
*/
private function getUriVariables(Request $request, HttpOperation $operation): array
{
$uriVariables = [];
foreach ($operation->getUriVariables() ?? [] as $parameterName => $_) {
$uriVariables[$parameterName] = $request->route($parameterName);
}

return $uriVariables;
}
}
23 changes: 23 additions & 0 deletions src/Laravel/Eloquent/Options.php
@@ -0,0 +1,23 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Laravel\Eloquent;

use ApiPlatform\State\OptionsInterface;

class Options implements OptionsInterface
{
public function __construct(public string $model)
{
}
}