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

ApiPlatform\Metadata\Operations is marked as @internal but it probably shouldn't #5084

Closed
denisvmedia opened this issue Oct 23, 2022 · 1 comment

Comments

@denisvmedia
Copy link

denisvmedia commented Oct 23, 2022

API Platform version(s) affected: 3.0.2

Description
I'm migrating from v2.6.5 to v3.0.2. In v2 I used AutoGroupResourceMetadataFactory proposed in this Symfony Cast with my minor modifications.

AutoGroupResourceMetadataFactory.php
<?php
declare(strict_types=1);

namespace App\ApiPlatform;

use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;

/**
 * This class is responsible for auto-creation of normalization/denormalization groups
 * to allow flexible transformations/settings on a per-entity/per-property basis.
 *
 * @see https://symfonycasts.com/screencast/api-platform-security/resource-metadata-factory
 */
class AutoGroupResourceMetadataFactory implements ResourceMetadataFactoryInterface
{
    public function __construct(private ResourceMetadataFactoryInterface $decorated)
    {
    }

    public function create(string $resourceClass): ResourceMetadata
    {
        $resourceMetadata = $this->decorated->create($resourceClass);

        $itemOperations = $resourceMetadata->getItemOperations();
        $resourceMetadata = $resourceMetadata->withItemOperations(
            $this->updateContextOnOperations((array) $itemOperations, $resourceMetadata->getShortName() ?? '', true)
        );

        $collectionOperations = $resourceMetadata->getCollectionOperations();
        $resourceMetadata = $resourceMetadata->withCollectionOperations(
            $this->updateContextOnOperations((array) $collectionOperations, $resourceMetadata->getShortName() ?? '', false)
        );

        return $resourceMetadata;
    }

    private function updateContextOnOperations(array $operations, string $shortName, bool $isItem): array
    {
        foreach ($operations as $operationName => $operationOptions) {
            $operationOptions['normalization_context'] = $operationOptions['normalization_context'] ?? [];
            $operationOptions['normalization_context']['groups'] = $operationOptions['normalization_context']['groups'] ?? [];
            $operationOptions['normalization_context']['groups'] = array_unique(array_merge(
                $operationOptions['normalization_context']['groups'],
                $this->getDefaultGroups($shortName, true, $isItem, $operationName)
            ));
            $operationOptions['denormalization_context'] = $operationOptions['denormalization_context'] ?? [];
            $operationOptions['denormalization_context']['groups'] = $operationOptions['denormalization_context']['groups'] ?? [];
            $operationOptions['denormalization_context']['groups'] = array_unique(array_merge(
                $operationOptions['denormalization_context']['groups'],
                $this->getDefaultGroups($shortName, false, $isItem, $operationName)
            ));
            $operations[$operationName] = $operationOptions;
        }

        return $operations;
    }

    private function getDefaultGroups(string $shortName, bool $normalization, bool $isItem, string $operationName): array
    {
        // TODO: $shortName can come empty, or preg_replace may fail. Maybe throw an error?
        $shortName = (string) preg_replace('/(?<!^)[A-Z]/', '_$0', $shortName);
        $shortName = strtolower($shortName);
        $readOrWrite = $normalization ? 'read' : 'write';
        $itemOrCollection = $isItem ? 'item' : 'collection';

        return [
            // {shortName}:{read/write}
            // e.g. user:read
            sprintf('%s:%s', $shortName, $readOrWrite),
            // {shortName}:{item/collection}:{read/write}
            // e.g. user:collection:read
            sprintf('%s:%s:%s', $shortName, $itemOrCollection, $readOrWrite),
            // {shortName}:{item/collection}:{operationName}
            // e.g. user:collection:get
            sprintf('%s:%s:%s', $shortName, $itemOrCollection, $operationName),
        ];
    }
}

ResourceMetadataFactoryInterface was removed in v3. So, I rewrote the class this way (not complete yet, but works for me):

AutoGroupResourceMetadataCollectionFactory.php
<?php

declare(strict_types=1);

namespace App\ApiPlatform;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\Operations;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;

/**
 * This class is responsible for auto-creation of normalization/denormalization groups
 * to allow flexible transformations/settings on a per-entity/per-property basis.
 *
 * @see https://symfonycasts.com/screencast/api-platform-security/resource-metadata-factory
 */
class AutoGroupResourceMetadataCollectionFactory implements ResourceMetadataCollectionFactoryInterface
{
    public function __construct(private readonly ResourceMetadataCollectionFactoryInterface $decorated)
    {
    }

    public function create(string $resourceClass): ResourceMetadataCollection
    {
        $resourceMetadataCollection = $this->decorated->create($resourceClass);

        /**
         * @var ApiResource $resourceMetadata */
        foreach ($resourceMetadataCollection as $i => $resourceMetadata) {
            $operations = $resourceMetadata->getOperations();
            $resourceMetadata->withOperations(
                $this->updateContextOnOperations(
                    $operations,
                    $resourceMetadata->getShortName() ?? '',
                )
            );
        }

        return $resourceMetadataCollection;
    }

