Completed
Push — master ( 98955b...63d555 )
by Kévin
03:49
created

OrderFilter::apply()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 12
rs 9.2
c 0
b 0
f 0
cc 4
eloc 6
nc 3
nop 5
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\Orm\Filter;
15
16
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
17
use Doctrine\Common\Persistence\ManagerRegistry;
18
use Doctrine\ORM\QueryBuilder;
19
use Psr\Log\LoggerInterface;
20
use Symfony\Component\HttpFoundation\Request;
21
use Symfony\Component\HttpFoundation\RequestStack;
22
23
/**
24
 * Order the collection by given properties.
25
 *
26
 * The ordering is done in the same sequence as they are specified in the query,
27
 * and for each property a direction value can be specified.
28
 *
29
 * For each property passed, if the resource does not have such property or if the
30
 * direction value is different from "asc" or "desc" (case insensitive), the property
31
 * is ignored.
32
 *
33
 * @author Kévin Dunglas <[email protected]>
34
 * @author Théo FIDRY <[email protected]>
35
 */
36
class OrderFilter extends AbstractContextAwareFilter
37
{
38
    const NULLS_SMALLEST = 'nulls_smallest';
39
    const NULLS_LARGEST = 'nulls_largest';
40
    const NULLS_DIRECTION_MAP = [
41
        self::NULLS_SMALLEST => [
42
            'ASC' => 'ASC',
43
            'DESC' => 'DESC',
44
        ],
45
        self::NULLS_LARGEST => [
46
            'ASC' => 'DESC',
47
            'DESC' => 'ASC',
48
        ],
49
    ];
50
51
    /**
52
     * @var string Keyword used to retrieve the value
53
     */
54
    protected $orderParameterName;
55
56
    public function __construct(ManagerRegistry $managerRegistry, RequestStack $requestStack = null, string $orderParameterName = 'order', LoggerInterface $logger = null, array $properties = null)
57
    {
58
        if (null !== $properties) {
59
            $properties = array_map(function ($propertyOptions) {
60
                // shorthand for default direction
61
                if (\is_string($propertyOptions)) {
62
                    $propertyOptions = [
63
                        'default_direction' => $propertyOptions,
64
                    ];
65
                }
66
67
                return $propertyOptions;
68
            }, $properties);
69
        }
70
71
        parent::__construct($managerRegistry, $requestStack, $logger, $properties);
72
73
        $this->orderParameterName = $orderParameterName;
74
    }
75
76
    /**
77
     * {@inheritdoc}
78
     */
79
    public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null, array $context = [])
80
    {
81
        if (!isset($context['filters'][$this->orderParameterName]) || !\is_array($context['filters'][$this->orderParameterName])) {
82
            parent::apply($queryBuilder, $queryNameGenerator, $resourceClass, $operationName, $context);
83
84
            return;
85
        }
86
87
        foreach ($context['filters'][$this->orderParameterName] as $property => $value) {
88
            $this->filterProperty($property, $value, $queryBuilder, $queryNameGenerator, $resourceClass, $operationName, $context);
0 ignored issues
show
Unused Code introduced by
The call to OrderFilter::filterProperty() has too many arguments starting with $context.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
89
        }
90
    }
91
92
    /**
93
     * {@inheritdoc}
94
     */
95
    public function getDescription(string $resourceClass): array
96
    {
97
        $description = [];
98
99
        $properties = $this->properties;
100
        if (null === $properties) {
101
            $properties = array_fill_keys($this->getClassMetadata($resourceClass)->getFieldNames(), null);
102
        }
103
104
        foreach ($properties as $property => $propertyOptions) {
105
            if (!$this->isPropertyMapped($property, $resourceClass)) {
106
                continue;
107
            }
108
109
            $description[sprintf('%s[%s]', $this->orderParameterName, $property)] = [
110
                'property' => $property,
111
                'type' => 'string',
112
                'required' => false,
113
            ];
114
        }
115
116
        return $description;
117
    }
118
119
    /**
120
     * {@inheritdoc}
121
     */
122
    protected function filterProperty(string $property, $direction, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
123
    {
124
        if (!$this->isPropertyEnabled($property, $resourceClass) || !$this->isPropertyMapped($property, $resourceClass)) {
125
            return;
126
        }
127
128
        if (empty($direction) && null !== $defaultDirection = $this->properties[$property]['default_direction'] ?? null) {
129
            // fallback to default direction
130
            $direction = $defaultDirection;
131
        }
132
133
        $direction = strtoupper($direction);
134
        if (!\in_array($direction, ['ASC', 'DESC'], true)) {
135
            return;
136
        }
137
138
        $alias = $queryBuilder->getRootAliases()[0];
139
        $field = $property;
140
141 View Code Duplication
        if ($this->isPropertyNested($property, $resourceClass)) {
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...
142
            list($alias, $field) = $this->addJoinsForNestedProperty($property, $alias, $queryBuilder, $queryNameGenerator, $resourceClass);
143
        }
144
145
        if (null !== $nullsComparison = $this->properties[$property]['nulls_comparison'] ?? null) {
146
            $nullsDirection = self::NULLS_DIRECTION_MAP[$nullsComparison][$direction];
147
148
            $nullRankHiddenField = sprintf('_%s_%s_null_rank', $alias, $field);
149
150
            $queryBuilder->addSelect(sprintf('CASE WHEN %s.%s IS NULL THEN 0 ELSE 1 END AS HIDDEN %s', $alias, $field, $nullRankHiddenField));
151
            $queryBuilder->addOrderBy($nullRankHiddenField, $nullsDirection);
152
        }
153
154
        $queryBuilder->addOrderBy(sprintf('%s.%s', $alias, $field), $direction);
155
    }
156
157
    /**
158
     * {@inheritdoc}
159
     */
160
    protected function extractProperties(Request $request/*, string $resourceClass*/): array
161
    {
162
        @trigger_error(sprintf('The use of "%s::extractProperties()" is deprecated since 2.2. Use the "filters" key of the context instead.', __CLASS__), E_USER_DEPRECATED);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
163
        $properties = $request->query->get($this->orderParameterName);
164
165
        return \is_array($properties) ? $properties : [];
166
    }
167
}
168