Completed
Push — feature/cleanup-search ( 3778bb )
by
unknown
13:54 queued 05:34
created

DocumentModel::buildSearchQuery()   C

Complexity

Conditions 11
Paths 15

Size

Total Lines 48
Code Lines 34

Duplication

Lines 10
Ratio 20.83 %

Code Coverage

Tests 0
CRAP Score 132

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 10
loc 48
ccs 0
cts 41
cp 0
rs 5.2653
cc 11
eloc 34
nc 15
nop 2
crap 132

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * Use doctrine odm as backend
4
 */
5
6
namespace Graviton\RestBundle\Model;
7
8
use Doctrine\ODM\MongoDB\DocumentManager;
9
use Doctrine\ODM\MongoDB\DocumentRepository;
10
use Graviton\Rql\Node\SearchNode;
11
use Graviton\SchemaBundle\Model\SchemaModel;
12
use Graviton\SecurityBundle\Entities\SecurityUser;
13
use Symfony\Bridge\Monolog\Logger;
14
use Symfony\Component\HttpFoundation\Request;
15
use Doctrine\ODM\MongoDB\Query\Builder;
16
use Graviton\Rql\Visitor\MongoOdm as Visitor;
17
use Xiag\Rql\Parser\Node\LimitNode;
18
use Xiag\Rql\Parser\Node\Query\AbstractLogicOperatorNode;
19
use Xiag\Rql\Parser\Query;
20
use Graviton\ExceptionBundle\Exception\RecordOriginModifiedException;
21
use Xiag\Rql\Parser\Exception\SyntaxErrorException as RqlSyntaxErrorException;
22
use Graviton\SchemaBundle\Document\Schema as SchemaDocument;
23
use Xiag\Rql\Parser\Query as XiagQuery;
24
use \Doctrine\ODM\MongoDB\Query\Builder as MongoBuilder;
25
26
/**
27
 * Use doctrine odm as backend
28
 *
29
 * @author  List of contributors <https://github.com/libgraviton/graviton/graphs/contributors>
30
 * @license http://opensource.org/licenses/gpl-license.php GNU Public License
31
 * @link    http://swisscom.ch
32
 */
