• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

api-platform / core / 3904303394

pending completion
3904303394

Pull #5332

github

GitHub
Merge 2e0a3ceae into 965332bc8
Pull Request #5332: Revert "chore(deprecation): Only use ValueResolverInterface if it exists"

2 of 2 new or added lines in 1 file covered. (100.0%)

7939 of 12085 relevant lines covered (65.69%)

81.77 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/Doctrine/Odm/Filter/SearchFilter.php
1
<?php
2

3
/*
4
 * This file is part of the API Platform project.
5
 *
6
 * (c) Kévin Dunglas <dunglas@gmail.com>
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\Doctrine\Odm\Filter;
15

16
use ApiPlatform\Api\IdentifiersExtractorInterface;
17
use ApiPlatform\Api\IriConverterInterface;
18
use ApiPlatform\Doctrine\Common\Filter\SearchFilterInterface;
19
use ApiPlatform\Doctrine\Common\Filter\SearchFilterTrait;
20
use ApiPlatform\Exception\InvalidArgumentException;
21
use ApiPlatform\Metadata\Operation;
22
use Doctrine\ODM\MongoDB\Aggregation\Builder;
23
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata as MongoDBClassMetadata;
24
use Doctrine\ODM\MongoDB\Types\Type as MongoDbType;
25
use Doctrine\Persistence\ManagerRegistry;
26
use Doctrine\Persistence\Mapping\ClassMetadata;
27
use MongoDB\BSON\Regex;
28
use Psr\Log\LoggerInterface;
29
use Symfony\Component\PropertyAccess\PropertyAccess;
30
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
31
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
32

33
/**
34
 * Filter the collection by given properties.
35
 *
36
 * @author Kévin Dunglas <dunglas@gmail.com>
37
 * @author Alan Poulain <contact@alanpoulain.eu>
38
 */
