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

src/Serializer/Filter/PropertyFilter.php (2 issues)

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\Serializer\Filter;
15
16
use Symfony\Component\HttpFoundation\Request;
17
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
18
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
19
20
/**
21
 * Property filter.
22
 *
23
 * @author Baptiste Meyer <[email protected]>
24
 */
25
final class PropertyFilter implements FilterInterface
26
{
27
    private $overrideDefaultProperties;
28
    private $parameterName;
29
    private $whitelist;
30
    private $nameConverter;
31
32
    public function __construct(string $parameterName = 'properties', bool $overrideDefaultProperties = false, array $whitelist = null, NameConverterInterface $nameConverter = null)
33
    {
34
        $this->overrideDefaultProperties = $overrideDefaultProperties;
35
        $this->parameterName = $parameterName;
36
        $this->whitelist = null === $whitelist ? null : $this->formatWhitelist($whitelist);
37
        $this->nameConverter = $nameConverter;
38
    }
39
40
    /**
41
     * {@inheritdoc}
42
     */
43
    public function apply(Request $request, bool $normalization, array $attributes, array &$context)
44
    {
45
        if (null !== $propertyAttribute = $request->attributes->get('_api_filter_property')) {
46
            $properties = $propertyAttribute;
47
        } elseif (\array_key_exists($this->parameterName, $commonAttribute = $request->attributes->get('_api_filters', []))) {
48
            $properties = $commonAttribute[$this->parameterName];
49
        } else {
50
            $properties = $request->query->get($this->parameterName);
51
        }
52
53
        if (!\is_array($properties)) {
54
            return;
55
        }
56
57
        $properties = $this->denormalizeProperties($properties);
58
59
        if (null !== $this->whitelist) {
60
            $properties = $this->getProperties($properties, $this->whitelist);
61
        }
62
63
        if (!$this->overrideDefaultProperties && isset($context[AbstractNormalizer::ATTRIBUTES])) {
64
            $properties = array_merge_recursive((array) $context[AbstractNormalizer::ATTRIBUTES], $properties);
65
        }
66
67
        $context[AbstractNormalizer::ATTRIBUTES] = $properties;
68
    }
69
70
    /**
71
     * {@inheritdoc}
72
     */
73
    public function getDescription(string $resourceClass): array
74
    {
75
        $example = sprintf('%1$s[]={propertyName}&%1$s[]={anotherPropertyName}&%1$s[{nestedPropertyParent}][]={nestedProperty}',
76
            $this->parameterName
77
        );
78
79
        return [
80
            "$this->parameterName[]" => [
81
                'property' => null,
82
                'type' => 'string',
83
                'is_collection' => true,
84
                'required' => false,
85
                'swagger' => [
86
                    'description' => 'Allows you to reduce the response to contain only the properties you need. If your desired property is nested, you can address it using nested arrays. Example: '.$example,
87
                    'name' => "$this->parameterName[]",
88
                    'type' => 'array',
89
                    'items' => [
90
                        'type' => 'string',
91
                    ],
92
                ],
93
                'openapi' => [
94
                    'description' => 'Allows you to reduce the response to contain only the properties you need. If your desired property is nested, you can address it using nested arrays. Example: '.$example,
95
                    'name' => "$this->parameterName[]",
96
                    'schema' => [
97
                        'type' => 'array',
98
                        'items' => [
99
                            'type' => 'string',
100
                        ],
101
                    ],
102
                ],
103
            ],
104
        ];
105
    }
106
107
    /**
108
     * Generate an array of whitelist properties to match the format that properties
109
     * will have in the request.
110
     *
111
     * @param array $whitelist the whitelist to format
112
     *
113
     * @return array An array containing the whitelist ready to match request parameters
114
     */
115
    private function formatWhitelist(array $whitelist): array
116
    {
117
        if (array_values($whitelist) === $whitelist) {
118
            return $whitelist;
119
        }
120
        foreach ($whitelist as $name => $value) {
121
            if (null === $value) {
122
                unset($whitelist[$name]);
123
                $whitelist[] = $name;
124
            }
125
        }
126
127
        return $whitelist;
128
    }
129
130
    private function getProperties(array $properties, array $whitelist = null): array
131
    {
132
        $whitelist = $whitelist ?? $this->whitelist;
133
        $result = [];
134
135
        foreach ($properties as $key => $value) {
136
            if (is_numeric($key)) {
137
                if (\in_array($propertyName = $this->denormalizePropertyName($value), $whitelist, true)) {
0 ignored issues
show
It seems like $whitelist can also be of type null; however, parameter $haystack of in_array() does only seem to accept array, 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
                if (\in_array($propertyName = $this->denormalizePropertyName($value), /** @scrutinizer ignore-type */ $whitelist, true)) {
Loading history...
138
                    $result[] = $propertyName;
139
                }
140
141
                continue;
142
            }
143
144
            if (\is_array($value) && isset($whitelist[$key]) && $recursiveResult = $this->getProperties($value, $whitelist[$key])) {
145
                $result[$this->denormalizePropertyName($key)] = $recursiveResult;
146
            }
147
        }
148
149
        return $result;
150
    }
151
152
    private function denormalizeProperties(array $properties): array
153
    {
154
        if (null === $this->nameConverter || !$properties) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $properties 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...
155
            return $properties;
156
        }
157
158
        $result = [];
159
        foreach ($properties as $key => $value) {
160
            $result[$this->denormalizePropertyName($key)] = \is_array($value) ? $this->denormalizeProperties($value) : $this->denormalizePropertyName($value);
161
        }
162
163
        return $result;
164
    }
165
166
    private function denormalizePropertyName($property)
167
    {
168
        return null !== $this->nameConverter ? $this->nameConverter->denormalize($property) : $property;
169
    }
170
}
171