    private function updateContextOnOperations(Operations $operations, string $shortName): Operations
    {
        /** @var Operation $operation */
        foreach ($operations as $operationName => $operation) {
            $isItem = !($operation instanceof GetCollection);

            $normalizationContext = $operation->getNormalizationContext() ?? [];
            $normalizationContext['groups'] = $normalizationContext['groups'] ?? [];
            $normalizationContext['groups'] = array_unique(array_merge(
                $normalizationContext['groups'],
                $this->getDefaultGroups($shortName, true, $isItem, $operationName)
            ));
            $operation = $operation->withNormalizationContext($normalizationContext);

            $denormalizationContext = $operation->getDenormalizationContext() ?? [];
            $denormalizationContext['groups'] = $denormalizationContext['groups'] ?? [];
            $denormalizationContext['groups'] = array_unique(array_merge(
                $denormalizationContext['groups'],
                $this->getDefaultGroups($shortName, false, $isItem, $operationName)
            ));
            $operation = $operation->withDenormalizationContext($denormalizationContext);

            $operations->add($operationName, $operation);
        }

        return $operations;
    }

    private function getDefaultGroups(string $shortName, bool $normalization, bool $isItem, string $operationName): array
    {
        // TODO: $shortName can come empty, or preg_replace may fail. Maybe throw an error?
        $shortName = (string) preg_replace('/(?<!^)[A-Z]/', '_$0', $shortName);
        $shortName = strtolower($shortName);
        $readOrWrite = $normalization ? 'read' : 'write';
        $itemOrCollection = $isItem ? 'item' : 'collection';

        return [
            // {shortName}:{read/write}
            // e.g. user:read
            sprintf('%s:%s', $shortName, $readOrWrite),
            // {shortName}:{item/collection}:{read/write}
            // e.g. user:collection:read
            sprintf('%s:%s:%s', $shortName, $itemOrCollection, $readOrWrite),
            // {shortName}:{item/collection}:{operationName}
            // e.g. user:collection:get
            sprintf('%s:%s:%s', $shortName, $itemOrCollection, $operationName),
        ];
    }
}

For this to work, I had to use ApiPlatform\Metadata\Operations, but it is marked as @internal. I still can use it, but my IDE crosses it out. And as it's marked as internal, there seems to be no guarantee on the interface stability.

Also, while ApiPlatform\Metadata\Operations is marked as @internal, ApiPlatform\Metadata\Operation is not. Which seems to be inconsistent.

Finally, ApiPlatform\Metadata\ApiResource::getOperations(...) returns ?Operations and ApiPlatform\Metadata\ApiResource::withOperations(...) accepts Operations. ApiResource is not @internal and the mentioned methods are public and are not marked as @internal.

Summarizing the facts, I believe ApiPlatform\Metadata\Operations should not be considered @internal.

What do you think?

Possible Solution

Remove @internal from ApiPlatform\Metadata\Operations.

@soyuka
Copy link
Member

soyuka commented Oct 24, 2022

👍 this should be patched in the BC layer as well (2.7)

norival added a commit to norival/api-platform-core that referenced this issue Oct 24, 2022
norival added a commit to norival/api-platform-core that referenced this issue Oct 24, 2022
@dunglas dunglas closed this as completed Oct 24, 2022
soyuka added a commit that referenced this issue Nov 4, 2022
* fix: update yaml extractor test file coding standard (#5068)

* fix(graphql): add clearer error message when TwigBundle is disabled but graphQL clients are enabled (#5064)

* fix(metadata): add class key in payload argument resolver (#5067)

* fix: add class key in payload argument resolver

* add null if everything else goes wrong

* fix: upgrade command remove ApiSubresource attribute  (#5049)

Fixes #5038

* fix(doctrine): use abitrary index instead of value (#5079)

* fix: uri template should respect rfc 6570 (#5080)

* fix: remove @internal annotation for Operations (#5089)

See #5084

* fix(metadata): define a name on a single operation (#5090)

fixes #5082

* fix(metadata): deprecate when user decorates in legacy mode (#5091)

fixes #5078

* fix(graphql): use right nested operation (#5102)

* Revert "fix(graphql): use right nested operation (#5102)" (#5111)

This reverts commit 44337dd.

* fix(graphql): always allow to query nested resources (#5112)

* fix(graphql): always allow to query nested resources

* review

Co-authored-by: Alan Poulain <contact@alanpoulain.eu>

* chore: php-cs-fixer update

* fix: only alias if exists for opcache preload

Fixes api-platform/api-platform#2284 (#5110)

Co-authored-by: Liviu Mirea <liviu.mirea@wecodepixels.com>

* chore: php-cs-fixer update (#5118)

* chore: php-cs-fixer update

* chore: php-cs-fixer update

* fix(metadata): upgrade script keep operation name (#5109)

origin: #5105

Co-authored-by: WilliamPeralta <william.peralta18@gmail.com>

* chore: v2.7.3 changelog

* chore: v3.0.3 changelog

Co-authored-by: helyakin <CourcierMarvin@gmail.com>
Co-authored-by: ArnoudThibaut <thibaut.arnoud@gmail.com>
Co-authored-by: davy-beauzil <38990335+davy-beauzil@users.noreply.github.com>
Co-authored-by: Baptiste Leduc <baptiste.leduc@gmail.com>
Co-authored-by: Xavier Laviron <norival@users.noreply.github.com>
Co-authored-by: Alan Poulain <contact@alanpoulain.eu>
Co-authored-by: Liviu Cristian Mirea-Ghiban <contact@liviucmg.com>
Co-authored-by: Liviu Mirea <liviu.mirea@wecodepixels.com>
Co-authored-by: WilliamPeralta <william.peralta18@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants