Completed
Pull Request — master (#410)
by
unknown
06:58
created

getCollectionOperationHydraDoc()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 11
rs 9.4285
cc 3
eloc 6
nc 3
nop 3
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
namespace ApiPlatform\Core\Bridge\NelmioApiDoc\Extractor\AnnotationsProvider;
13
14
use ApiPlatform\Core\Api\FilterCollection;
15
use ApiPlatform\Core\Api\OperationMethodResolverInterface;
16
use ApiPlatform\Core\Bridge\NelmioApiDoc\ApiPlatformParser;
17
use ApiPlatform\Core\Hydra\ApiDocumentationBuilderInterface;
18
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
19
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface;
20
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
21
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
22
use Nelmio\ApiDocBundle\Extractor\AnnotationsProviderInterface;
23
use Symfony\Component\HttpFoundation\Request;
24
use Symfony\Component\Routing\Route;
25
use Symfony\Component\Routing\RouterInterface;
26
27
/**
28
 * Creates Nelmio ApiDoc annotations for the api platform.
29
 *
30
 * @author Kévin Dunglas <[email protected]>
31
 */
32
final class ApiPlatformProvider implements AnnotationsProviderInterface
33
{
34
    private $resourceNameCollectionFactory;
35
    private $apiDocumentationBuilder;
36
    private $resourceMetadataFactory;
37
    private $filters;
38
    private $operationMethodResolver;
39
    private $router;
40
41
    public function __construct(ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ApiDocumentationBuilderInterface $apiDocumentationBuilder, ResourceMetadataFactoryInterface $resourceMetadataFactory, FilterCollection $filters, OperationMethodResolverInterface $operationMethodResolver, RouterInterface $router)
42
    {
43
        $this->resourceNameCollectionFactory = $resourceNameCollectionFactory;
44
        $this->apiDocumentationBuilder = $apiDocumentationBuilder;
45
        $this->resourceMetadataFactory = $resourceMetadataFactory;
46
        $this->filters = $filters;
47
        $this->operationMethodResolver = $operationMethodResolver;
48
        $this->router = $router;
49
    }
50
51
    /**
52
     * {@inheritdoc}
53
     */
54
    public function getAnnotations() : array
55
    {
56
        $annotations = [];
57
        $hydraDoc = $this->apiDocumentationBuilder->getApiDocumentation();
58
        $entrypointHydraDoc = $this->getResourceHydraDoc($hydraDoc, '#Entrypoint');
59
60
        foreach ($this->resourceNameCollectionFactory->create() as $resourceClass) {
61
            $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
62
63
            $prefixedShortName = ($iri = $resourceMetadata->getIri()) ? $iri : '#'.$resourceMetadata->getShortName();
64
            $resourceHydraDoc = $this->getResourceHydraDoc($hydraDoc, $prefixedShortName);
65
66
            if ($hydraDoc) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $hydraDoc of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
67 View Code Duplication
                foreach ($resourceMetadata->getCollectionOperations() as $operationName => $operation) {
0 ignored issues
show
Bug introduced by
The expression $resourceMetadata->getCollectionOperations() of type null|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
68
                    $annotations[] = $this->getApiDoc(true, $resourceClass, $resourceMetadata, $operationName, $operation, $resourceHydraDoc, $entrypointHydraDoc);
0 ignored issues
show
Bug introduced by
It seems like $resourceHydraDoc defined by $this->getResourceHydraD...oc, $prefixedShortName) on line 64 can also be of type null; however, ApiPlatform\Core\Bridge\...rmProvider::getApiDoc() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
Bug introduced by
It seems like $entrypointHydraDoc defined by $this->getResourceHydraD...ydraDoc, '#Entrypoint') on line 58 can also be of type null; however, ApiPlatform\Core\Bridge\...rmProvider::getApiDoc() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
69
                }
70
71 View Code Duplication
                foreach ($resourceMetadata->getItemOperations() as $operationName => $operation) {
0 ignored issues
show
Bug introduced by
The expression $resourceMetadata->getItemOperations() of type null|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
72
                    $annotations[] = $this->getApiDoc(false, $resourceClass, $resourceMetadata, $operationName, $operation, $resourceHydraDoc);
0 ignored issues
show
Bug introduced by
It seems like $resourceHydraDoc defined by $this->getResourceHydraD...oc, $prefixedShortName) on line 64 can also be of type null; however, ApiPlatform\Core\Bridge\...rmProvider::getApiDoc() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
73
                }
74
            }
75
        }
76
77
        return $annotations;
78
    }
79
80
    /**
81
     * Builds ApiDoc annotation from ApiPlatform data.
82
     *
83
     * @param bool             $collection
84
     * @param string           $resourceClass
85
     * @param ResourceMetadata $resourceMetadata
86
     * @param string           $operationName
87
     * @param array            $operation
88
     * @param array            $resourceHydraDoc
89
     * @param array            $entrypointHydraDoc
90
     *
91
     * @return ApiDoc
92
     */
93
    private function getApiDoc(bool $collection, string $resourceClass, ResourceMetadata $resourceMetadata, string $operationName, array $operation, array $resourceHydraDoc, array $entrypointHydraDoc = []) : ApiDoc
