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 3 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
7 changes: 7 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@
"friends-of-behat/mink-extension": "^2.2",
"friends-of-behat/symfony-extension": "^2.1",
"guzzlehttp/guzzle": "^6.0 || ^7.0",
"illuminate/config": "^8.70|^9.0|^10.0",
"illuminate/contracts": "^8.70|^9.0|^10.0",
"illuminate/database": "^8.70|^9.0|^10.0",
"illuminate/http": "^8.70|^9.0|^10.0",
"illuminate/pagination": "^8.70|^9.0|^10.0",
"illuminate/routing": "^8.70|^9.0|^10.0",
"illuminate/support": "^8.70|^9.0|^10.0",
"jangregor/phpstan-prophecy": "^1.0",
"justinrainbow/json-schema": "^5.2.1",
"phpspec/prophecy-phpunit": "^2.0",
Expand Down
1 change: 1 addition & 0 deletions src/Documentation/Action/DocumentationAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ class: OpenApi::class,
);

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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/composer.lock
/vendor
/.phpunit.result.cache
40 changes: 40 additions & 0 deletions src/Laravel/ApiPlatformMiddleware.php
Original file line number Diff line number Diff line change
@@ -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
{
if ($operationName) {
$request->attributes->set('_api_operation', $this->operationMetadataFactory->create($operationName));
}

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

return $next($request);
}
}
635 changes: 635 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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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,

Check failure on line 31 in src/Laravel/Controller/ApiPlatformController.php

View workflow job for this annotation

GitHub Actions / PHPStan (PHP 8.3)

Parameter $app of method ApiPlatform\Laravel\Controller\ApiPlatformController::__construct() has invalid type Illuminate\Foundation\Application.

Check failure on line 31 in src/Laravel/Controller/ApiPlatformController.php

View workflow job for this annotation

GitHub Actions / PHPStan (PHP 8.3)

Property ApiPlatform\Laravel\Controller\ApiPlatformController::$app has unknown class Illuminate\Foundation\Application as its type.
) {
}

/**
* 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
Original file line number Diff line number Diff line change
@@ -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)
{
}
}