33
class DocumentModel extends SchemaModel implements ModelInterface
34
{
35
    /**
36
     * @var string
37
     */
38
    protected $description;
39
    /**
40
     * @var string[]
41
     */
42
    protected $fieldTitles;
43
    /**
44
     * @var string[]
45
     */
46
    protected $fieldDescriptions;
47
    /**
48
     * @var string[]
49
     */
50
    protected $requiredFields = array();
51
    /**
52
     * @var string[]
53
     */
54
    protected $searchableFields = array();
55
    /**
56
     * @var DocumentRepository
57
     */
58
    private $repository;
59
    /**
60
     * @var Visitor
61
     */
62
    private $visitor;
63
    /**
64
     * @var array
65
     */
66
    protected $notModifiableOriginRecords;
67
    /**
68
     * @var  integer
69
     */
70
    private $paginationDefaultLimit;
71
72
    /**
73
     * @var boolean
74
     */
75
    protected $filterByAuthUser;
76
77
    /**
78
     * @var string
79
     */
80
    protected $filterByAuthField;
81
82
    /**
83
     * @var DocumentManager
84
     */
85
    protected $manager;
86
87
    /**
88
     * @param Visitor $visitor                    rql query visitor
89
     * @param array   $notModifiableOriginRecords strings with not modifiable recordOrigin values
90
     * @param integer $paginationDefaultLimit     amount of data records to be returned when in pagination context
91
     */
92 4
    public function __construct(
93
        Visitor $visitor,
94
        $notModifiableOriginRecords,
95
        $paginationDefaultLimit
96
    ) {
97 4
        parent::__construct();
98 4
        $this->visitor = $visitor;
99 4
        $this->notModifiableOriginRecords = $notModifiableOriginRecords;
100 4
        $this->paginationDefaultLimit = (int) $paginationDefaultLimit;
101 4
    }
102
103
    /**
104
     * get repository instance
105
     *
106
     * @return DocumentRepository
107
     */
108 2
    public function getRepository()
109
    {
110 2
        return $this->repository;
111
    }
112
113
    /**
114
     * create new app model
115
     *
116
     * @param DocumentRepository $repository Repository of countries
117
     *
118
     * @return \Graviton\RestBundle\Model\DocumentModel
119
     */
120 4
    public function setRepository(DocumentRepository $repository)
121
    {
122 4
        $this->repository = $repository;
123 4
        $this->manager = $repository->getDocumentManager();
124
125 4
        return $this;
126
    }
127
128
    /**
129
     * {@inheritDoc}
130
     *
131
     * @param Request        $request The request object
132
     * @param SecurityUser   $user    SecurityUser Object
0 ignored issues
show
Documentation introduced by
Should the type for parameter $user not be null|SecurityUser?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
133
     * @param SchemaDocument $schema  Schema model used for search fields extraction
0 ignored issues
show
Documentation introduced by
Should the type for parameter $schema not be null|SchemaDocument?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
134
     *
135
     * @return array
136
     */
137
    public function findAll(Request $request, SecurityUser $user = null, SchemaDocument $schema = null)
138
    {
139
        $pageNumber = $request->query->get('page', 1);
140
        $numberPerPage = (int) $request->query->get('perPage', $this->getDefaultLimit());
141
        $startAt = ($pageNumber - 1) * $numberPerPage;
142
143
        /** @var XiagQuery $xiagQuery */
144
        $xiagQuery = $request->attributes->get('rqlQuery');
145
146
        /** @var MongoBuilder $queryBuilder */
147
        $queryBuilder = $this->repository
148
            ->createQueryBuilder();
149
150
        // Setting RQL Query
151
        if ($xiagQuery) {
152
            // Clean up Search rql param and set it as Doctrine query
153
            if ($xiagQuery->getQuery() && $this->hasCustomSearchIndex() && (float) $this->getMongoDBVersion() >= 2.6) {
154
                $searchQueries = $this->buildSearchQuery($xiagQuery, $queryBuilder);
155
                $xiagQuery = $searchQueries['xiagQuery'];
156
                $queryBuilder = $searchQueries['queryBuilder'];
157
            }
158
            $queryBuilder = $this->doRqlQuery(
159
                $queryBuilder,
160
                $xiagQuery
161
            );
162
        } else {
163
            // @todo [lapistano]: seems the offset is missing for this query.
164
            /** @var \Doctrine\ODM\MongoDB\Query\Builder $qb */
165
            $queryBuilder->find($this->repository->getDocumentName());
166
        }
167
168
169
        /** @var LimitNode $rqlLimit */
170
        $rqlLimit = $xiagQuery instanceof XiagQuery ? $xiagQuery->getLimit() : false;
171
172
        // define offset and limit
173
        if (!$rqlLimit || !$rqlLimit->getOffset()) {
174
            $queryBuilder->skip($startAt);
0 ignored issues
show
Bug introduced by
The method skip does only exist in Doctrine\ODM\MongoDB\Query\Builder, but not in Doctrine\ODM\MongoDB\Query\Expr.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
175
        } else {
176
            $startAt = (int) $rqlLimit->getOffset();
177
            $queryBuilder->skip($startAt);
178
        }
179
180
        if (!$rqlLimit || is_null($rqlLimit->getLimit())) {
181
            $queryBuilder->limit($numberPerPage);
0 ignored issues
show
Bug introduced by
The method limit does only exist in Doctrine\ODM\MongoDB\Query\Builder, but not in Doctrine\ODM\MongoDB\Query\Expr.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
182
        } else {
183
            $numberPerPage = (int) $rqlLimit->getLimit();
184
            $queryBuilder->limit($numberPerPage);
185
        }
186
187
        // Limit can not be negative nor null.
188
        if ($numberPerPage < 1) {
189
            throw new RqlSyntaxErrorException('negative or null limit in rql');
190
        }
191
192
        /**
193
         * add a default sort on id if none was specified earlier
194
         *
195
         * not specifying something to sort on leads to very weird cases when fetching references.
196
         */
197
        if (!array_key_exists('sort', $queryBuilder->getQuery()->getQuery())) {
198
            $queryBuilder->sort('_id');
199
        }
200
201
        // run query
202
        $query = $queryBuilder->getQuery();
203
        $records = array_values($query->execute()->toArray());
204
205
        $totalCount = $query->count();
206
        $numPages = (int) ceil($totalCount / $numberPerPage);
207
        $page = (int) ceil($startAt / $numberPerPage) + 1;
208
        if ($numPages > 1) {
209
            $request->attributes->set('paging', true);
210
            $request->attributes->set('page', $page);
211
            $request->attributes->set('numPages', $numPages);
212
            $request->attributes->set('startAt', $startAt);
213
            $request->attributes->set('perPage', $numberPerPage);
214
            $request->attributes->set('totalCount', $totalCount);
215
        }
216
217
        return $records;
218
    }
219
220
    private function buildSearchQuery(XiagQuery $xiagQuery, MongoBuilder $queryBuilder)
221
    {
222
        $innerQuery = $xiagQuery->getQuery();
223
        $hasSearch = false;
224
        $nodes = [];
225
        if ($innerQuery instanceof AbstractLogicOperatorNode) {
226
            foreach ($innerQuery->getQueries() as $key => $innerRql) {
227
                if ($innerRql instanceof SearchNode) {
228 View Code Duplication
                    if (!$hasSearch) {
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...
229
                        $searchString = implode(' ', $innerRql->getSearchTerms());
230
                        $queryBuilder->addAnd($queryBuilder->expr()->text($searchString));
231
                        $hasSearch = true;
232
                    }
233
                } else {
234
                    $nodes[] = $innerRql;
235
                }
236
            }
237 View Code Duplication
        } elseif ($innerQuery instanceof SearchNode) {
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...
238
            $searchString = implode(' ', $innerQuery->getSearchTerms());
239
            $queryBuilder->addAnd($queryBuilder->expr()->text($searchString));
240
            $hasSearch = true;
241
        }
242
        // Remove the Search from RQL xiag
243
        if ($hasSearch && $nodes) {
244
            $newXiagQuery = new XiagQuery();
245
            $newXiagQuery->setLimit($xiagQuery->getLimit());
246
            if ($xiagQuery->getSelect()) {
247
                $newXiagQuery->setSelect($xiagQuery->getSelect());
248
            }
249
            if ($xiagQuery->getSort()) {
250
                $newXiagQuery->setSort($xiagQuery->getSort());
251
            }
252
            $binderClass = get_class($innerQuery);
253
            /** @var AbstractLogicOperatorNode $newBinder */
254
            $newBinder = new $binderClass();
255
            foreach ($nodes as $node) {
256
                $newBinder->addQuery($node);
257
            }
258
            $newXiagQuery->setQuery($newBinder);
259
            // Reset original query, so that there is no Search param
260
            $xiagQuery = $newXiagQuery;
261
            $queryBuilder->sortMeta('score', 'textScore');
262
        }
263
        return [
264
            'xiagQuery'     => $xiagQuery,
265
            'queryBuilder'  => $queryBuilder
266
        ];
267
    }
268
269
    /**
270
     * @param string $prefix the prefix for custom text search indexes
271
     * @return bool
272
     * @throws \Doctrine\ODM\MongoDB\MongoDBException
273
     */
274
    private function hasCustomSearchIndex($prefix = 'search')
275
    {
276
        $collection = $this->repository->getDocumentManager()->getDocumentCollection($this->repository->getClassName());
277
        $indexesInfo = $collection->getIndexInfo();
278
        foreach ($indexesInfo as $indexInfo) {
279
            if ($indexInfo['name']==$prefix.$collection->getName().'Index') {
280
                return true;
281
            }
282
        }
283
        return false;
284
    }
285
286
    /**
287
     * @return string the version of the MongoDB as a string
288
     */
289
    private function getMongoDBVersion()
290
    {
291
        $buildInfo = $this->repository->getDocumentManager()->getDocumentDatabase(
292
            $this->repository->getClassName()
293
        )->command(['buildinfo'=>1]);
294
        if (isset($buildInfo['version'])) {
295
            return $buildInfo['version'];
296
        } else {
297
            return "unkown";
298
        }
299
    }
300
301
    /**
302
     * @param object $entity       entity to insert
303
     * @param bool   $returnEntity true to return entity
304
     * @param bool   $doFlush      if we should flush or not after insert
305
     *
306
     * @return Object|null
307
     */
308
    public function insertRecord($entity, $returnEntity = true, $doFlush = true)
309
    {
310
        $this->checkIfOriginRecord($entity);
311
        $this->manager->persist($entity);
312
313
        if ($doFlush) {
314
            $this->manager->flush($entity);
315
        }
316
        if ($returnEntity) {
317
            return $this->find($entity->getId());
318
        }
319
    }
320
321
    /**
322
     * @param string $documentId id of entity to find
323
     *
324
     * @return Object
0 ignored issues
show
Documentation introduced by
Should the return type not be object|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...
325
     */
326 4
    public function find($documentId)
327
    {
328 4
        return $this->repository->find($documentId);
329
    }
330
331
    /**
332
     * {@inheritDoc}
333
     *
334
     * @param string $documentId   id of entity to update
335
     * @param Object $entity       new entity
336
     * @param bool   $returnEntity true to return entity
337
     *
338
     * @return Object|null
339
     */
340 2
    public function updateRecord($documentId, $entity, $returnEntity = true)
341
    {
342
        // In both cases the document attribute named originRecord must not be 'core'
343 2
        $this->checkIfOriginRecord($entity);
344 2
        $this->checkIfOriginRecord($this->selectSingleFields($documentId, ['recordOrigin']));
0 ignored issues
show
Bug introduced by
It seems like $this->selectSingleField... array('recordOrigin')) targeting Graviton\RestBundle\Mode...l::selectSingleFields() can also be of type array or null; however, Graviton\RestBundle\Mode...::checkIfOriginRecord() does only seem to accept object, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
345
346 2
        if (!is_null($documentId)) {
347 2
            $this->deleteById($documentId);
348
            // detach so odm knows it's gone
349 2
            $this->manager->detach($entity);
350 2
            $this->manager->clear();
351 1
        }
352
353 2
        $entity = $this->manager->merge($entity);
354
355 2
        $this->manager->persist($entity);
356 2
        $this->manager->flush($entity);
357
358 2
        if ($returnEntity) {
359
            return $entity;
360
        }
361 2
    }
