Passed
Push — master ( 8bd912...d93388 )
by Alan
06:58 queued 02:20
created

Operation/Factory/SubresourceOperationFactory.php (1 issue)

1
<?php
2
3
/*
4
 * This file is part of the API Platform project.
5
 *
6
 * (c) Kévin Dunglas <[email protected]>
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\Core\Operation\Factory;
15
16
use ApiPlatform\Core\Bridge\Symfony\Routing\RouteNameGenerator;
17
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
18
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
19
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
20
use ApiPlatform\Core\Operation\PathSegmentNameGeneratorInterface;
21
22
/**
23
 * @internal
24
 */
25
final class SubresourceOperationFactory implements SubresourceOperationFactoryInterface
26
{
27
    public const SUBRESOURCE_SUFFIX = '_subresource';
28
    public const FORMAT_SUFFIX = '.{_format}';
29
    public const ROUTE_OPTIONS = ['defaults' => [], 'requirements' => [], 'options' => [], 'host' => '', 'schemes' => [], 'condition' => '', 'controller' => null];
30
31
    private $resourceMetadataFactory;
32
    private $propertyNameCollectionFactory;
33
    private $propertyMetadataFactory;
34
    private $pathSegmentNameGenerator;
35
36
    public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, PathSegmentNameGeneratorInterface $pathSegmentNameGenerator)
37
    {
38
        $this->resourceMetadataFactory = $resourceMetadataFactory;
39
        $this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
40
        $this->propertyMetadataFactory = $propertyMetadataFactory;
41
        $this->pathSegmentNameGenerator = $pathSegmentNameGenerator;
42
    }
43
44
    /**
45
     * {@inheritdoc}
46
     */
47
    public function create(string $resourceClass): array
48
    {
49
        $tree = [];
50
        $this->computeSubresourceOperations($resourceClass, $tree);
51
52
        return $tree;
53
    }
54
55
    /**
56
     * Handles subresource operations recursively and declare their corresponding routes.
57
     *
58
     * @param string $rootResourceClass null on the first iteration, it then keeps track of the origin resource class
59
     * @param array  $parentOperation   the previous call operation
60
     * @param int    $depth             the number of visited
61
     * @param int    $maxDepth
62
     */
63
    private function computeSubresourceOperations(string $resourceClass, array &$tree, string $rootResourceClass = null, array $parentOperation = null, array $visited = [], int $depth = 0, int $maxDepth = null): void
64
    {
65
        if (null === $rootResourceClass) {
66
            $rootResourceClass = $resourceClass;
67
        }
68
69
        foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $property) {
70
            $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $property);
71
72
            if (!$subresource = $propertyMetadata->getSubresource()) {
73
                continue;
74
            }
75
76
            $subresourceClass = $subresource->getResourceClass();
77
            $subresourceMetadata = $this->resourceMetadataFactory->create($subresourceClass);
78
            $isLastItem = $resourceClass === $parentOperation['resource_class'] && $propertyMetadata->isIdentifier();
79
80
            // A subresource that is also an identifier can't be a start point
81
            if ($isLastItem && (null === $parentOperation || false === $parentOperation['collection'])) {
82
                continue;
83
            }
84
85
            $visiting = "$resourceClass $property $subresourceClass";
86
87
            // Handle maxDepth
88
            if ($rootResourceClass === $resourceClass) {
89
                $maxDepth = $subresource->getMaxDepth();
90
                // reset depth when we return to rootResourceClass
91
                $depth = 0;
92
            }
93
94
            if (null !== $maxDepth && $depth >= $maxDepth) {
95
                break;
96
            }
97
            if (isset($visited[$visiting])) {
98
                continue;
99
            }
100
101
            $rootResourceMetadata = $this->resourceMetadataFactory->create($rootResourceClass);
102
            $operationName = 'get';
103
            $operation = [
104
                'property' => $property,
105
                'collection' => $subresource->isCollection(),
106
                'resource_class' => $subresourceClass,
107
                'shortNames' => [$subresourceMetadata->getShortName()],
108
            ];
