• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

api-platform / core / 15133993414

20 May 2025 09:30AM UTC coverage: 26.313% (-1.2%) from 27.493%
15133993414

Pull #7161

github

web-flow
Merge e2c03d45f into 5459ba375
Pull Request #7161: fix(metadata): infer parameter string type from schema

0 of 2 new or added lines in 1 file covered. (0.0%)

11019 existing lines in 363 files now uncovered.

12898 of 49018 relevant lines covered (26.31%)

34.33 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.85
/src/Symfony/Routing/IriConverter.php
1
<?php
2

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

12
declare(strict_types=1);
13

14
namespace ApiPlatform\Symfony\Routing;
15

16
use ApiPlatform\Metadata\CollectionOperationInterface;
17
use ApiPlatform\Metadata\Exception\InvalidArgumentException;
18
use ApiPlatform\Metadata\Exception\InvalidIdentifierException;
19
use ApiPlatform\Metadata\Exception\ItemNotFoundException;
20
use ApiPlatform\Metadata\Exception\OperationNotFoundException;
21
use ApiPlatform\Metadata\Exception\RuntimeException;
22
use ApiPlatform\Metadata\Get;
23
use ApiPlatform\Metadata\GetCollection;
24
use ApiPlatform\Metadata\HttpOperation;
25
use ApiPlatform\Metadata\IdentifiersExtractorInterface;
26
use ApiPlatform\Metadata\IriConverterInterface;
27
use ApiPlatform\Metadata\Operation;
28
use ApiPlatform\Metadata\Operation\Factory\OperationMetadataFactoryInterface;
29
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
30
use ApiPlatform\Metadata\ResourceClassResolverInterface;
31
use ApiPlatform\Metadata\UriVariablesConverterInterface;
32
use ApiPlatform\Metadata\UrlGeneratorInterface;
33
use ApiPlatform\Metadata\Util\AttributesExtractor;
34
use ApiPlatform\Metadata\Util\ClassInfoTrait;
35
use ApiPlatform\Metadata\Util\ResourceClassInfoTrait;
36
use ApiPlatform\State\ProviderInterface;
37
use ApiPlatform\State\UriVariablesResolverTrait;
38
use Symfony\Component\Routing\Exception\ExceptionInterface as RoutingExceptionInterface;
39
use Symfony\Component\Routing\RouterInterface;
40

41
/**
42
 * {@inheritdoc}
43
 *
44
 * @author Antoine Bluchet <soyuka@gmail.com>
45
 */
