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

Factory/SerializerPropertyMetadataFactory.php (1 issue)

Labels
Severity
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\Metadata\Property\Factory;
15
16
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
17
use ApiPlatform\Core\Exception\ResourceClassNotFoundException;
18
use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
19
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
20
use ApiPlatform\Core\Util\ResourceClassInfoTrait;
21
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface as SerializerClassMetadataFactoryInterface;
22
23
/**
24
 * Populates read/write and link status using serialization groups.
25
 *
26
 * @author Kévin Dunglas <[email protected]>
27
 * @author Teoh Han Hui <[email protected]>
28
 */
29
final class SerializerPropertyMetadataFactory implements PropertyMetadataFactoryInterface
30
{
31
    use ResourceClassInfoTrait;
32
33
    private $serializerClassMetadataFactory;
34
    private $decorated;
35
36
    public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, SerializerClassMetadataFactoryInterface $serializerClassMetadataFactory, PropertyMetadataFactoryInterface $decorated, ResourceClassResolverInterface $resourceClassResolver = null)
37
    {
38
        $this->resourceMetadataFactory = $resourceMetadataFactory;
39
        $this->serializerClassMetadataFactory = $serializerClassMetadataFactory;
40
        $this->decorated = $decorated;
41
        $this->resourceClassResolver = $resourceClassResolver;
42
    }
43
44
    /**
45
     * {@inheritdoc}
46
     */
47
    public function create(string $resourceClass, string $property, array $options = []): PropertyMetadata
48
    {
49
        $propertyMetadata = $this->decorated->create($resourceClass, $property, $options);
50
51
        // in case of a property inherited (in a child class), we need it's properties
52
        // to be mapped against serialization groups instead of the parent ones.
53
        if (null !== ($childResourceClass = $propertyMetadata->getChildInherited())) {
54
            $resourceClass = $childResourceClass;
55
        }
56
57
        try {
58
            [$normalizationGroups, $denormalizationGroups] = $this->getEffectiveSerializerGroups($options, $resourceClass);
59
        } catch (ResourceClassNotFoundException $e) {
60
            // TODO: for input/output classes, the serializer groups must be read from the actual resource class
61
            return $propertyMetadata;
62
        }
63
64
        $propertyMetadata = $this->transformReadWrite($propertyMetadata, $resourceClass, $property, $normalizationGroups, $denormalizationGroups);
65
66
        return $this->transformLinkStatus($propertyMetadata, $normalizationGroups, $denormalizationGroups);
67
    }
68
69
    /**
70
     * Sets readable/writable based on matching normalization/denormalization groups.
71
     *
72
     * A false value is never reset as it could be unreadable/unwritable for other reasons.
73
     * If normalization/denormalization groups are not specified, the property is implicitly readable/writable.
74
     *
75
     * @param string[]|null $normalizationGroups
76
     * @param string[]|null $denormalizationGroups
77
     */
78
    private function transformReadWrite(PropertyMetadata $propertyMetadata, string $resourceClass, string $propertyName, array $normalizationGroups = null, array $denormalizationGroups = null): PropertyMetadata
79
    {
80
        $groups = $this->getPropertySerializerGroups($resourceClass, $propertyName);
81
82
        if (false !== $propertyMetadata->isReadable()) {
83
            $propertyMetadata = $propertyMetadata->withReadable(null === $normalizationGroups || !empty(array_intersect($normalizationGroups, $groups)));
84
        }
85
86
        if (false !== $propertyMetadata->isWritable()) {
87
            $propertyMetadata = $propertyMetadata->withWritable(null === $denormalizationGroups || !empty(array_intersect($denormalizationGroups, $groups)));
88
        }
89
90
        return $propertyMetadata;
91
    }
92
93
    /**
94
     * Sets readableLink/writableLink based on matching normalization/denormalization groups.
95
     *
96
     * If normalization/denormalization groups are not specified,
97
     * set link status to false since embedding of resource must be explicitly enabled
98
     *
99
     * @param string[]|null $normalizationGroups
100
     * @param string[]|null $denormalizationGroups
101
     */
102
    private function transformLinkStatus(PropertyMetadata $propertyMetadata, array $normalizationGroups = null, array $denormalizationGroups = null): PropertyMetadata