362
363
    /**
364
     * {@inheritDoc}
365
     *
366
     * @param string|object $id id of entity to delete or entity instance
367
     *
368
     * @return null|Object
369
     */
370
    public function deleteRecord($id)
371
    {
372
        if (is_object($id)) {
373
            $entity = $id;
374
        } else {
375
            $entity = $this->find($id);
376
        }
377
378
        $return = $entity;
379
        if ($entity) {
380
            $this->checkIfOriginRecord($entity);
381
            $this->manager->remove($entity);
382
            $this->manager->flush();
383
            $return = null;
384
        }
385
386
        return $return;
387
    }
388
389
    /**
390
     * Triggers a flush on the DocumentManager
391
     *
392
     * @param null $document optional document
393
     *
394
     * @return void
395
     */
396
    public function flush($document = null)
397
    {
398
        $this->manager->flush($document);
399
    }
400
401
    /**
402
     * A low level delete without any checks
403
     *
404
     * @param mixed $id record id
405
     *
406
     * @return void
407
     */
408 2
    private function deleteById($id)
409
    {
410 2
        $builder = $this->repository->createQueryBuilder();
411
        $builder
412 2
            ->remove()
413 2
            ->field('id')->equals($id)
414 2
            ->getQuery()
415 2
            ->execute();
416 2
    }