46
final class IriConverter implements IriConverterInterface
47
{
48
    use ClassInfoTrait;
49
    use ResourceClassInfoTrait;
50
    use UriVariablesResolverTrait;
51

52
    private $localOperationCache = [];
53
    private $localIdentifiersExtractorOperationCache = [];
54

55
    public function __construct(private readonly ProviderInterface $provider, private readonly RouterInterface $router, private readonly IdentifiersExtractorInterface $identifiersExtractor, ResourceClassResolverInterface $resourceClassResolver, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, ?UriVariablesConverterInterface $uriVariablesConverter = null, private readonly ?IriConverterInterface $decorated = null, private readonly ?OperationMetadataFactoryInterface $operationMetadataFactory = null)
56
    {
UNCOV
57
        $this->resourceClassResolver = $resourceClassResolver;
965✔
UNCOV
58
        $this->uriVariablesConverter = $uriVariablesConverter;
965✔
59
    }
60

61
    /**
62
     * {@inheritdoc}
63
     */
64
    public function getResourceFromIri(string $iri, array $context = [], ?Operation $operation = null): object
65
    {
66
        try {
UNCOV
67
            $parameters = $this->router->match($iri);
118✔
68
        } catch (RoutingExceptionInterface $e) {
9✔
69
            throw new InvalidArgumentException(\sprintf('No route matches "%s".', $iri), $e->getCode(), $e);
9✔
70
        }
71

UNCOV
72
        $parameters['_api_operation_name'] ??= null;
111✔
73

UNCOV
74
        if (!isset($parameters['_api_resource_class'], $parameters['_api_operation_name'])) {
111✔
75
            throw new InvalidArgumentException(\sprintf('No resource associated to "%s".', $iri));
×
76
        }
77

78
        // uri_variables come from the Request context and may not be available
UNCOV
79
        foreach ($context['uri_variables'] ?? [] as $key => $value) {
111✔
80
            if (!isset($parameters[$key]) || $parameters[$key] !== (string) $value) {
7✔
81
                throw new InvalidArgumentException(\sprintf('The iri "%s" does not reference the correct resource.', $iri));
2✔
82
            }
83
        }
84

UNCOV
85
        if ($operation && !is_a($parameters['_api_resource_class'], $operation->getClass(), true)) {
109✔
86
            throw new InvalidArgumentException(\sprintf('The iri "%s" does not reference the correct resource.', $iri));
×
87
        }
88

UNCOV
89
        $operation = $parameters['_api_operation'] = $this->resourceMetadataCollectionFactory->create($parameters['_api_resource_class'])->getOperation($parameters['_api_operation_name']);
109✔
90

UNCOV
91
        if ($operation instanceof CollectionOperationInterface) {
109✔
UNCOV
92
            throw new InvalidArgumentException(\sprintf('The iri "%s" references a collection not an item.', $iri));
1✔
93
        }
94

UNCOV
95
        if (!$operation instanceof HttpOperation) {
108✔
96
            throw new RuntimeException(\sprintf('The iri "%s" does not reference an HTTP operation.', $iri));
×
97
        }
UNCOV
98
        $attributes = AttributesExtractor::extractAttributes($parameters);
108✔
99

100
        try {
UNCOV
101
            $uriVariables = $this->getOperationUriVariables($operation, $parameters, $attributes['resource_class']);
108✔
UNCOV
102
        } catch (InvalidIdentifierException $e) {
1✔
103
            throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
×
104
        }
105

UNCOV
106
        if ($item = $this->provider->provide($operation, $uriVariables, $context)) {
107✔
UNCOV
107
            return $item;
102✔
108
        }
109

UNCOV
110
        throw new ItemNotFoundException(\sprintf('Item not found for "%s".', $iri));
5✔
111
    }
112

113
    /**
114
     * {@inheritdoc}
115
     */
116
    public function getIriFromResource(object|string $resource, int $referenceType = UrlGeneratorInterface::ABS_PATH, ?Operation $operation = null, array $context = []): ?string
117
    {
UNCOV
118
        $resourceClass = $context['force_resource_class'] ?? (\is_string($resource) ? $resource : $this->getObjectClass($resource));
888✔
119

UNCOV
120
        if ($this->operationMetadataFactory && isset($context['item_uri_template'])) {
888✔
UNCOV
121
            $operation = $this->operationMetadataFactory->create($context['item_uri_template']);
6✔
122
        }
123

UNCOV
124
        $localOperationCacheKey = ($operation?->getName() ?? '').$resourceClass.(\is_string($resource) ? '_c' : '_i');
888✔
UNCOV
125
        if ($operation && isset($this->localOperationCache[$localOperationCacheKey])) {
888✔
UNCOV
126
            return $this->generateSymfonyRoute($resource, $referenceType, $this->localOperationCache[$localOperationCacheKey], $context, $this->localIdentifiersExtractorOperationCache[$localOperationCacheKey] ?? null);
369✔
127
        }
128

UNCOV
129
        if (!$this->resourceClassResolver->isResourceClass($resourceClass)) {
888✔
UNCOV
130
            return $this->generateSkolemIri($resource, $referenceType, $operation, $context, $resourceClass);
45✔
131
        }
132

133
        // This is only for when a class (that is not a resource) extends another one that is a resource, we should remove this behavior
UNCOV
134
        if (!\is_string($resource) && !isset($context['force_resource_class'])) {
870✔
UNCOV
135
            $resourceClass = $this->getResourceClass($resource, true);
790✔
136
        }
137

UNCOV
138
        if (!$operation) {
870✔
UNCOV
139
            $operation = (new Get())->withClass($resourceClass);
790✔
140
        }
141

UNCOV
142
        if ($operation instanceof HttpOperation && 301 === $operation->getStatus()) {
870✔
143
            $operation = ($operation instanceof CollectionOperationInterface ? new GetCollection() : new Get())->withClass($operation->getClass());
1✔
144
            unset($context['uri_variables']);
1✔
145
        }
146

UNCOV
147
        $identifiersExtractorOperation = $operation;
870✔
148
        // In symfony the operation name is the route name, try to find one if none provided
149
        if (
UNCOV
150
            !$operation->getName()
870✔
UNCOV
151
            || ($operation instanceof HttpOperation && 'POST' === $operation->getMethod())
870✔
152
        ) {
UNCOV
153
            $forceCollection = $operation instanceof CollectionOperationInterface;
805✔
154
            try {
UNCOV
155
                $operation = $this->resourceMetadataCollectionFactory->create($resourceClass)->getOperation(null, $forceCollection, true);
805✔
UNCOV
156
                $identifiersExtractorOperation = $operation;
802✔
UNCOV
157
            } catch (OperationNotFoundException) {
7✔
158
            }
159
        }
160

UNCOV
161
        if (!$operation->getName() || ($operation instanceof HttpOperation && $operation->getUriTemplate() && str_starts_with($operation->getUriTemplate(), SkolemIriConverter::$skolemUriTemplate))) {
870✔
UNCOV
162
            return $this->generateSkolemIri($resource, $referenceType, $operation, $context, $resourceClass);
17✔
163
        }
164

UNCOV
165
        $this->localOperationCache[$localOperationCacheKey] = $operation;
863✔
UNCOV
166
        $this->localIdentifiersExtractorOperationCache[$localOperationCacheKey] = $identifiersExtractorOperation;
863✔
167

UNCOV
168
        return $this->generateSymfonyRoute($resource, $referenceType, $operation, $context, $identifiersExtractorOperation);
863✔
169
    }
170

171
    private function generateSkolemIri(object|string $resource, int $referenceType = UrlGeneratorInterface::ABS_PATH, ?Operation $operation = null, array $context = [], ?string $resourceClass = null): string
172
    {
UNCOV
173
        if (!$this->decorated) {
61✔
UNCOV
174
            throw new InvalidArgumentException(\sprintf('Unable to generate an IRI for the item of type "%s"', $resourceClass));
2✔
175
        }
176

177
        // Use a skolem iri, the route is defined in genid.xml
UNCOV
178
        return $this->decorated->getIriFromResource($resource, $referenceType, $operation, $context);
59✔
179
    }
180

181
    private function generateSymfonyRoute(object|string $resource, int $referenceType = UrlGeneratorInterface::ABS_PATH, ?Operation $operation = null, array $context = [], ?Operation $identifiersExtractorOperation = null): string
182
    {
UNCOV
183
        $identifiers = $context['uri_variables'] ?? [];
863✔
184

UNCOV
185
        if (\is_object($resource)) {
863✔
186
            try {
UNCOV
187
                $identifiers = $this->identifiersExtractor->getIdentifiersFromItem($resource, $identifiersExtractorOperation, $context);
786✔
UNCOV
188
            } catch (InvalidArgumentException|RuntimeException $e) {
10✔
189
                // We can try using context uri variables if any
UNCOV
190
                if (!$identifiers) {
10✔
UNCOV
191
                    throw new InvalidArgumentException(\sprintf('Unable to generate an IRI for the item of type "%s"', $operation->getClass()), $e->getCode(), $e);
6✔
192
                }
193
            }
194
        }
195

196
        try {
UNCOV
197
            $routeName = $operation instanceof HttpOperation ? ($operation->getRouteName() ?? $operation->getName()) : $operation->getName();
863✔
198

UNCOV
199
            return $this->router->generate($routeName, $identifiers, $operation->getUrlGenerationStrategy() ?? $referenceType);
863✔
UNCOV
200
        } catch (RoutingExceptionInterface $e) {
40✔
UNCOV
201
            throw new InvalidArgumentException(\sprintf('Unable to generate an IRI for the item of type "%s"', $operation->getClass()), $e->getCode(), $e);
40✔
202
        }
203
    }
204
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc