QueryService::getRqlQuery()   A
last analyzed

Complexity

Conditions 4
Paths 5

Size

Total Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
dl 0
loc 31
ccs 0
cts 23
cp 0
rs 9.424
c 0
b 0
f 0
cc 4
nc 5
nop 0
crap 20
1
<?php
2
/**
3
 * QueryService
4
 */
5
namespace Graviton\RestBundle\Service;
6
7
use Doctrine\MongoDB\Query\Builder;
8
use Doctrine\ODM\MongoDB\DocumentRepository;
9
use Graviton\RestBundle\Restriction\Manager;
10
use Graviton\Rql\Node\SearchNode;
11
use Graviton\Rql\Visitor\VisitorInterface;
12
use Symfony\Component\HttpFoundation\Request;
13
use Xiag\Rql\Parser\Exception\SyntaxErrorException;
14
use Xiag\Rql\Parser\Node\LimitNode;
15
use Xiag\Rql\Parser\Node\Query\LogicOperator\AndNode;
16
use Xiag\Rql\Parser\Query;
17
18
/**
19
 * class that deals with the Request and applies it to the query builder
20
 * in order to get the results needed
21
 *
22
 * @author   List of contributors <https://github.com/libgraviton/graviton/graphs/contributors>
23
 * @license  https://opensource.org/licenses/MIT MIT License
24
 * @link     http://swisscom.ch
25
 */