0 ignored issues
show
Unused Code introduced by
The parameter $operation is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
94
    {
95
        if ($collection) {
96
            $method = $this->operationMethodResolver->getCollectionOperationMethod($resourceClass, $operationName);
97
            $operationHydraDoc = $this->getCollectionOperationHydraDoc($resourceMetadata->getShortName(), $method, $entrypointHydraDoc);            
98
        } else {
99
            $method = $this->operationMethodResolver->getItemOperationMethod($resourceClass, $operationName);
100
            $operationHydraDoc = $this->getOperationHydraDoc($method, $resourceHydraDoc);
101
        }
102
103
        $route = $this->getRoute($resourceClass, $collection, $operationName);
104
105
        $data = [
106
            'resource' => $route->getPath(),
107
            'description' => $operationHydraDoc['hydra:title'],
108
            'resourceDescription' => $resourceHydraDoc['hydra:title'],
109
            'section' => $resourceHydraDoc['hydra:title'],
110
        ];
111
112 View Code Duplication
        if (isset($operationHydraDoc['expects']) && 'owl:Nothing' !== $operationHydraDoc['expects']) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
113
            $data['input'] = sprintf('%s:%s', ApiPlatformParser::IN_PREFIX, $resourceClass);
114
        }
115
116 View Code Duplication
        if (isset($operationHydraDoc['returns']) && 'owl:Nothing' !== $operationHydraDoc['returns']) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
117
            $data['output'] = sprintf('%s:%s', ApiPlatformParser::OUT_PREFIX, $resourceClass);
118
        }
119
120
        if ($collection && Request::METHOD_GET === $method) {
121
            $resourceFilters = $resourceMetadata->getCollectionOperationAttribute($operationName, 'filters', [], true);
122
123
            $data['filters'] = [];
124
            foreach ($this->filters as $filterName => $filter) {
125
                if (in_array($filterName, $resourceFilters)) {
126
                    foreach ($filter->getDescription($resource) as $name => $definition) {
0 ignored issues
show
Bug introduced by
The variable $resource does not exist. Did you mean $resourceClass?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
127
                        $data['filters'][] = ['name' => $name] + $definition;
128
                    }
129
                }
130
            }
131
        }
132
133
        $apiDoc = new ApiDoc($data);
134
        $apiDoc->setRoute($route);
135
136
        return $apiDoc;
137
    }
138
139
    /**
140
     * Gets Hydra documentation for the given resource.
141
     *
142
     * @param array  $hydraApiDoc
143
     * @param string $prefixedShortName
144
     *
145
     * @return array|null
146
     */
147
    private function getResourceHydraDoc(array $hydraApiDoc, string $prefixedShortName)
148
    {
149
        foreach ($hydraApiDoc['hydra:supportedClass'] as $supportedClass) {
150
            if ($supportedClass['@id'] === $prefixedShortName) {
151
                return $supportedClass;
152
            }
153
        }
154
    }
155
156
    /**
157
     * Gets the Hydra documentation of a given operation.
158
     *
159
     * @param string $method
160
     * @param array  $hydraDoc
161
     *
162
     * @return array|null
163
     */
164
    private function getOperationHydraDoc(string $method, array $hydraDoc)
165
    {
166
        foreach ($hydraDoc['hydra:supportedOperation'] as $supportedOperation) {
167
            if ($supportedOperation['hydra:method'] === $method) {
168
                return $supportedOperation;
169
            }
170
        }
171
    }
172
173
    /**
174
     * Gets the Hydra documentation for the collection operation.
175
     *
176
     * @param string $shortName
177
     * @param string $method
178
     * @param array  $hydraEntrypointDoc
179
     *
180
     * @return array|null
181
     */
182
    private function getCollectionOperationHydraDoc(string $shortName, string $method, array $hydraEntrypointDoc)
183
    {
184
        $propertyName = '#Entrypoint/'.lcfirst($shortName);
185
186
        foreach ($hydraEntrypointDoc['hydra:supportedProperty'] as $supportedProperty) {
187
            $hydraProperty = $supportedProperty['hydra:property'];
188
            if ($hydraProperty['@id'] === $propertyName) {
189
                return $this->getOperationHydraDoc($method, $hydraProperty);
190
            }
191
        }
192
    }
193
194
    /**
195
     * Finds the route for an operation on a resource.
196
     *
197
     * @param string $resourceClass
198
     * @param bool   $collection
199
     * @param string $operationName
200
     *
201
     * @return Route
202
     *
203
     * @throws \InvalidArgumentException
204
     */
205
    private function getRoute(string $resourceClass, bool $collection, string $operationName) : Route
206
    {
207
        $operationNameKey = sprintf('_%s_operation_name', $collection ? 'collection' : 'item');
208
        $found = false;
209
210
        foreach ($this->router->getRouteCollection()->all() as $routeName => $route) {
211
            $currentResourceClass = $route->getDefault('_resource_class');
212
            $currentOperationName = $route->getDefault($operationNameKey);
213
214
            if ($resourceClass === $currentResourceClass && $operationName === $currentOperationName) {
215
                $found = true;
216
                break;
217
            }
218
        }
219
220
        if (!$found) {
221
            throw new \InvalidArgumentException(sprintf('No route found for operation "%s" for type "%s".', $operationName, $resourceClass));
222
        }
223
224
        return $route;
0 ignored issues
show
Bug introduced by
The variable $route seems to be defined by a foreach iteration on line 210. Are you sure the iterator is never empty, otherwise this variable is not defined?

It seems like you are relying on a variable being defined by an iteration:

foreach ($a as $b) {
}

// $b is defined here only if $a has elements, for example if $a is array()
// then $b would not be defined here. To avoid that, we recommend to set a
// default value for $b.


// Better
$b = 0; // or whatever default makes sense in your context
foreach ($a as $b) {
}

// $b is now guaranteed to be defined here.
Loading history...
225
    }
226
}
227