Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ api_platform_extras:
nullable_required: false
#Add @id as an optional property to all POST, PUT and PATCH schemas.
jsonld_update_schema: false
hydra_pagination_enrichment:
#Adds numeric pagination fields to Hydra view keys (prefix depends on api_platform.serializer.hydra_prefix).
enabled: false
# NOT IMPLEMENTED YET
simple_normalizer:
enabled: false
Expand All @@ -35,6 +38,16 @@ api_platform_extras:

Enable features by setting the corresponding flag to true.

## Hydra Pagination Enrichment Feature

`hydra_pagination_enrichment` adds numeric pagination fields (`firstPage`, `lastPage`, `currentPage`, `previousPage`, `nextPage`, `itemsPerPage`) to Hydra collection view in both schema and response.
- ! enrichment skipped if cursor pagination used

The Hydra key prefix is controlled by API Platform and is boolean:

- `api_platform.serializer.hydra_prefix: true` -> prefixed keys (for example `hydra:view`, `hydra:first`)
- `api_platform.serializer.hydra_prefix: false` (default) -> unprefixed keys (`view`, `first`)

## JWT Refresh Feature

`jwt_refresh` is active only when:
Expand Down
134 changes: 134 additions & 0 deletions src/ApiPlatform/Hydra/JsonSchema/SchemaFactoryDecorator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
<?php

declare(strict_types=1);

namespace Netgen\ApiPlatformExtras\ApiPlatform\Hydra\JsonSchema;

use ApiPlatform\JsonSchema\Schema;
use ApiPlatform\JsonSchema\SchemaFactoryAwareInterface;
use ApiPlatform\JsonSchema\SchemaFactoryInterface;
use ApiPlatform\Metadata\Operation;

use function is_array;