26
class QueryService
27
{
28
29
    /**
30
     * @var VisitorInterface
31
     */
32
    private $visitor;
33
34
    /**
35
     * @var Manager
36
     */
37
    private $restrictionManager;
38
39
    /**
40
     * @var integer
41
     */
42
    private $paginationDefaultLimit;
43
44
    /**
45
     * @var Request
46
     */
47
    private $request;
48
49
    /**
50
     * @var Builder
51
     */
52
    private $queryBuilder;
53
54
    /**
55
     * @var DocumentRepository
56
     */
57
    private $repository;
58
59
    /**
60
     * @param VisitorInterface $visitor                visitor
61
     * @param Manager          $restrictionManager     restriction manager
62
     * @param integer          $paginationDefaultLimit default pagination limit
63
     */
64
    public function __construct(
65
        VisitorInterface $visitor,
66
        Manager $restrictionManager,
67
        $paginationDefaultLimit
68
    ) {
69
        $this->visitor = $visitor;
70
        $this->restrictionManager = $restrictionManager;
71
        $this->paginationDefaultLimit = intval($paginationDefaultLimit);
72
    }
73
74
    /**
75
     * public function that returns an array of records (or just one record if that's requested)
76
     * based on the Request passed to it.
77
     * sets all necessary stuff on the querybuilder and the request
78
     *
79
     * @param Request            $request    request
80
     * @param DocumentRepository $repository repository
81
     *
82
     * @throws \Doctrine\ODM\MongoDB\MongoDBException
83
     *
84
     * @return array|null|object either array of records or the record
85
     *
86
     */
87
    public function getWithRequest(Request &$request, DocumentRepository $repository)
88
    {
89
        $returnValue = null;
90
91
        $this->request = &$request;
92
        $this->repository = $repository;
93
        $this->queryBuilder = $repository->createQueryBuilder();
94
95
        $this->applyRqlQuery();
96
97
        if ($this->queryBuilder instanceof \Doctrine\ODM\MongoDB\Aggregation\Builder) {
98
            /**
99
             * this is only the case when queryBuilder was overridden, most likely via a PostEvent
100
             * in the rql parsing phase.
101
             */
102
            $this->queryBuilder->hydrate($repository->getClassName());
103
104
            $records = array_values($this->queryBuilder->execute()->toArray());
105
            $request->attributes->set('recordCount', count($records));
106
107
            $returnValue = $records;
108
        } elseif (is_null($this->getDocumentId())) {
109
            /**
110
             * this is or the "all" action -> multiple documents returned
111
             */
112
            $query = $this->queryBuilder->getQuery();
113
            $records = array_values($query->execute()->toArray());
114
115
            $request->attributes->set('totalCount', $query->count());
116
            $request->attributes->set('recordCount', count($records));
117
118
            $returnValue = $records;
119
        } else {
120
            /**
121
             * this is the "getAction" -> one document returned
122
             */
123
            $this->queryBuilder->field('id')->equals($this->getDocumentId());
124
125
            $query = $this->queryBuilder->getQuery();
126
            $records = array_values($query->execute()->toArray());
127
128
            if (is_array($records) && !empty($records) && is_object($records[0])) {
129
                $returnValue = $records[0];
130
            }
131
        }
132
133
        // need to set paging information?
134
        if (!is_null($returnValue) && $request->attributes->has('totalCount')) {
135
            $numPages = (int) ceil($request->attributes->get('totalCount') / $this->getPaginationPageSize());
136
            $page = (int) ceil($this->getPaginationSkip() / $this->getPaginationPageSize()) + 1;
137
            if ($numPages > 1) {
138
                $request->attributes->set('paging', true);
139
                $request->attributes->set('page', $page);
140
                $request->attributes->set('numPages', $numPages);
141
                $request->attributes->set('startAt', $this->getPaginationSkip());
142
                $request->attributes->set('perPage', $this->getPaginationPageSize());
143
            }
144
        }
145
146
        return $returnValue;
147
    }
148
149
    /**
150
     * if a single document has been requested, this returns the document id. if it returns null,
151
     * then we return multiple documents
152
     *
153
     * @return string|null either document id or null
154
     */
155
    private function getDocumentId()
156
    {
157
        return $this->request->attributes->get('singleDocument', null);
158
    }
159
160
    /**
161
     * apply all stuff from the rql query (if any) to the local querybuilder
162
     *
163
     * @return void
164
     */
165
    private function applyRqlQuery()
166
    {
167
        $rqlQuery = $this->getRqlQuery();
168
169
        // Setting RQL Query
170
        if ($rqlQuery) {
171
            // Check if search and if this Repository have search indexes.
172
            if ($query = $rqlQuery->getQuery()) {
173
                if ($query instanceof AndNode) {
174
                    foreach ($query->getQueries() as $xq) {
175
                        if ($xq instanceof SearchNode && !$this->hasSearchIndex()) {
176
                            throw new \InvalidArgumentException('Search operation not supported on this endpoint');
177
                        }
178
                    }
179
                } elseif ($query instanceof SearchNode && !$this->hasSearchIndex()) {
180
                    throw new \InvalidArgumentException('Search operation not supported on this endpoint');
181
                }
182
            }
183
184
            $this->visitor->setRepository($this->repository);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Graviton\Rql\Visitor\VisitorInterface as the method setRepository() does only exist in the following implementations of said interface: Graviton\Rql\Visitor\MongoOdm.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
185
            $this->queryBuilder = $this->visitor->visit($rqlQuery);
186
        }
187
188
        if (is_null($this->getDocumentId()) && $this->queryBuilder instanceof Builder) {
0 ignored issues
show
Coding Style introduced by
Blank line found at start of control structure
Loading history...
189
190
            /*** default sort ***/
191
            if (!array_key_exists('sort', $this->queryBuilder->getQuery()->getQuery())) {
192
                $this->queryBuilder->sort('_id');
193
            }
194
195
            /*** pagination stuff ***/
196
            if (!array_key_exists('limit', $this->queryBuilder->getQuery()->getQuery())) {
197
                $this->queryBuilder->skip($this->getPaginationSkip());
198
                $this->queryBuilder->limit($this->getPaginationPageSize());
199
            }
200
        }
201
    }
202
203
    /**
204
     * returns the correct rql query for the request, including optional specified restrictions
205
     * in the service definition (via restrictionManager)
206
     *
207
     * @return Query the query
208
     */
209
    private function getRqlQuery()
210
    {
211
        /** @var Query $rqlQuery */
212
        $rqlQuery = $this->request->attributes->get('rqlQuery', false);
213
214
        // apply field restrictions as specified in service definition
215
        $restrictionNode = $this->restrictionManager->handle($this->repository);
216
        if ($restrictionNode) {
217
            if (!$rqlQuery instanceof Query) {
218
                $rqlQuery = new Query();
219
            }
220
221
            $query = $rqlQuery->getQuery();
222
            if (is_null($query)) {
223
                // only our query
224
                $query = $restrictionNode;
225
            } else {
226
                // we have an existing query
227
                $query = new AndNode(
228
                    [
229
                        $query,
230
                        $restrictionNode
231
                    ]
232
                );
233
            }
234
235
            $rqlQuery->setQuery($query);
236
        }
237
238
        return $rqlQuery;
239
    }
240
241
    /**
242
     * Check if collection has search indexes in DB
243
     *
244
     * @return bool
245
     */
246
    private function hasSearchIndex()
247
    {
248
        $metadata = $this->repository->getClassMetadata();
249
        $indexes = $metadata->getIndexes();
250
        if (empty($indexes)) {
251
            return false;
252
        }
253
254
        $text = array_filter(
255
            $indexes,
256
            function ($index) {
257
                if (isset($index['keys'])) {
258
                    $hasText = false;
259
                    foreach ($index['keys'] as $name => $direction) {
260
                        if ($direction == 'text') {
261
                            $hasText = true;
262
                        }
263
                    }
264
                    return $hasText;
265
                }
266
            }
267
        );
268
269
        return !empty($text);
270
    }
271
272
    /**
273
     * get the pagination page size
274
     *
275
     * @return int page size
0 ignored issues
show
Documentation introduced by
Should the return type not be integer|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
276
     */
277
    private function getPaginationPageSize()
278
    {
279
        $limitNode = $this->getPaginationLimitNode();
280
281
        if ($limitNode) {
282
            $limit = $limitNode->getLimit();
283
284
            if ($limit < 1) {
285
                throw new SyntaxErrorException('invalid limit in rql');
286
            }
287
288
            return $limit;
289
        }
290
291
        return $this->paginationDefaultLimit;
292
    }
293
294
    /**
295
     * gets the pagination skip
296
     *
297
     * @return int skip
0 ignored issues
show
Documentation introduced by
Should the return type not be integer|double?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
298
     */
299
    private function getPaginationSkip()
300
    {
301
        $limitNode = $this->getPaginationLimitNode();
302
303
        if ($limitNode) {
304
            return abs($limitNode->getOffset());
305
        }
306
307
        return 0;
308
    }
309
310
    /**
311
     * gets the limit node
312
     *
313
     * @return bool|LimitNode the node or false
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use LimitNode|false.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
314
     */
315
    private function getPaginationLimitNode()
316
    {
317
        /** @var Query $rqlQuery */
318
        $rqlQuery = $this->request->attributes->get('rqlQuery');
319
320
        if ($rqlQuery instanceof Query && $rqlQuery->getLimit() instanceof LimitNode) {
321
            return $rqlQuery->getLimit();
322
        }
323
324
        return false;
325
    }
326
}
327