Completed
Push — master ( 2975e7...46c3ee )
by Rafael
07:49
created

AllNodesWithPagination::applyWhere()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
dl 0
loc 26
ccs 0
cts 13
cp 0
rs 9.504
c 0
b 0
f 0
cc 4
nc 3
nop 3
crap 20
1
<?php
2
/*******************************************************************************
3
 *  This file is part of the GraphQL Bundle package.
4
 *
5
 *  (c) YnloUltratech <[email protected]>
6
 *
7
 *  For the full copyright and license information, please view the LICENSE
8
 *  file that was distributed with this source code.
9
 ******************************************************************************/
10
11
namespace Ynlo\GraphQLBundle\Query\Node;
12
13
use Doctrine\ORM\Query\Expr\Orx;
14
use Doctrine\ORM\QueryBuilder;
15
use GraphQL\Error\Error;
16
use Ynlo\GraphQLBundle\Definition\InputObjectDefinition;
17
use Ynlo\GraphQLBundle\Definition\ObjectDefinition;
18
use Ynlo\GraphQLBundle\Definition\Plugin\PaginationDefinitionPlugin;
19
use Ynlo\GraphQLBundle\Filter\FilterContext;
20
use Ynlo\GraphQLBundle\Filter\FilterInterface;
21
use Ynlo\GraphQLBundle\Model\ConnectionInterface;
22
use Ynlo\GraphQLBundle\Model\NodeConnection;
23
use Ynlo\GraphQLBundle\Model\NodeInterface;
24
use Ynlo\GraphQLBundle\Pagination\DoctrineCursorPaginatorInterface;
25
use Ynlo\GraphQLBundle\Pagination\DoctrineOffsetCursorPaginator;
26
use Ynlo\GraphQLBundle\Pagination\PaginationRequest;
27
use Ynlo\GraphQLBundle\Resolver\QueryExecutionContext;
28
29
/**
30
 * Base class to fetch nodes
31
 */