39
final class SearchFilter extends AbstractFilter implements SearchFilterInterface
40
{
41
    use SearchFilterTrait;
42

43
    public const DOCTRINE_INTEGER_TYPE = [MongoDbType::INTEGER, MongoDbType::INT];
44

45
    public function __construct(ManagerRegistry $managerRegistry, IriConverterInterface $iriConverter, ?IdentifiersExtractorInterface $identifiersExtractor, PropertyAccessorInterface $propertyAccessor = null, LoggerInterface $logger = null, array $properties = null, NameConverterInterface $nameConverter = null)
46
    {
47
        parent::__construct($managerRegistry, $logger, $properties, $nameConverter);
×
48

49
        $this->iriConverter = $iriConverter;
×
50
        $this->identifiersExtractor = $identifiersExtractor;
×
51
        $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
×
52
    }
53

54
    protected function getIriConverter(): IriConverterInterface
55
    {
56
        return $this->iriConverter;
×
57
    }
58

59
    protected function getPropertyAccessor(): PropertyAccessorInterface
60
    {
61
        return $this->propertyAccessor;
×
62
    }
63

64
    /**
65
     * {@inheritdoc}
66
     */
67
    protected function filterProperty(string $property, $value, Builder $aggregationBuilder, string $resourceClass, Operation $operation = null, array &$context = []): void
68
    {
69
        if (
×
70
            null === $value ||
×
71
            !$this->isPropertyEnabled($property, $resourceClass) ||
×
72
            !$this->isPropertyMapped($property, $resourceClass, true)
×
73
        ) {
74
            return;
×
75
        }
76

77
        $matchField = $field = $property;
×
78

79
        $values = $this->normalizeValues((array) $value, $property);
×
80
        if (null === $values) {
×
81
            return;
×
82
        }
83

84
        $associations = [];
×
85
        if ($this->isPropertyNested($property, $resourceClass)) {
×
86
            [$matchField, $field, $associations] = $this->addLookupsForNestedProperty($property, $aggregationBuilder, $resourceClass);
×
87
        }
88

89
        $caseSensitive = true;
×
90
        $strategy = $this->properties[$property] ?? self::STRATEGY_EXACT;
×
91

92
        // prefixing the strategy with i makes it case insensitive
93
        if (str_starts_with($strategy, 'i')) {
×
94
            $strategy = substr($strategy, 1);
×
95
            $caseSensitive = false;
×
96
        }
97

98
        /** @var MongoDBClassMetadata */
99
        $metadata = $this->getNestedMetadata($resourceClass, $associations);
×
100

101
        if ($metadata->hasField($field) && !$metadata->hasAssociation($field)) {
×
102
            if ('id' === $field) {
×
103
                $values = array_map($this->getIdFromValue(...), $values);
×
104
            }
105

106
            if (!$this->hasValidValues($values, $this->getDoctrineFieldType($property, $resourceClass))) {
×
107
                $this->logger->notice('Invalid filter ignored', [
×
108
                    'exception' => new InvalidArgumentException(sprintf('Values for field "%s" are not valid according to the doctrine type.', $field)),
×
109
                ]);
110

111
                return;
×
112
            }
113

114
            $this->addEqualityMatchStrategy($strategy, $aggregationBuilder, $field, $matchField, $values, $caseSensitive, $metadata);
×
115

116
            return;
×
117
        }
118

119
        // metadata doesn't have the field, nor an association on the field
120
        if (!$metadata->hasAssociation($field)) {
×
121
            return;
×
122
        }
123

124
        $values = array_map($this->getIdFromValue(...), $values);
×
125

126
        $associationResourceClass = $metadata->getAssociationTargetClass($field);
×
127
        $associationFieldIdentifier = $metadata->getIdentifierFieldNames()[0];
×
128
        $doctrineTypeField = $this->getDoctrineFieldType($associationFieldIdentifier, $associationResourceClass);
×
129

130
        if (!$this->hasValidValues($values, $doctrineTypeField)) {
×
131
            $this->logger->notice('Invalid filter ignored', [
×
132
                'exception' => new InvalidArgumentException(sprintf('Values for field "%s" are not valid according to the doctrine type.', $property)),
×
133
            ]);
134

135
            return;
×
136
        }
137

138
        $this->addEqualityMatchStrategy($strategy, $aggregationBuilder, $field, $matchField, $values, $caseSensitive, $metadata);
×
139
    }
140

141
    /**
142
     * Add equality match stage according to the strategy.
143
     */
144
    private function addEqualityMatchStrategy(string $strategy, Builder $aggregationBuilder, string $field, string $matchField, array $values, bool $caseSensitive, ClassMetadata $metadata): void
145
    {
146
        $inValues = [];
×
147
        foreach ($values as $inValue) {
×
148
            $inValues[] = $this->getEqualityMatchStrategyValue($strategy, $field, $inValue, $caseSensitive, $metadata);
×
149
        }
150

151
        $aggregationBuilder
152
            ->match()
×
153
            ->field($matchField)
×
154
            ->in($inValues);
×
155
    }
156

157
    /**
158
     * Get equality match value according to the strategy.
159
     *
160
     * @throws InvalidArgumentException If strategy does not exist
161
     */
162
    private function getEqualityMatchStrategyValue(string $strategy, string $field, mixed $value, bool $caseSensitive, ClassMetadata $metadata): mixed
163
    {
164
        $type = $metadata->getTypeOfField($field);
×
165

166
        if (!MongoDbType::hasType($type)) {
×
167
            return $value;
×
168
        }
169
        if (MongoDbType::STRING !== $type) {
×
170
            return MongoDbType::getType($type)->convertToDatabaseValue($value);
×
171
        }
172

173
        $quotedValue = preg_quote($value);
×
174

175
        return match ($strategy) {
×
176
            self::STRATEGY_EXACT => $caseSensitive ? $value : new Regex("^$quotedValue$", 'i'),
×
177
            self::STRATEGY_PARTIAL => new Regex($quotedValue, $caseSensitive ? '' : 'i'),
×
178
            self::STRATEGY_START => new Regex("^$quotedValue", $caseSensitive ? '' : 'i'),
×
179
            self::STRATEGY_END => new Regex("$quotedValue$", $caseSensitive ? '' : 'i'),
×
180
            self::STRATEGY_WORD_START => new Regex("(^$quotedValue.*|.*\s$quotedValue.*)", $caseSensitive ? '' : 'i'),
×
181
            default => throw new InvalidArgumentException(sprintf('strategy %s does not exist.', $strategy)),
×
182
        };
183
    }
184

185
    /**
186
     * {@inheritdoc}
187
     */
188
    protected function getType(string $doctrineType): string
189
    {
190
        return match ($doctrineType) {
×
191
            MongoDbType::INT, MongoDbType::INTEGER => 'int',
×
192
            MongoDbType::BOOL, MongoDbType::BOOLEAN => 'bool',
×
193
            MongoDbType::DATE, MongoDbType::DATE_IMMUTABLE => \DateTimeInterface::class,
×
194
            MongoDbType::FLOAT => 'float',
×
195
            default => 'string',
×
196
        };
197
    }
198
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc