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

Bridge/Doctrine/MongoDbOdm/Filter/SearchFilter.php (1 issue)

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\Bridge\Doctrine\MongoDbOdm\Filter;
15
16
use ApiPlatform\Core\Api\IdentifiersExtractorInterface;
17
use ApiPlatform\Core\Api\IriConverterInterface;
18
use ApiPlatform\Core\Bridge\Doctrine\Common\Filter\SearchFilterInterface;
19
use ApiPlatform\Core\Bridge\Doctrine\Common\Filter\SearchFilterTrait;
20
use ApiPlatform\Core\Exception\InvalidArgumentException;
21
use Doctrine\Common\Persistence\ManagerRegistry;
22
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
23
use Doctrine\ODM\MongoDB\Aggregation\Builder;
24
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata as MongoDBClassMetadata;
25
use Doctrine\ODM\MongoDB\Types\Type as MongoDbType;
26
use MongoDB\BSON\Regex;
27
use Psr\Log\LoggerInterface;
28
use Symfony\Component\PropertyAccess\PropertyAccess;
29
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
30
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
31
32
/**
33
 * Filter the collection by given properties.
34
 *
35
 * @experimental
36
 *
37
 * @author Kévin Dunglas <[email protected]>
38
 * @author Alan Poulain <[email protected]>
39
 */