109
110
            if (null === $parentOperation) {
111
                $rootShortname = $rootResourceMetadata->getShortName();
112
                $operation['identifiers'] = [['id', $rootResourceClass, true]];
113
                $operation['operation_name'] = sprintf(
114
                    '%s_%s%s',
115
                    RouteNameGenerator::inflector($operation['property'], $operation['collection'] ?? false),
116
                    $operationName,
117
                    self::SUBRESOURCE_SUFFIX
118
                );
119
120
                $subresourceOperation = $rootResourceMetadata->getSubresourceOperations()[$operation['operation_name']] ?? [];
121
122
                $operation['route_name'] = sprintf(
123
                    '%s%s_%s',
124
                    RouteNameGenerator::ROUTE_NAME_PREFIX,
125
                    RouteNameGenerator::inflector($rootShortname),
126
                    $operation['operation_name']
127
                );
128
129
                $prefix = trim(trim($rootResourceMetadata->getAttribute('route_prefix', '')), '/');
130
                if ('' !== $prefix) {
131
                    $prefix .= '/';
132
                }
133
134
                $operation['path'] = $subresourceOperation['path'] ?? sprintf(
135
                    '/%s%s/{id}/%s%s',
136
                    $prefix,
137
                    $this->pathSegmentNameGenerator->getSegmentName($rootShortname),
0 ignored issues
show
It seems like $rootShortname can also be of type null; however, parameter $name of ApiPlatform\Core\Operati...rface::getSegmentName() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

137
                    $this->pathSegmentNameGenerator->getSegmentName(/** @scrutinizer ignore-type */ $rootShortname),
Loading history...
138
                    $this->pathSegmentNameGenerator->getSegmentName($operation['property'], $operation['collection']),
139
                    self::FORMAT_SUFFIX
140
                );
141
142
                if (!\in_array($rootShortname, $operation['shortNames'], true)) {
143
                    $operation['shortNames'][] = $rootShortname;
144
                }
145
            } else {
146
                $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
147
                $operation['identifiers'] = $parentOperation['identifiers'];
148
                $operation['identifiers'][] = [$parentOperation['property'], $resourceClass, $isLastItem ? true : $parentOperation['collection']];
149
                $operation['operation_name'] = str_replace(
150
                    'get'.self::SUBRESOURCE_SUFFIX,
151
                    RouteNameGenerator::inflector($isLastItem ? 'item' : $property, $operation['collection']).'_get'.self::SUBRESOURCE_SUFFIX,
152
                    $parentOperation['operation_name']
153
                );
154
                $operation['route_name'] = str_replace($parentOperation['operation_name'], $operation['operation_name'], $parentOperation['route_name']);
155
156
                if (!\in_array($resourceMetadata->getShortName(), $operation['shortNames'], true)) {
157
                    $operation['shortNames'][] = $resourceMetadata->getShortName();
158
                }
159
160
                $subresourceOperation = $rootResourceMetadata->getSubresourceOperations()[$operation['operation_name']] ?? [];
161
162
                if (isset($subresourceOperation['path'])) {
163
                    $operation['path'] = $subresourceOperation['path'];
164
                } else {
165
                    $operation['path'] = str_replace(self::FORMAT_SUFFIX, '', (string) $parentOperation['path']);
166
167
                    if ($parentOperation['collection']) {
168
                        [$key] = end($operation['identifiers']);
169
                        $operation['path'] .= sprintf('/{%s}', $key);
170
                    }
171
172
                    if ($isLastItem) {
173
                        $operation['path'] .= self::FORMAT_SUFFIX;
174
                    } else {
175
                        $operation['path'] .= sprintf('/%s%s', $this->pathSegmentNameGenerator->getSegmentName($property, $operation['collection']), self::FORMAT_SUFFIX);
176
                    }
177
                }
178
            }
179
180
            foreach (self::ROUTE_OPTIONS as $routeOption => $defaultValue) {
181
                $operation[$routeOption] = $subresourceOperation[$routeOption] ?? $defaultValue;
182
            }
183
184
            $tree[$operation['route_name']] = $operation;
185
186
            $this->computeSubresourceOperations($subresourceClass, $tree, $rootResourceClass, $operation, $visited + [$visiting => true], ++$depth, $maxDepth);
187
        }
188
    }
189
}
190