32
class AllNodesWithPagination extends AllNodes
33
{
34
    /**
35
     * @param array[]               $args
36
     * @param QueryExecutionContext $context
37
     *
38
     * @return mixed
39
     *
40
     * @throws Error
41
     */
42
    public function __invoke($args = [], QueryExecutionContext $context)
43
    {
44
        $orderBy = $args['orderBy'] ?? [];
45
        $first = $args['first'] ?? null;
46
        $last = $args['last'] ?? null;
47
        $before = $args['before'] ?? null;
48
        $after = $args['after'] ?? null;
49
        $search = $args['search'] ?? null;
50
        $filters = $args['filters'] ?? null;
51
        $where = $args['where'] ?? null;
52
53
        $this->initialize();
54
55
        $qb = $this->createQuery();
56
        $this->applyOrderBy($qb, $orderBy);
57
58
        if ($this->getContext()->getRoot()) {
59
            $this->applyFilterByParent($qb, $this->getContext()->getRoot());
60
        }
61
62
        if ($search) {
63
            $this->search($qb, $search);
64
        }
65
66
        if ($filters) {
67
            $this->applyFilters($qb, $filters);
0 ignored issues
show
Deprecated Code introduced by
The function Ynlo\GraphQLBundle\Query...ination::applyFilters() has been deprecated: since v1.1, `applyWhere` should be used instead ( Ignorable by Annotation )

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

67
            /** @scrutinizer ignore-deprecated */ $this->applyFilters($qb, $filters);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
68
        }
69
70
        if ($where) {
71
            $this->applyWhere($context, $qb, $where);
72
        }
73
74
        $this->configureQuery($qb);
75
        foreach ($this->extensions as $extension) {
76
            $extension->configureQuery($qb, $this, $this->context);
77
        }
78
79
        if (!$first && !$last) {
80
            $error = sprintf('You must provide a `first` or `last` value to properly paginate records in "%s" connection.', $this->queryDefinition->getName());
81
            throw new Error($error);
82
        }
83
84
        if ($this->queryDefinition->hasMeta('pagination')) {
85
            $limitAllowed = $this->queryDefinition->getMeta('pagination')['limit'];
86
87
            if ($first > $limitAllowed || $last > $limitAllowed) {
88
                $current = $first ?? $last;
89
                $where = $first ? 'first' : 'last';
90
                $error = sprintf(
91
                    'Requesting %s records for `%s` exceeds the `%s` limit of %s records for "%s" connection',
92
                    $current,
93
                    $this->queryDefinition->getName(),
94
                    $where,
95
                    $limitAllowed,
96
                    $this->queryDefinition->getName()
97
                );
98
                throw new Error($error);
99
            }
100
        }
101
102
        $paginator = $this->createPaginator();
103
104
        $connection = $this->createConnection();
105
        $paginator->paginate($qb, new PaginationRequest($first, $last, $after, $before), $connection);
106
107
        return $connection;
108
    }
109
110
    protected function createConnection(): ConnectionInterface
111
    {
112
        return new NodeConnection();
113
    }
114
115
    protected function createPaginator(): DoctrineCursorPaginatorInterface
116
    {
117
        return new DoctrineOffsetCursorPaginator();
118
    }
119
120
    /**
121
     * Apply advanced filters
122
     *
123
     * @deprecated since v1.1, `applyWhere` should be used instead
124
     */
125
    protected function applyFilters(QueryBuilder $qb, array $filters)
126
    {
127
        $definition = $this->objectDefinition;
128
        foreach ($filters as $field => $value) {
129
            if (!$definition->hasField($field) || !$prop = $definition->getField($field)->getOriginName()) {
130
                continue;
131
            }
132
133
            $entityField = sprintf('%s.%s', $this->queryAlias, $prop);
134
135
            switch (gettype($value)) {
136
                case 'string':
137
                    $qb->andWhere($qb->expr()->eq($entityField, $qb->expr()->literal($value)));
138
                    break;
139
                case 'integer':
140
                case 'double':
141
                    $qb->andWhere($qb->expr()->eq($entityField, $value));
142
                    break;
143
                case 'boolean':
144
                    $qb->andWhere($qb->expr()->eq($entityField, (int) $value));
145
                    break;
146
                case 'array':
147
                    if (empty($value)) {
148
                        $qb->andWhere($qb->expr()->isNull($entityField));
149
                    } else {
150
                        $qb->andWhere($qb->expr()->in($entityField, $value));
151
                    }
152
                    break;
153
                case 'NULL':
154
                    $qb->andWhere($qb->expr()->isNull($entityField));
155
                    break;
156
            }
157
        }
158
    }
159
160
    /**
161
     * @param QueryExecutionContext $context
162
     * @param QueryBuilder          $qb
163
     * @param array                 $where
164
     *
165
     * @throws \ReflectionException
166
     */
167
    protected function applyWhere(QueryExecutionContext $context, QueryBuilder $qb, array $where): void
168
    {
169
        $whereType = $context->getDefinition()->getArgument('where')->getType();
170
171
        /** @var InputObjectDefinition $whereDefinition */
172
        $whereDefinition = $context->getEndpoint()->getType($whereType);
173
174
        /** @var ObjectDefinition $node */
175
        $node = $context->getEndpoint()->getType($context->getDefinition()->getNode());
176
177
        foreach ($where as $filterName => $condition) {
178
            $filterDefinition = $whereDefinition->getField($filterName);
179
180
            //TODO: load filters from services
181
            /** @var FilterInterface $filter */
182
            $filter = (new \ReflectionClass($filterDefinition->getResolver()))->newInstanceWithoutConstructor();
183
184
            $fieldName = $filterDefinition->getMeta('filter_field');
185
            if ($fieldName && $node->hasField($fieldName)) {
186
                $relatedField = $node->getField($fieldName);
187
                $filterContext = new FilterContext($context->getEndpoint(), $node, $relatedField);
188
            } else {
189
                $filterContext = new FilterContext($context->getEndpoint(), $node);
190
            }
191
192
            $filter($filterContext, $qb, $condition);
193
        }
194
    }
195
196
    /**
197
     * Filter some columns with simple string.
198
     */
199
    protected function search(QueryBuilder $qb, string $search)
200
    {
201
        //search every word separate
202
        $searchArray = explode(' ', $search);
203
204
        $alias = $qb->getRootAliases()[0];
205
206
        //TODO: allow some config to customize search fields
207
        $em = $this->getManager();
208
        $metadata = $em->getClassMetadata($this->entity);
209
        $searchFields = $metadata->getFieldNames();
210
211
        if (\count($searchFields) > 0) {
212
            $meta = $qb->getEntityManager()->getClassMetadata($qb->getRootEntities()[0]);
213
            foreach ($searchArray as $q) {
214
                $q = trim(rtrim($q));
215
                $id = md5($q);
216
                $orx = new Orx();
217
                foreach ($searchFields as $field) {
218
                    if (strpos($field, '.') !== false && !isset($meta->embeddedClasses[explode('.', $field)[0]])) {
219
                        $orx->add("$field LIKE :search_$id");
220
                    } else { //append current alias
221
                        $orx->add("$alias.$field LIKE :search_$id");
222
                    }
223
                }
224
                $qb->andWhere($orx);
225
                $qb->setParameter("search_$id", "%$q%");
226
            }
227
        }
228
    }
229
230
    /**
231
     * @param QueryBuilder  $qb
232
     * @param NodeInterface $root
233
     */
234
    protected function applyFilterByParent(QueryBuilder $qb, NodeInterface $root)
235
    {
236
        $parentField = null;
237
        if ($this->queryDefinition->hasMeta('pagination')) {
238
            $parentField = $this->queryDefinition->getMeta('pagination')['parent_field'] ?? null;
239
        }
240
        if (!$parentField) {
241
            throw new \RuntimeException(
242
                sprintf(
243
                    'Missing parent field to filter "%s" by given parent.
244
             The "parent_field" should be specified.',
245
                    $this->queryDefinition->getName()
246
                )
247
            );
248
        }
249
250
        if ($this->objectDefinition->hasField($parentField)) {
251
            $parentField = $this->objectDefinition->getField($parentField)->getOriginName();
252
        }
253
254
        $paramName = 'root'.mt_rand();
255
        if ($this->queryDefinition->getMeta('pagination')['parent_relation'] === PaginationDefinitionPlugin::MANY_TO_MANY) {
256
            $qb->andWhere(sprintf(':%s MEMBER OF %s.%s', $paramName, $this->queryAlias, $parentField))
257
               ->setParameter($paramName, $root);
258
        } else {
259
            $qb->andWhere(sprintf('%s.%s = :%s', $this->queryAlias, $parentField, $paramName))
260
               ->setParameter($paramName, $root);
261
        }
262
    }
263
}
264