103
    {
104
        // No need to check link status if property is not readable and not writable
105
        if (false === $propertyMetadata->isReadable() && false === $propertyMetadata->isWritable()) {
106
            return $propertyMetadata;
107
        }
108
109
        $type = $propertyMetadata->getType();
110
        if (null === $type) {
111
            return $propertyMetadata;
112
        }
113
114
        $relatedClass = $type->isCollection() && ($collectionValueType = $type->getCollectionValueType()) ? $collectionValueType->getClassName() : $type->getClassName();
115
116
        // if property is not a resource relation, don't set link status (as it would have no meaning)
117
        if (null === $relatedClass || !$this->isResourceClass($relatedClass)) {
118
            return $propertyMetadata;
119
        }
120
121
        // find the resource class
122
        // this prevents serializer groups on non-resource child class from incorrectly influencing the decision
123
        if (null !== $this->resourceClassResolver) {
124
            $relatedClass = $this->resourceClassResolver->getResourceClass(null, $relatedClass);
125
        }
126
127
        $relatedGroups = $this->getClassSerializerGroups($relatedClass);
128
129
        if (null === $propertyMetadata->isReadableLink()) {
130
            $propertyMetadata = $propertyMetadata->withReadableLink(null !== $normalizationGroups && !empty(array_intersect($normalizationGroups, $relatedGroups)));
131
        }
132
133
        if (null === $propertyMetadata->isWritableLink()) {
134
            $propertyMetadata = $propertyMetadata->withWritableLink(null !== $denormalizationGroups && !empty(array_intersect($denormalizationGroups, $relatedGroups)));
135
        }
136
137
        return $propertyMetadata;
138
    }
139
140
    /**
141
     * Gets the effective serializer groups used in normalization/denormalization.
142
     *
143
     * Groups are extracted in the following order:
144
     *
145
     * - From the "serializer_groups" key of the $options array.
146
     * - From metadata of the given operation ("collection_operation_name" and "item_operation_name" keys).
147
     * - From metadata of the current resource.
148
     *
149
     * @throws ResourceClassNotFoundException
150
     *
151
     * @return (string[]|null)[]
152
     */
153
    private function getEffectiveSerializerGroups(array $options, string $resourceClass): array
154
    {
155
        if (isset($options['serializer_groups'])) {
156
            $groups = (array) $options['serializer_groups'];
157
158
            return [$groups, $groups];
159
        }
160
161
        $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
0 ignored issues
show
The method create() does not exist on null. ( Ignorable by Annotation )

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

161
        /** @scrutinizer ignore-call */ 
162
        $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
162
        if (isset($options['collection_operation_name'])) {
163
            $normalizationContext = $resourceMetadata->getCollectionOperationAttribute($options['collection_operation_name'], 'normalization_context', null, true);
164
            $denormalizationContext = $resourceMetadata->getCollectionOperationAttribute($options['collection_operation_name'], 'denormalization_context', null, true);
165
        } elseif (isset($options['item_operation_name'])) {
166
            $normalizationContext = $resourceMetadata->getItemOperationAttribute($options['item_operation_name'], 'normalization_context', null, true);
167
            $denormalizationContext = $resourceMetadata->getItemOperationAttribute($options['item_operation_name'], 'denormalization_context', null, true);
168
        } elseif (isset($options['graphql_operation_name'])) {
169
            $normalizationContext = $resourceMetadata->getGraphqlAttribute($options['graphql_operation_name'], 'normalization_context', null, true);
170
            $denormalizationContext = $resourceMetadata->getGraphqlAttribute($options['graphql_operation_name'], 'denormalization_context', null, true);
171
        } else {
172
            $normalizationContext = $resourceMetadata->getAttribute('normalization_context');
173
            $denormalizationContext = $resourceMetadata->getAttribute('denormalization_context');
174
        }
175
176
        return [
177
            isset($normalizationContext['groups']) ? (array) $normalizationContext['groups'] : null,
178
            isset($denormalizationContext['groups']) ? (array) $denormalizationContext['groups'] : null,
179
        ];
180
    }
181
182
    /**
183
     * Gets the serializer groups defined on a property.
184
     *
185
     * @return string[]
186
     */
187
    private function getPropertySerializerGroups(string $class, string $property): array
188
    {
189
        $serializerClassMetadata = $this->serializerClassMetadataFactory->getMetadataFor($class);
190
191
        foreach ($serializerClassMetadata->getAttributesMetadata() as $serializerAttributeMetadata) {
192
            if ($property === $serializerAttributeMetadata->getName()) {
193
                return $serializerAttributeMetadata->getGroups();
194
            }
195
        }
196
197
        return [];
198
    }
199
200
    /**
201
     * Gets all serializer groups used in a class.
202
     *
203
     * @return string[]
204
     */
205
    private function getClassSerializerGroups(string $class): array
206
    {
207
        $serializerClassMetadata = $this->serializerClassMetadataFactory->getMetadataFor($class);
208
209
        $groups = [];
210
        foreach ($serializerClassMetadata->getAttributesMetadata() as $serializerAttributeMetadata) {
211
            $groups = array_merge($groups, $serializerAttributeMetadata->getGroups());
212
        }
213
214
        return array_unique($groups);
215
    }
216
}
217