Skip to content

Commit

Permalink
Merge 4.0
Browse files Browse the repository at this point in the history
  • Loading branch information
soyuka committed Oct 28, 2024
2 parents a4b79d1 + 001d771 commit 6f71d4b
Show file tree
Hide file tree
Showing 24 changed files with 456 additions and 115 deletions.
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Changelog

## v4.0.6

### Bug fixes

* [195c4e788](https://github.com/api-platform/core/commit/195c4e7883520416e042ac78143b18652a216fbf) fix(hydra): hydra context changed (#6710)
* [4f65ef2d0](https://github.com/api-platform/core/commit/4f65ef2d061215df348e3505856f0f41c7c909ed) fix(metadata): providing parameter constraints skips automatic ones (#6756)
* [5a8ef115a](https://github.com/api-platform/core/commit/5a8ef115a90791992a6c1325fb6d1ac458b22153) fix(symfony): ECMA-262 pattern with RegExp validator (#6733)
* [67c5a2a24](https://github.com/api-platform/core/commit/67c5a2a2463bca94f0997b4fab1248a08994465b) fix(laravel): jsonapi error serialization (#6755)
* [ac6f667f3](https://github.com/api-platform/core/commit/ac6f667f301f6c4c399a707faf00567239bd98d8) fix(laravel): collection relations other than HasMany (#6737)

### Features

* [cecd77149](https://github.com/api-platform/core/commit/cecd77149795c1a455ac72bc3ed0606413e69900) feat(laravel): use laravel cache setting (#6751)

## v4.0.5

### Bug fixes
Expand Down Expand Up @@ -152,6 +166,12 @@ Notes:

* [0d5f35683](https://github.com/api-platform/core/commit/0d5f356839eb6aa9f536044abe4affa736553e76) feat(laravel): laravel component (#5882)

## v3.4.5

### Bug fixes

* [fc8fa00a1](https://github.com/api-platform/core/commit/fc8fa00a19320b65547a60537261959c11f8e6a8) fix(hydra): iri template when using query parameter (#6742)

## v3.4.4

### Bug fixes
Expand Down
11 changes: 4 additions & 7 deletions docs/guides/doctrine-search-filter.php
Original file line number Diff line number Diff line change
Expand Up @@ -120,24 +120,21 @@ public function testGetDocumentation(): void
$this->assertJsonContains([
'search' => [
'@type' => 'IriTemplate',
'template' => '/books.jsonld{?id,title,author}',
'template' => '/books.jsonld{?title,author}',
'variableRepresentation' => 'BasicRepresentation',
'mapping' => [
[
'@type' => 'IriTemplateMapping',
'variable' => 'id',
'property' => 'id',
],
[
'@type' => 'IriTemplateMapping',
'variable' => 'title',
'property' => 'title',
'required' => false,
],
[
'@type' => 'IriTemplateMapping',
'variable' => 'author',
'property' => 'author',
]
'required' => false,
],
],
],
]);
Expand Down
24 changes: 15 additions & 9 deletions src/Hydra/Serializer/CollectionFiltersNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
use ApiPlatform\Doctrine\Orm\State\Options;
use ApiPlatform\JsonLd\Serializer\HydraPrefixTrait;
use ApiPlatform\Metadata\FilterInterface;
use ApiPlatform\Metadata\Parameter;
use ApiPlatform\Metadata\Parameters;
use ApiPlatform\Metadata\QueryParameterInterface;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
Expand Down Expand Up @@ -133,10 +132,9 @@ public function setNormalizer(NormalizerInterface $normalizer): void
/**
* Returns the content of the Hydra search property.
*
* @param FilterInterface[] $filters
* @param array<string, Parameter> $parameters
* @param FilterInterface[] $filters
*/
private function getSearch(string $resourceClass, array $parts, array $filters, array|Parameters|null $parameters, string $hydraPrefix): array
private function getSearch(string $resourceClass, array $parts, array $filters, ?Parameters $parameters, string $hydraPrefix): array
{
$variables = [];
$mapping = [];
Expand All @@ -153,13 +151,19 @@ private function getSearch(string $resourceClass, array $parts, array $filters,
continue;
}

if (!($property = $parameter->getProperty()) && ($filterId = $parameter->getFilter()) && ($filter = $this->getFilter($filterId))) {
foreach ($filter->getDescription($resourceClass) as $variable => $description) {
// This is a practice induced by PHP and is not necessary when implementing URI template
if (($filterId = $parameter->getFilter()) && \is_string($filterId) && ($filter = $this->getFilter($filterId))) {
$filterDescription = $filter->getDescription($resourceClass);

foreach ($filterDescription as $variable => $description) {
// // This is a practice induced by PHP and is not necessary when implementing URI template
if (str_ends_with((string) $variable, '[]')) {
continue;
}

if (($prop = $parameter->getProperty()) && ($description['property'] ?? null) !== $prop) {
continue;
}

// :property is a pattern allowed when defining parameters
$k = str_replace(':property', $description['property'], $key);
$variable = str_replace($description['property'], $k, $variable);
Expand All @@ -171,10 +175,12 @@ private function getSearch(string $resourceClass, array $parts, array $filters,
$mapping[] = $m;
}

continue;
if ($filterDescription) {
continue;
}
}

if (!$property) {
if (!($property = $parameter->getProperty())) {
continue;
}

Expand Down
29 changes: 27 additions & 2 deletions src/JsonApi/Serializer/ErrorNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,34 @@ public function normalize(mixed $object, ?string $format = null, array $context
$jsonApiObject = $this->itemNormalizer->normalize($object, $format, $context);
$error = $jsonApiObject['data']['attributes'];
$error['id'] = $jsonApiObject['data']['id'];
$error['type'] = $jsonApiObject['data']['id'];
if (isset($error['type'])) {
$error['links'] = ['type' => $error['type']];
}

if (!isset($error['code']) && method_exists($object, 'getId')) {
$error['code'] = $object->getId();
}

if (!isset($error['violations'])) {
return ['errors' => [$error]];
}

$errors = [];
foreach ($error['violations'] as $violation) {
$e = ['detail' => $violation['message']] + $error;
if (isset($error['links']['type'])) {
$type = $error['links']['type'];
$e['links']['type'] = \sprintf('%s/%s', $type, $violation['propertyPath']);
$e['id'] = str_replace($type, $e['links']['type'], $e['id']);
}
if (isset($e['code'])) {
$e['code'] = \sprintf('%s/%s', $error['code'], $violation['propertyPath']);
}
unset($e['violations']);
$errors[] = $e;
}

return ['errors' => [$error]];
return ['errors' => $errors];
}

/**
Expand Down
5 changes: 5 additions & 0 deletions src/JsonApi/Serializer/ReservedAttributeNameConverter.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

namespace ApiPlatform\JsonApi\Serializer;

use ApiPlatform\Metadata\Exception\ProblemExceptionInterface;
use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;

Expand Down Expand Up @@ -44,6 +45,10 @@ public function normalize(string $propertyName, ?string $class = null, ?string $
$propertyName = $this->nameConverter->normalize($propertyName, $class, $format, $context);
}

if ($class && is_a($class, ProblemExceptionInterface::class, true)) {
return $propertyName;
}

if (isset(self::JSON_API_RESERVED_ATTRIBUTES[$propertyName])) {
$propertyName = self::JSON_API_RESERVED_ATTRIBUTES[$propertyName];
}
Expand Down
26 changes: 17 additions & 9 deletions src/Laravel/ApiPlatformProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
use ApiPlatform\JsonApi\JsonSchema\SchemaFactory as JsonApiSchemaFactory;
use ApiPlatform\JsonApi\Serializer\CollectionNormalizer as JsonApiCollectionNormalizer;
use ApiPlatform\JsonApi\Serializer\EntrypointNormalizer as JsonApiEntrypointNormalizer;
use ApiPlatform\JsonApi\Serializer\ErrorNormalizer as JsonApiErrorNormalizer;
use ApiPlatform\JsonApi\Serializer\ItemNormalizer as JsonApiItemNormalizer;
use ApiPlatform\JsonApi\Serializer\ObjectNormalizer as JsonApiObjectNormalizer;
use ApiPlatform\JsonApi\Serializer\ReservedAttributeNameConverter;
Expand Down Expand Up @@ -297,7 +298,7 @@ public function register(): void
});

$this->app->extend(PropertyMetadataFactoryInterface::class, function (PropertyInfoPropertyMetadataFactory $inner, Application $app) {
/** @var ConfigRepository */
/** @var ConfigRepository $config */
$config = $app['config'];

return new CachePropertyMetadataFactory(
Expand All @@ -313,12 +314,12 @@ public function register(): void
$app->make(ResourceClassResolverInterface::class)
),
),
true === $config->get('app.debug') ? 'array' : 'file'
true === $config->get('app.debug') ? 'array' : $config->get('cache.default', 'file')
);
});

$this->app->singleton(PropertyNameCollectionFactoryInterface::class, function (Application $app) {
/** @var ConfigRepository */
/** @var ConfigRepository $config */
$config = $app['config'];

return new CachePropertyNameCollectionMetadataFactory(
Expand All @@ -331,7 +332,7 @@ public function register(): void
)
)
),
true === $config->get('app.debug') ? 'array' : 'file'
true === $config->get('app.debug') ? 'array' : $config->get('cache.default', 'file')
);
});

Expand All @@ -345,7 +346,7 @@ public function register(): void

// TODO: add cached metadata factories
$this->app->singleton(ResourceMetadataCollectionFactoryInterface::class, function (Application $app) {
/** @var ConfigRepository */
/** @var ConfigRepository $config */
$config = $app['config'];
$formats = $config->get('api-platform.formats');

Expand Down Expand Up @@ -401,7 +402,7 @@ public function register(): void
$app->make('filters')
)
),
true === $config->get('app.debug') ? 'array' : 'file'
true === $config->get('app.debug') ? 'array' : $config->get('cache.default', 'file')
);
});

Expand Down Expand Up @@ -907,6 +908,10 @@ public function register(): void
return new ReservedAttributeNameConverter($app->make(NameConverterInterface::class));
});