417
418
    /**
419
     * Checks in a performant way if a certain record id exists in the database
420
     *
421
     * @param mixed $id record id
422
     *
423
     * @return bool true if it exists, false otherwise
424
     */
425 4
    public function recordExists($id)
0 ignored issues
show
Coding Style introduced by
function recordExists() does not seem to conform to the naming convention (^(?:is|has|should|may|supports)).

This check examines a number of code elements and verifies that they conform to the given naming conventions.

You can set conventions for local variables, abstract classes, utility classes, constant, properties, methods, parameters, interfaces, classes, exceptions and special methods.

Loading history...
426
    {
427 4
        return is_array($this->selectSingleFields($id, ['id'], false));
428
    }
429
430
    /**
431
     * Returns a set of fields from an existing resource in a performant manner.
432
     * If you need to check certain fields on an object (and don't need everything), this
433
     * is a better way to get what you need.
434
     * If the record is not present, you will receive null. If you don't need an hydrated
435
     * instance, make sure to pass false there.
436
     *
437
     * @param mixed $id      record id
438
     * @param array $fields  list of fields you need.
439
     * @param bool  $hydrate whether to hydrate object or not
440
     *
441
     * @return array|null|object
442
     */
443 4
    public function selectSingleFields($id, array $fields, $hydrate = true)
