Completed
Push — feature/EVO-5751-text-search-j... ( 10a790...24ba67 )
by
unknown
18:25 queued 09:44
created

DocumentModel::findAll()   D

Complexity

Conditions 16
Paths 84

Size

Total Lines 92
Code Lines 52

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 272

Importance

Changes 8
Bugs 0 Features 0
Metric Value
c 8
b 0
f 0
dl 0
loc 92
ccs 0
cts 59
cp 0
rs 4.8736
cc 16
eloc 52
nc 84
nop 3
crap 272

How to fix   Long Method    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\RestBundle\Service\RqlTranslator;
11
use Graviton\Rql\Node\SearchNode;
12
use Graviton\SchemaBundle\Model\SchemaModel;
13
use Graviton\SecurityBundle\Entities\SecurityUser;
14
use Symfony\Bridge\Monolog\Logger;
15
use Symfony\Component\HttpFoundation\Request;
16
use Doctrine\ODM\MongoDB\Query\Builder;
17
use Graviton\Rql\Visitor\MongoOdm as Visitor;
18
use Xiag\Rql\Parser\AbstractNode;
19
use Xiag\Rql\Parser\Node\LimitNode;
20
use Xiag\Rql\Parser\Node\Query\AbstractLogicOperatorNode;
21
use Xiag\Rql\Parser\Query;
22
use Graviton\ExceptionBundle\Exception\RecordOriginModifiedException;
23
use Xiag\Rql\Parser\Exception\SyntaxErrorException as RqlSyntaxErrorException;
24
use Graviton\SchemaBundle\Document\Schema as SchemaDocument;
25
use Xiag\Rql\Parser\Query as XiagQuery;
26
27
/**
28
 * Use doctrine odm as backend
29
 *
30
 * @author  List of contributors <https://github.com/libgraviton/graviton/graphs/contributors>
31
 * @license http://opensource.org/licenses/gpl-license.php GNU Public License
32
 * @link    http://swisscom.ch
33
 */