40
final class SearchFilter extends AbstractFilter implements SearchFilterInterface
41
{
42
    use SearchFilterTrait;
43
44
    public const DOCTRINE_INTEGER_TYPE = MongoDbType::INTEGER;
45
46
    public function __construct(ManagerRegistry $managerRegistry, IriConverterInterface $iriConverter, IdentifiersExtractorInterface $identifiersExtractor, PropertyAccessorInterface $propertyAccessor = null, LoggerInterface $logger = null, array $properties = null, NameConverterInterface $nameConverter = null)
47
    {
48
        parent::__construct($managerRegistry, $logger, $properties, $nameConverter);
49
50
        $this->iriConverter = $iriConverter;
51
        $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
52
        $this->identifiersExtractor = $identifiersExtractor;
53
    }
54
55
    protected function getIriConverter(): IriConverterInterface
56
    {
57
        return $this->iriConverter;
58
    }
59
60
    protected function getPropertyAccessor(): PropertyAccessorInterface
61
    {
62
        return $this->propertyAccessor;
63
    }
64
65
    /**
66
     * {@inheritdoc}
67
     */
68
    protected function filterProperty(string $property, $value, Builder $aggregationBuilder, string $resourceClass, string $operationName = null, array &$context = [])
69
    {
70
        if (
71
            null === $value ||
72
            !$this->isPropertyEnabled($property, $resourceClass) ||
73
            !$this->isPropertyMapped($property, $resourceClass, true)
74
        ) {
75
            return;
76
        }
77
78
        $matchField = $field = $property;
79
80
        $associations = [];
81
        if ($this->isPropertyNested($property, $resourceClass)) {
82
            [$matchField, $field, $associations] = $this->addLookupsForNestedProperty($property, $aggregationBuilder, $resourceClass);
83
        }
84
85
        /**
86
         * @var MongoDBClassMetadata
87
         */
88
        $metadata = $this->getNestedMetadata($resourceClass, $associations);
89
90
        $values = $this->normalizeValues((array) $value, $property);
91
        if (null === $values) {
92
            return;
93
        }
94
95
        $caseSensitive = true;
96
97
        if ($metadata->hasField($field) && !$metadata->hasAssociation($field)) {
98
            if ('id' === $field) {
99
                $values = array_map([$this, 'getIdFromValue'], $values);
100
            }
101
102
            if (!$this->hasValidValues($values, $this->getDoctrineFieldType($property, $resourceClass))) {
103
                $this->logger->notice('Invalid filter ignored', [
104
                    'exception' => new InvalidArgumentException(sprintf('Values for field "%s" are not valid according to the doctrine type.', $field)),
105
                ]);
106
107
                return;
108
            }
109
110
            $strategy = $this->properties[$property] ?? self::STRATEGY_EXACT;
111
112
            // prefixing the strategy with i makes it case insensitive
113
            if (0 === strpos($strategy, 'i')) {
114
                $strategy = substr($strategy, 1);
115
                $caseSensitive = false;
116
            }
117
118
            $inValues = [];
119
            foreach ($values as $inValue) {
120
                $inValues[] = $this->addEqualityMatchStrategy($strategy, $field, $inValue, $caseSensitive, $metadata);
121
            }
122
123
            $aggregationBuilder
124
                ->match()
125
                ->field($matchField)
126
                ->in($inValues);
127
        }
128
129
        // metadata doesn't have the field, nor an association on the field
130
        if (!$metadata->hasAssociation($field)) {
131
            return;
132
        }
133
134
        $values = array_map([$this, 'getIdFromValue'], $values);
135
        $associationFieldIdentifier = 'id';
0 ignored issues
show
The assignment to $associationFieldIdentifier is dead and can be removed.
Loading history...
136
        $doctrineTypeField = $this->getDoctrineFieldType($property, $resourceClass);
137
138
        if (null !== $this->identifiersExtractor) {
139
            $associationResourceClass = $metadata->getAssociationTargetClass($field);
140
            $associationFieldIdentifier = $this->identifiersExtractor->getIdentifiersFromResourceClass($associationResourceClass)[0];
141
            $doctrineTypeField = $this->getDoctrineFieldType($associationFieldIdentifier, $associationResourceClass);
142
        }
143
144
        if (!$this->hasValidValues($values, $doctrineTypeField)) {
145
            $this->logger->notice('Invalid filter ignored', [
146
                'exception' => new InvalidArgumentException(sprintf('Values for field "%s" are not valid according to the doctrine type.', $property)),
147
            ]);
148
149
            return;
150
        }
151
152
        $aggregationBuilder
153
            ->match()
154
            ->field($matchField)
155
            ->in($values);
156
    }
157
158
    /**
159
     * Add equality match stage according to the strategy.
160
     *
161
     * @throws InvalidArgumentException If strategy does not exist
162
     *
163
     * @return Regex|string
164
     */
165
    private function addEqualityMatchStrategy(string $strategy, string $field, $value, bool $caseSensitive, ClassMetadata $metadata)
166
    {
167
        $type = $metadata->getTypeOfField($field);
168
169
        switch ($strategy) {
170
            case MongoDbType::STRING !== $type:
171
                return MongoDbType::getType($type)->convertToDatabaseValue($value);
172
            case null:
173
            case self::STRATEGY_EXACT:
174
                return $caseSensitive ? $value : new Regex("^$value$", 'i');
175
            case self::STRATEGY_PARTIAL:
176
                return new Regex($value, $caseSensitive ? '' : 'i');
177
            case self::STRATEGY_START:
178
                return new Regex("^$value", $caseSensitive ? '' : 'i');
179
            case self::STRATEGY_END:
180
                return new Regex("$value$", $caseSensitive ? '' : 'i');
181
            case self::STRATEGY_WORD_START:
182
                return new Regex("(^$value.*|.*\s$value.*)", $caseSensitive ? '' : 'i');
183
            default:
184
                throw new InvalidArgumentException(sprintf('strategy %s does not exist.', $strategy));
185
        }
186
    }
187
188
    /**
189
     * {@inheritdoc}
190
     */
191
    protected function getType(string $doctrineType): string
192
    {
193
        switch ($doctrineType) {
194
            case MongoDbType::INT:
195
            case MongoDbType::INTEGER:
196
                return 'int';
197
            case MongoDbType::BOOL:
198
            case MongoDbType::BOOLEAN:
199
                return 'bool';
200
            case MongoDbType::DATE:
201
                return \DateTimeInterface::class;
202
            case MongoDbType::FLOAT:
203
                return 'float';
204
        }
205
206
        return 'string';
207
    }
208
}
209