444
    {
445 4
        $builder = $this->repository->createQueryBuilder();
446 4
        $idField = $this->repository->getClassMetadata()->getIdentifier()[0];
447
448
        $record = $builder
449 4
            ->field($idField)->equals($id)
450 4
            ->select($fields)
451 4
            ->hydrate($hydrate)
452 4
            ->getQuery()
453 4
            ->getSingleResult();
454
455 4
        return $record;
456
    }
457
458
    /**
459
     * get classname of entity
460
     *
461
     * @return string|null
462
     */
463 4
    public function getEntityClass()
464
    {
465 4
        if ($this->repository instanceof DocumentRepository) {
466 4
            return $this->repository->getDocumentName();
467
        }
468
469
        return null;
470
    }
471
472
    /**
473
     * {@inheritDoc}
474
     *
475
     * Currently this is being used to build the route id used for redirecting
476
     * to newly made documents. It might benefit from having a different name
477
     * for those purposes.
478
     *
479
     * We might use a convention based mapping here:
480
     * Graviton\CoreBundle\Document\App -> mongodb://graviton_core
481
     * Graviton\CoreBundle\Entity\Table -> mysql://graviton_core
482
     *
483
     * @todo implement this in a more convention based manner
484
     *
485
     * @return string
486
     */
487
    public function getConnectionName()
488
    {
489
        $bundle = strtolower(substr(explode('\\', get_class($this))[1], 0, -6));
490
491
        return 'graviton.' . $bundle;
492
    }
493
494
    /**
495
     * Does the actual query using the RQL Bundle.
496
     *
497
     * @param Builder $queryBuilder Doctrine ODM QueryBuilder
498
     * @param Query   $query        query from parser
499
     *
500
     * @return array
0 ignored issues
show
Documentation introduced by
Should the return type not be Builder|\Doctrine\ODM\MongoDB\Query\Expr?

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...
501
     */
502
    protected function doRqlQuery($queryBuilder, Query $query)
503
    {
504
        $this->visitor->setBuilder($queryBuilder);
505
506
        return $this->visitor->visit($query);
507
    }
508
509
    /**
510
     * Checks the recordOrigin attribute of a record and will throw an exception if value is not allowed
511
     *
512
     * @param Object $record record
513
     *
514
     * @return void
515
     */
516 14
    protected function checkIfOriginRecord($record)
517
    {
518 7
        if ($record instanceof RecordOriginInterface
519 14
            && !$record->isRecordOriginModifiable()
520 7
        ) {
521 6
            $values = $this->notModifiableOriginRecords;
522 6
            $originValue = strtolower(trim($record->getRecordOrigin()));
523
524 6
            if (in_array($originValue, $values)) {
525 2
                $msg = sprintf("Must not be one of the following keywords: %s", implode(', ', $values));
526
527 2
                throw new RecordOriginModifiedException($msg);
528
            }
529 2
        }
530 12
    }
531
532
    /**
533
     * Determines the configured amount fo data records to be returned in pagination context.
534
     *
535
     * @return int
536
     */
537
    private function getDefaultLimit()
538
    {
539
        if (0 < $this->paginationDefaultLimit) {
540
            return $this->paginationDefaultLimit;
541
        }
542
543
        return 10;
544
    }
545
546
    /**
547
     * @param Boolean $active active
548
     * @param String  $field  field
549
     * @return void
550
     */
551 4
    public function setFilterByAuthUser($active, $field)
552
    {
553 4
        $this->filterByAuthUser = is_bool($active) ? $active : false;
554 4
        $this->filterByAuthField = $field;
555 4
    }
556
}
557