34
class DocumentModel extends SchemaModel implements ModelInterface
35
{
36
    /**
37
     * @var string
38
     */
39
    protected $description;
40
    /**
41
     * @var string[]
42
     */
43
    protected $fieldTitles;
44
    /**
45
     * @var string[]
46
     */
47
    protected $fieldDescriptions;
48
    /**
49
     * @var string[]
50
     */
51
    protected $requiredFields = array();
52
    /**
53
     * @var string[]
54
     */
55
    protected $searchableFields = array();
56
    /**
57
     * @var DocumentRepository
58
     */
59
    private $repository;
60
    /**
61
     * @var Visitor
62
     */
63
    private $visitor;
64
    /**
65
     * @var array
66
     */
67
    protected $notModifiableOriginRecords;
68
    /**
69
     * @var  integer
70
     */
71
    private $paginationDefaultLimit;
72
73
    /**
74
     * @var boolean
75
     */
76
    protected $filterByAuthUser;
77
78
    /**
79
     * @var string
80
     */
81
    protected $filterByAuthField;
82
83
    /**
84
     * @var RqlTranslator
85
     */
86
    protected $translator;
87
88
    /**
89
     * @var DocumentManager
90
     */
91
    protected $manager;
92
93
    /**
94
     * @param Visitor       $visitor                    rql query visitor
95
     * @param RqlTranslator $translator                 Translator for query modification
96
     * @param array         $notModifiableOriginRecords strings with not modifiable recordOrigin values
97
     * @param integer       $paginationDefaultLimit     amount of data records to be returned when in pagination context
98
     */
99 4
    public function __construct(
100
        Visitor $visitor,
101
        RqlTranslator $translator,
102
        $notModifiableOriginRecords,
103
        $paginationDefaultLimit
104
    ) {
105 4
        parent::__construct();
106 4
        $this->visitor = $visitor;
107 4
        $this->translator = $translator;
108 4
        $this->notModifiableOriginRecords = $notModifiableOriginRecords;
109 4
        $this->paginationDefaultLimit = (int) $paginationDefaultLimit;
110 4
    }
111
112
    /**
113
     * get repository instance
114
     *
115
     * @return DocumentRepository
116
     */
117 2
    public function getRepository()
118
    {
119 2
        return $this->repository;
120
    }
121
122
    /**
123
     * create new app model
124
     *
125
     * @param DocumentRepository $repository Repository of countries
126
     *
127
     * @return \Graviton\RestBundle\Model\DocumentModel
128
     */
129 4
    public function setRepository(DocumentRepository $repository)
130
    {
131 4
        $this->repository = $repository;
132 4
        $this->manager = $repository->getDocumentManager();
133
134 4
        return $this;
135
    }
136
137
    /**
138
     * {@inheritDoc}
139
     *
140
     * @param Request        $request The request object
141
     * @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...
142
     * @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...
143
     *
144
     * @return array
145
     */
146
    public function findAll(Request $request, SecurityUser $user = null, SchemaDocument $schema = null)
147
    {
148
        $pageNumber = $request->query->get('page', 1);
149
        $numberPerPage = (int) $request->query->get('perPage', $this->getDefaultLimit());
150
        $startAt = ($pageNumber - 1) * $numberPerPage;
151
        // Only 1 search text node allowed.
152
        $hasSearch = false;
153
        /** @var XiagQuery $queryParams */
154
        $xiagQuery = $request->attributes->get('rqlQuery');
155
156
        /** @var \Doctrine\ODM\MongoDB\Query\Builder $queryBuilder */
157
        $queryBuilder = $this->repository
158
            ->createQueryBuilder();
159
160
        // *** do we have an RQL expression, do we need to filter data?
161
        if ($request->attributes->get('hasRql', false)) {
162
            $innerQuery = $request->attributes->get('rqlQuery')->getQuery();
163
            $queryBuilder = $this->doRqlQuery(
164
                $queryBuilder,
165
                $this->translator->translateSearchQuery($xiagQuery, [])
166
            );
167
            if ($innerQuery instanceof AbstractLogicOperatorNode && $this->hasCustomSearchIndex()) {
168
                foreach ($innerQuery->getQueries() as $innerRql) {
169
                    if (!$hasSearch && $innerRql instanceof SearchNode) {
170
                        $searchString = implode(' ', $innerRql->getSearchTerms());
171
                        $queryBuilder->addAnd(
172
                            $queryBuilder->expr()->text($searchString)
0 ignored issues
show
Bug introduced by
The method expr 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...
173
                        );
174
                        $hasSearch = true;
175
                    }
176
                }
177
            }
178
        } else {
179
            // @todo [lapistano]: seems the offset is missing for this query.
180
            /** @var \Doctrine\ODM\MongoDB\Query\Builder $qb */
181
            $queryBuilder->find($this->repository->getDocumentName());
182
        }
183
184
        /** @var LimitNode $rqlLimit */
185
        $rqlLimit = $xiagQuery->getLimit();
186
        
187
        // define offset and limit
188
        if (!$rqlLimit || !$rqlLimit->getOffset()) {
189
            $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...
190
        } else {
191
            $startAt = (int) $queryParams->getLimit()->getOffset();
192
            $queryBuilder->skip($startAt);
193
        }
194
195
        if (!$rqlLimit || is_null($rqlLimit->getLimit())) {
196
            $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...
197
        } else {
198
            $numberPerPage = (int) $queryParams->getLimit()->getLimit();
199
            $queryBuilder->limit($numberPerPage);
200
        }
201
202
        // Limit can not be negative nor null.
203
        if ($numberPerPage < 1) {
204
            throw new RqlSyntaxErrorException('negative or null limit in rql');
205
        }
206
207
        /**
208
         * add a default sort on id if none was specified earlier
209
         *
210
         * not specifying something to sort on leads to very weird cases when fetching references
211
         * If search node, sort by Score
212
         * TODO Review this sorting, not 100% sure
213
         */
214
        if ($hasSearch && !array_key_exists('sort', $queryBuilder->getQuery()->getQuery())) {
215
            $queryBuilder->sortMeta('score', 'textScore');
0 ignored issues
show
Bug introduced by
The method sortMeta 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...
216
        } elseif (!array_key_exists('sort', $queryBuilder->getQuery()->getQuery())) {
217
            $queryBuilder->sort('_id');
218
        }
219
220
        // run query
221
        $query = $queryBuilder->getQuery();
222
        $records = array_values($query->execute()->toArray());
223
224
        $totalCount = $query->count();
225
        $numPages = (int) ceil($totalCount / $numberPerPage);
226
        $page = (int) ceil($startAt / $numberPerPage) + 1;
227
        if ($numPages > 1) {
228
            $request->attributes->set('paging', true);
229
            $request->attributes->set('page', $page);
230
            $request->attributes->set('numPages', $numPages);
231
            $request->attributes->set('startAt', $startAt);
232
            $request->attributes->set('perPage', $numberPerPage);
233
            $request->attributes->set('totalCount', $totalCount);
234
        }
235
236
        return $records;
237
    }
238
239
    /**
240
     * @param string $prefix the prefix for custom text search indexes
241
     * @return bool
242
     * @throws \Doctrine\ODM\MongoDB\MongoDBException
243
     */
244
    private function hasCustomSearchIndex($prefix = 'search')
245
    {
246
        $collection = $this->repository->getDocumentManager()->getDocumentCollection($this->repository->getClassName());
247
        $indexesInfo = $collection->getIndexInfo();
248
        foreach ($indexesInfo as $indexInfo) {
249
            if ($indexInfo['name']==$prefix.$collection->getName().'Index') {
250
                return true;
251
            }
252
        }
253
        return false;
254
    }
255
256
    /**
257
     * @return string the version of the MongoDB as a string
258
     */
259
    private function getMongoDBVersion()
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
260
    {
261
        $buildInfo = $this->repository->getDocumentManager()->getDocumentDatabase(
262
            $this->repository->getClassName()
263
        )->command(['buildinfo'=>1]);
264
        if (isset($buildInfo['version'])) {
265
            return $buildInfo['version'];
266
        } else {
267
            return "unkown";
268
        }
269
    }
270
271
    /**
272
     * @param object $entity       entity to insert
273
     * @param bool   $returnEntity true to return entity
274
     * @param bool   $doFlush      if we should flush or not after insert
275
     *
276
     * @return Object|null
277
     */
278
    public function insertRecord($entity, $returnEntity = true, $doFlush = true)
279
    {
280
        $this->checkIfOriginRecord($entity);
281
        $this->manager->persist($entity);
282
283
        if ($doFlush) {
284
            $this->manager->flush($entity);
285
        }
286
        if ($returnEntity) {
287
            return $this->find($entity->getId());
288
        }
289
    }
290
291
    /**
292
     * @param string $documentId id of entity to find
293
     *
294
     * @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...
295
     */
296 4
    public function find($documentId)
297
    {
298 4
        return $this->repository->find($documentId);
299
    }
300
301
    /**
302
     * {@inheritDoc}
303
     *
304
     * @param string $documentId   id of entity to update
305
     * @param Object $entity       new entity
306
     * @param bool   $returnEntity true to return entity
307
     *
308
     * @return Object|null
309
     */
310 3
    public function updateRecord($documentId, $entity, $returnEntity = true)
311
    {
312
        // In both cases the document attribute named originRecord must not be 'core'
313 2
        $this->checkIfOriginRecord($entity);
314 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...
315
316 2
        if (!is_null($documentId)) {
317 2
            $this->deleteById($documentId);
318
            // detach so odm knows it's gone
319 2
            $this->manager->detach($entity);
320 2
            $this->manager->clear();
321 1
        }
322
323 2
        $entity = $this->manager->merge($entity);
324
325 2
        $this->manager->persist($entity);
326 3
        $this->manager->flush($entity);
327
328 2
        if ($returnEntity) {
329
            return $entity;
330
        }
331 2
    }
332
333
    /**
334
     * {@inheritDoc}
335
     *
336
     * @param string|object $id id of entity to delete or entity instance
337
     *
338
     * @return null|Object
339
     */
340
    public function deleteRecord($id)
341
    {
342
        if (is_object($id)) {
343
            $entity = $id;
344
        } else {
345
            $entity = $this->find($id);
346
        }
347
348
        $return = $entity;
349
        if ($entity) {
350
            $this->checkIfOriginRecord($entity);
351
            $this->manager->remove($entity);
352
            $this->manager->flush();
353
            $return = null;
354
        }
355
356
        return $return;
357
    }
358
359
    /**
360
     * Triggers a flush on the DocumentManager
361
     *
362
     * @param null $document optional document
363
     *
364
     * @return void
365
     */
366
    public function flush($document = null)
367
    {
368
        $this->manager->flush($document);
369
    }
370
371
    /**
372
     * A low level delete without any checks
373
     *
374
     * @param mixed $id record id
375
     *
376
     * @return void
377
     */
378 2
    private function deleteById($id)
379
    {
380 2
        $builder = $this->repository->createQueryBuilder();
381
        $builder
382 2
            ->remove()
383 2
            ->field('id')->equals($id)
384 2
            ->getQuery()
385 2
            ->execute();
386 2
    }
387
388
    /**
389
     * Checks in a performant way if a certain record id exists in the database
390
     *
391
     * @param mixed $id record id
392
     *
393
     * @return bool true if it exists, false otherwise
394
     */
395 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...
396
    {
397 4
        return is_array($this->selectSingleFields($id, ['id'], false));
398
    }
399
400
    /**
401
     * Returns a set of fields from an existing resource in a performant manner.
402
     * If you need to check certain fields on an object (and don't need everything), this
403
     * is a better way to get what you need.
404
     * If the record is not present, you will receive null. If you don't need an hydrated
405
     * instance, make sure to pass false there.
406
     *
407
     * @param mixed $id      record id
408
     * @param array $fields  list of fields you need.
409
     * @param bool  $hydrate whether to hydrate object or not
410
     *
411
     * @return array|null|object
412
     */
413 4
    public function selectSingleFields($id, array $fields, $hydrate = true)
414
    {
415 4
        $builder = $this->repository->createQueryBuilder();
416 4
        $idField = $this->repository->getClassMetadata()->getIdentifier()[0];
417
418
        $record = $builder
419 4
            ->field($idField)->equals($id)
420 4
            ->select($fields)
421 4
            ->hydrate($hydrate)
422 4
            ->getQuery()
423 4
            ->getSingleResult();
424
425 4
        return $record;
426
    }
427
428
    /**
429
     * get classname of entity
430
     *
431
     * @return string|null
432
     */
433 4
    public function getEntityClass()
434
    {
435 4
        if ($this->repository instanceof DocumentRepository) {
436 4
            return $this->repository->getDocumentName();
437
        }
438
439
        return null;
440
    }
441
442
    /**
443
     * {@inheritDoc}
444
     *
445
     * Currently this is being used to build the route id used for redirecting
446
     * to newly made documents. It might benefit from having a different name
447
     * for those purposes.
448
     *
449
     * We might use a convention based mapping here:
450
     * Graviton\CoreBundle\Document\App -> mongodb://graviton_core
451
     * Graviton\CoreBundle\Entity\Table -> mysql://graviton_core
452
     *
453
     * @todo implement this in a more convention based manner
454
     *
455
     * @return string
456
     */
457
    public function getConnectionName()
458
    {
459
        $bundle = strtolower(substr(explode('\\', get_class($this))[1], 0, -6));
460
461
        return 'graviton.' . $bundle;
462
    }
463
464
    /**
465
     * Does the actual query using the RQL Bundle.
466
     *
467
     * @param Builder $queryBuilder Doctrine ODM QueryBuilder
468
     * @param Query   $query        query from parser
469
     *
470
     * @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...
471
     */
472
    protected function doRqlQuery($queryBuilder, Query $query)
473
    {
474
        $this->visitor->setBuilder($queryBuilder);
475
476
        return $this->visitor->visit($query);
477
    }
478
479
    /**
480
     * Checks the recordOrigin attribute of a record and will throw an exception if value is not allowed
481
     *
482
     * @param Object $record record
483
     *
484
     * @return void
485
     */
486 14
    protected function checkIfOriginRecord($record)
487
    {
488 7
        if ($record instanceof RecordOriginInterface
489 14
            && !$record->isRecordOriginModifiable()
490 7
        ) {
491 6
            $values = $this->notModifiableOriginRecords;
492 6
            $originValue = strtolower(trim($record->getRecordOrigin()));
493
494 6
            if (in_array($originValue, $values)) {
495 2
                $msg = sprintf("Must not be one of the following keywords: %s", implode(', ', $values));
496
497 2
                throw new RecordOriginModifiedException($msg);
498
            }
499 2
        }
500 12
    }
501
502
    /**
503
     * Determines the configured amount fo data records to be returned in pagination context.
504
     *
505
     * @return int
506
     */
507
    private function getDefaultLimit()
508
    {
509
        if (0 < $this->paginationDefaultLimit) {
510
            return $this->paginationDefaultLimit;
511
        }
512
513
        return 10;
514
    }
515
516
    /**
517
     * @param Boolean $active active
518
     * @param String  $field  field
519
     * @return void
520
     */
521 4
    public function setFilterByAuthUser($active, $field)
522
    {
523 4
        $this->filterByAuthUser = is_bool($active) ? $active : false;
524 4
        $this->filterByAuthField = $field;
525 4
    }
526
}
527