final class SchemaFactoryDecorator implements SchemaFactoryInterface, SchemaFactoryAwareInterface
{
private const string HYDRA_COLLECTION_BASE_SCHEMA_NAME = 'HydraCollectionBaseSchema';

private const array HYDRA_VIEW_KEYS = ['hydra:view', 'view'];

private const array PAGINATION_PROPERTIES = [
'firstPage' => [
'type' => 'integer',
'minimum' => 0,
],
'lastPage' => [
'type' => 'integer',
'minimum' => 0,
],
'currentPage' => [
'type' => 'integer',
'minimum' => 0,
],
'previousPage' => [
'type' => 'integer',
'minimum' => 0,
],
'nextPage' => [
'type' => 'integer',
'minimum' => 0,
],
'itemsPerPage' => [
'type' => 'integer',
'minimum' => 0,
],
];

private const array PAGINATION_EXAMPLE_VALUES = [
'firstPage' => 1,
'lastPage' => 10,
'currentPage' => 1,
'previousPage' => 1,
'nextPage' => 2,
'itemsPerPage' => 30,
];

public function __construct(
private readonly SchemaFactoryInterface $decorated,
) {}

public function setSchemaFactory(SchemaFactoryInterface $schemaFactory): void
{
if ($this->decorated instanceof SchemaFactoryAwareInterface) {
$this->decorated->setSchemaFactory($schemaFactory);
}
}

/** @param array<string, mixed>|null $serializerContext */
public function buildSchema(string $className, string $format = 'json', string $type = Schema::TYPE_OUTPUT, ?Operation $operation = null, ?Schema $schema = null, ?array $serializerContext = null, bool $forceCollection = false): Schema
{
$schema = $this->decorated->buildSchema($className, $format, $type, $operation, $schema, $serializerContext, $forceCollection);

if ('jsonld' !== $format) {
return $schema;
}

$definitions = $schema->getDefinitions();
$collectionBaseSchema = $definitions[self::HYDRA_COLLECTION_BASE_SCHEMA_NAME] ?? null;

if (!is_array($collectionBaseSchema)) {
return $schema;
}

$allOf = $collectionBaseSchema['allOf'] ?? null;
if (!is_array($allOf) || !isset($allOf[1]) || !is_array($allOf[1])) {
return $schema;
}

$properties = $allOf[1]['properties'] ?? null;
if (!is_array($properties)) {
return $schema;
}

if (
isset($properties['view']['properties']['firstPage'])
|| isset($properties['hydra:view']['properties']['firstPage'])
) {
return $schema;
}

foreach (self::HYDRA_VIEW_KEYS as $viewKey) {
$viewSchema = $properties[$viewKey] ?? null;
if (!is_array($viewSchema)) {
continue;
}

$viewProperties = $viewSchema['properties'] ?? [];
if (!is_array($viewProperties)) {
continue;
}

foreach (self::PAGINATION_PROPERTIES as $propertyName => $propertySchema) {
$viewProperties[$propertyName] ??= $propertySchema;
}

$viewSchema['properties'] = $viewProperties;
$viewExample = $viewSchema['example'] ?? [];
if (is_array($viewExample)) {
foreach (self::PAGINATION_EXAMPLE_VALUES as $propertyName => $propertyValue) {
$viewExample[$propertyName] ??= $propertyValue;
}

$viewSchema['example'] = $viewExample;
}

$properties[$viewKey] = $viewSchema;
}

$allOf[1]['properties'] = $properties;
$collectionBaseSchema['allOf'] = $allOf;
$definitions[self::HYDRA_COLLECTION_BASE_SCHEMA_NAME] = $collectionBaseSchema;

return $schema;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?php

declare(strict_types=1);

namespace Netgen\ApiPlatformExtras\ApiPlatform\Hydra\Serializer;

use ApiPlatform\Metadata\HttpOperation;
use ApiPlatform\State\Pagination\PaginatorInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;

use function array_key_exists;
use function is_array;
use function max;
use function min;

final class PartialCollectionViewNormalizerDecorator implements NormalizerInterface, NormalizerAwareInterface
{
public function __construct(
private readonly NormalizerInterface $decorated,
) {}

public function normalize(mixed $data, ?string $format = null, array $context = []): array|\ArrayObject|bool|float|int|string|null
{
$normalized = $this->decorated->normalize($data, $format, $context);
if (
!($data instanceof PaginatorInterface)
|| !is_array($normalized)
|| $this->isCursorPaginationEnabled($context)
) {
return $normalized;
}

$viewKey = $this->getViewKey($normalized);
if (null === $viewKey || !is_array($normalized[$viewKey])) {
return $normalized;
}

$currentPage = (int) $data->getCurrentPage();
$lastPage = (int) $data->getLastPage();

$normalized[$viewKey]['firstPage'] ??= 1;
$normalized[$viewKey]['lastPage'] ??= $lastPage;
$normalized[$viewKey]['currentPage'] ??= $currentPage;
$normalized[$viewKey]['previousPage'] ??= max(1, $currentPage - 1);
$normalized[$viewKey]['nextPage'] ??= min($currentPage + 1, $lastPage);
$normalized[$viewKey]['itemsPerPage'] ??= (int) $data->getItemsPerPage();

return $normalized;
}

public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
{
return $this->decorated->supportsNormalization($data, $format, $context);
}

/** @return array<string, bool|null> */
public function getSupportedTypes(?string $format): array
{
return $this->decorated->getSupportedTypes($format);
}

public function setNormalizer(NormalizerInterface $normalizer): void
{
if ($this->decorated instanceof NormalizerAwareInterface) {
$this->decorated->setNormalizer($normalizer);
}
}

/** @param array<string, mixed> $context */
private function isCursorPaginationEnabled(array $context): bool
{
$operation = $context['operation'] ?? null;

return $operation instanceof HttpOperation && $operation->getPaginationViaCursor() !== null;
}

/** @param array<string, mixed> $data */
private function getViewKey(array $data): ?string
{
if (array_key_exists('hydra:view', $data)) {
return 'hydra:view';
}

if (array_key_exists('view', $data)) {
return 'view';
}

return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

namespace Netgen\ApiPlatformExtras\DependencyInjection\CompilerPass;

use Netgen\ApiPlatformExtras\ApiPlatform\Hydra\JsonSchema\SchemaFactoryDecorator;
use Netgen\ApiPlatformExtras\ApiPlatform\Hydra\Serializer\PartialCollectionViewNormalizerDecorator;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;

use function sprintf;

final class HydraPaginationEnrichmentCompilerPass implements CompilerPassInterface
{
private const string BASE_FEATURE_PATH = 'netgen_api_platform_extras.features.hydra_pagination_enrichment';

public function process(ContainerBuilder $container): void
{
$featureEnabledParameter = sprintf('%s.enabled', self::BASE_FEATURE_PATH);
if (
!$container->hasParameter($featureEnabledParameter)
|| $container->getParameter($featureEnabledParameter) === false
|| !$container->hasDefinition('api_platform.hydra.json_schema.schema_factory')
) {
return;
}

$container
->setDefinition('netgen.api_platform_extras.hydra.json_schema.schema_factory', new Definition(SchemaFactoryDecorator::class))
->setArguments([
new Reference('netgen.api_platform_extras.hydra.json_schema.schema_factory.inner'),
])
->setDecoratedService('api_platform.hydra.json_schema.schema_factory');

if (!$container->hasDefinition('api_platform.hydra.normalizer.partial_collection_view')) {
return;
}

$container
->setDefinition('netgen.api_platform_extras.hydra.normalizer.partial_collection_view', new Definition(PartialCollectionViewNormalizerDecorator::class))
->setArguments([
new Reference('netgen.api_platform_extras.hydra.normalizer.partial_collection_view.inner'),
])
->setDecoratedService('api_platform.hydra.normalizer.partial_collection_view');
}
}
4 changes: 4 additions & 0 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ public function getConfigTreeBuilder(): TreeBuilder
->end()
->end()
->end()
->arrayNode('hydra_pagination_enrichment')
->info('Add numeric pagination fields to Hydra schema and collection response (hydra:view when api_platform.serializer.hydra_prefix=true, view when false).')
->canBeEnabled()
->end()
->arrayNode('simple_normalizer')
->canBeEnabled()
->end()
Expand Down
4 changes: 4 additions & 0 deletions src/NetgenApiPlatformExtrasBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Netgen\ApiPlatformExtras;

use Netgen\ApiPlatformExtras\DependencyInjection\CompilerPass\HydraPaginationEnrichmentCompilerPass;
use Netgen\ApiPlatformExtras\DependencyInjection\CompilerPass\IriTemplateGeneratorCompilerPass;
use Netgen\ApiPlatformExtras\DependencyInjection\CompilerPass\JwtRefreshCompilerPass;
use Netgen\ApiPlatformExtras\DependencyInjection\CompilerPass\SchemaDecorationCompilerPass;
Expand Down Expand Up @@ -33,6 +34,9 @@ public function build(ContainerBuilder $container): void
->addCompilerPass(
new SchemaDecorationCompilerPass(),
)
->addCompilerPass(
new HydraPaginationEnrichmentCompilerPass(),
)
->addCompilerPass(
new JwtRefreshCompilerPass(),
);
Expand Down