if (interface_exists(FieldsBuilderEnumInterface::class)) {
$this->registerGraphQl($this->app);
}

$this->app->singleton(JsonApiEntrypointNormalizer::class, function (Application $app) {
return new JsonApiEntrypointNormalizer(
$app->make(ResourceMetadataCollectionFactoryInterface::class),
Expand Down Expand Up @@ -946,9 +951,11 @@ public function register(): void
);
});

if (interface_exists(FieldsBuilderEnumInterface::class)) {
$this->registerGraphQl($this->app);
}
$this->app->singleton(JsonApiErrorNormalizer::class, function (Application $app) {
return new JsonApiErrorNormalizer(
$app->make(JsonApiItemNormalizer::class),
);
});

$this->app->singleton(JsonApiObjectNormalizer::class, function (Application $app) {
return new JsonApiObjectNormalizer(
Expand Down Expand Up @@ -985,6 +992,7 @@ public function register(): void
$list->insert($app->make(JsonApiEntrypointNormalizer::class), -800);
$list->insert($app->make(JsonApiCollectionNormalizer::class), -985);
$list->insert($app->make(JsonApiItemNormalizer::class), -890);
$list->insert($app->make(JsonApiErrorNormalizer::class), -790);
$list->insert($app->make(JsonApiObjectNormalizer::class), -995);

if (interface_exists(FieldsBuilderEnumInterface::class)) {
Expand Down
16 changes: 11 additions & 5 deletions src/Laravel/ApiResource/Error.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
name: '_api_errors_jsonapi',
outputFormats: ['jsonapi' => ['application/vnd.api+json']],
normalizationContext: ['groups' => ['jsonapi'], 'skip_null_values' => true],
uriTemplate: '/errros/{status}.jsonapi'
uriTemplate: '/errors/{status}.jsonapi'
),
],
graphQlOperations: []
Expand Down Expand Up @@ -124,6 +124,12 @@ public function getStatusCode(): int
return $this->status;
}

#[Groups(['jsonapi'])]
public function getId(): string
{
return (string) $this->status;
}

/**
* @param array<string, string> $headers
*/
Expand All @@ -132,7 +138,7 @@ public function setHeaders(array $headers): void
$this->headers = $headers;
}

#[Groups(['jsonld', 'jsonproblem'])]
#[Groups(['jsonld', 'jsonproblem', 'jsonapi'])]
public function getType(): string
{
return $this->type;
Expand All @@ -149,7 +155,7 @@ public function setType(string $type): void
$this->type = $type;
}

#[Groups(['jsonld', 'jsonproblem'])]
#[Groups(['jsonld', 'jsonproblem', 'jsonapi'])]
public function getStatus(): ?int
{
return $this->status;
Expand All @@ -160,13 +166,13 @@ public function setStatus(int $status): void
$this->status = $status;
}

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

#[Groups(['jsonld', 'jsonproblem'])]
#[Groups(['jsonld', 'jsonproblem', 'jsonapi'])]
public function getInstance(): ?string
{
return $this->instance;
Expand Down
14 changes: 7 additions & 7 deletions src/Laravel/ApiResource/ValidationError.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,25 +86,25 @@ public function getId(): string
}

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

#[Groups(['jsonld', 'json'])]
#[Groups(['jsonld', 'json', 'jsonapi'])]
public function getType(): string
{
return '/validation_errors/'.$this->id;
}

#[Groups(['jsonld', 'json'])]
#[Groups(['jsonld', 'json', 'jsonapi'])]
public function getTitle(): ?string
{
return 'Validation Error';
}

#[Groups(['jsonld', 'json'])]
#[Groups(['jsonld', 'json', 'jsonapi'])]
private string $detail;

public function getDetail(): ?string
Expand All @@ -117,7 +117,7 @@ public function setDetail(string $detail): void
$this->detail = $detail;
}

#[Groups(['jsonld', 'json'])]
#[Groups(['jsonld', 'json', 'jsonapi'])]
public function getStatus(): ?int
{
return $this->status;
Expand All @@ -128,7 +128,7 @@ public function setStatus(int $status): void
$this->status = $status;
}

#[Groups(['jsonld', 'json'])]
#[Groups(['jsonld', 'json', 'jsonapi'])]
public function getInstance(): ?string
{
return null;
Expand All @@ -138,7 +138,7 @@ public function getInstance(): ?string
* @return array<int,array{propertyPath:string,message:string,code?:string}>
*/
#[SerializedName('violations')]
#[Groups(['json', 'jsonld'])]
#[Groups(['json', 'jsonld', 'jsonapi'])]
public function getViolations(): array
{
return $this->violations;
Expand Down
9 changes: 8 additions & 1 deletion src/Laravel/State/ValidateProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,14 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
return $body;
}

$validator = Validator::make($request->request->all(), $rules);
// In Symfony, validation is done on the Resource object (here $body) using Deserialization before Validation
// Here, we did not deserialize yet, we validate on the raw body before.
$validationBody = $request->request->all();
if ('jsonapi' === $request->getRequestFormat()) {
$validationBody = $validationBody['data']['attributes'];
}

$validator = Validator::make($validationBody, $rules);
if ($validator->fails()) {
throw $this->getValidationError($validator, new ValidationException($validator));
}
Expand Down
Loading

0 comments on commit 6f71d4b

Please sign in to comment.