Completed
Push — develop ( 5fc409...89f0c5 )
by Lucas
11s
created

DocumentModel   F

Complexity

Total Complexity 65

Size/Duplication

Total Lines 554
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 19

Test Coverage

Coverage 25.24%

Importance

Changes 22
Bugs 6 Features 1
Metric Value
wmc 65
c 22
b 6
f 1
lcom 2
cbo 19
dl 0
loc 554
ccs 52
cts 206
cp 0.2524
rs 2.4812

22 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 1
A getRepository() 0 4 1
A setRepository() 0 7 1
D findAll() 0 82 13
C buildSearchQuery() 0 51 13
A hasCustomSearchIndex() 0 11 3
B buildSearchTextQuery() 0 13 5
A getMongoDBVersion() 0 11 2
A insertRecord() 0 11 3
A find() 0 4 1
A updateRecord() 0 18 3
A deleteRecord() 0 18 3
A flush() 0 4 1
A deleteById() 0 9 1
A recordExists() 0 4 1
A selectSingleFields() 0 14 1
A getEntityClass() 0 8 2
A getConnectionName() 0 6 1
A doRqlQuery() 0 6 1
A checkIfOriginRecord() 0 15 4
A getDefaultLimit() 0 8 2
A setFilterByAuthUser() 0 5 2

How to fix   Complexity   

Complex Class

Complex classes like DocumentModel often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DocumentModel, and based on these observations, apply Extract Interface, too.

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 string[]
57
     */
58
    protected $textIndexes = array();
59
    /**
60
     * @var DocumentRepository
61
     */
62
    private $repository;
63
    /**
64
     * @var Visitor
65
     */
66
    private $visitor;
67
    /**
68
     * @var array
69
     */
70
    protected $notModifiableOriginRecords;
71
    /**
72
     * @var  integer
73
     */
74
    private $paginationDefaultLimit;
75
76
    /**
77
     * @var boolean
78
     */
79
    protected $filterByAuthUser;
80
81
    /**
82
     * @var string
83
     */
84
    protected $filterByAuthField;
85
86
    /**
87
     * @var DocumentManager
88
     */
89
    protected $manager;
90
91
    /**
92 4
     * @param Visitor $visitor                    rql query visitor
93
     * @param array   $notModifiableOriginRecords strings with not modifiable recordOrigin values
94
     * @param integer $paginationDefaultLimit     amount of data records to be returned when in pagination context
95
     */
96
    public function __construct(
97 4
        Visitor $visitor,
98 4
        $notModifiableOriginRecords,
99 4
        $paginationDefaultLimit
100 4
    ) {
101 4
        parent::__construct();
102
        $this->visitor = $visitor;
103
        $this->notModifiableOriginRecords = $notModifiableOriginRecords;
104
        $this->paginationDefaultLimit = (int) $paginationDefaultLimit;
105
    }
106
107
    /**
108 2
     * get repository instance
109
     *
110 2
     * @return DocumentRepository
111
     */
112
    public function getRepository()
113
    {
114
        return $this->repository;
115
    }
116
117
    /**
118
     * create new app model
119
     *
120 4
     * @param DocumentRepository $repository Repository of countries
121
     *
122 4
     * @return \Graviton\RestBundle\Model\DocumentModel
123 4
     */
124
    public function setRepository(DocumentRepository $repository)
125 4
    {
126
        $this->repository = $repository;
127
        $this->manager = $repository->getDocumentManager();
128
129
        return $this;
130
    }
131
132
    /**
133
     * {@inheritDoc}
134
     *
135
     * @param Request        $request The request object
136
     * @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...
137
     * @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...
138
     *
139
     * @return array
140
     */
141
    public function findAll(Request $request, SecurityUser $user = null, SchemaDocument $schema = null)
142
    {
143
        $pageNumber = $request->query->get('page', 1);
144
        $numberPerPage = (int) $request->query->get('perPage', $this->getDefaultLimit());
145
        $startAt = ($pageNumber - 1) * $numberPerPage;
146
147
        /** @var XiagQuery $xiagQuery */
148
        $xiagQuery = $request->attributes->get('rqlQuery');
149
150
        /** @var MongoBuilder $queryBuilder */
151
        $queryBuilder = $this->repository
152
            ->createQueryBuilder();
153
154
        // Setting RQL Query
155
        if ($xiagQuery) {
156
            // Clean up Search rql param and set it as Doctrine query
157
            if ($xiagQuery->getQuery() && $this->hasCustomSearchIndex() && (float) $this->getMongoDBVersion() >= 2.6) {
158
                $searchQueries = $this->buildSearchQuery($xiagQuery, $queryBuilder);
159
                $xiagQuery = $searchQueries['xiagQuery'];
160
                $queryBuilder = $searchQueries['queryBuilder'];
161
            }
162
            $queryBuilder = $this->doRqlQuery(
163
                $queryBuilder,
164
                $xiagQuery
165
            );
166
        } else {
167
            // @todo [lapistano]: seems the offset is missing for this query.
168
            /** @var \Doctrine\ODM\MongoDB\Query\Builder $qb */
169
            $queryBuilder->find($this->repository->getDocumentName());
170
        }
171
172
173
        /** @var LimitNode $rqlLimit */
174
        $rqlLimit = $xiagQuery instanceof XiagQuery ? $xiagQuery->getLimit() : false;
175
176
        // define offset and limit
177
        if (!$rqlLimit || !$rqlLimit->getOffset()) {
178
            $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...
179
        } else {
180
            $startAt = (int) $rqlLimit->getOffset();
181
            $queryBuilder->skip($startAt);
182
        }
183
184
        if (!$rqlLimit || is_null($rqlLimit->getLimit())) {
185
            $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...
186
        } else {
187
            $numberPerPage = (int) $rqlLimit->getLimit();
188
            $queryBuilder->limit($numberPerPage);
189
        }
190
191
        // Limit can not be negative nor null.
192
        if ($numberPerPage < 1) {
193
            throw new RqlSyntaxErrorException('negative or null limit in rql');
194
        }
195
196
        /**
197
         * add a default sort on id if none was specified earlier
198
         *
199
         * not specifying something to sort on leads to very weird cases when fetching references.
200
         */
201
        if (!array_key_exists('sort', $queryBuilder->getQuery()->getQuery())) {
202
            $queryBuilder->sort('_id');
203
        }
204
205
        // run query
206
        $query = $queryBuilder->getQuery();
207
        $records = array_values($query->execute()->toArray());
208
209
        $totalCount = $query->count();
210
        $numPages = (int) ceil($totalCount / $numberPerPage);
211
        $page = (int) ceil($startAt / $numberPerPage) + 1;
212
        if ($numPages > 1) {
213
            $request->attributes->set('paging', true);
214
            $request->attributes->set('page', $page);
215
            $request->attributes->set('numPages', $numPages);
216
            $request->attributes->set('startAt', $startAt);
217
            $request->attributes->set('perPage', $numberPerPage);
218
            $request->attributes->set('totalCount', $totalCount);
219
        }
220
221
        return $records;
222
    }
223
224
    /**
225
     * @param XiagQuery    $xiagQuery    Xiag Builder
226
     * @param MongoBuilder $queryBuilder Mongo Doctrine query builder
227
     * @return array
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array<string,Query|Builder>.

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...
228
     */
229
    private function buildSearchQuery(XiagQuery $xiagQuery, MongoBuilder $queryBuilder)
230
    {
231
        $innerQuery = $xiagQuery->getQuery();
232
        $hasSearch = false;
233
        $nodes = [];
234
        if ($innerQuery instanceof AbstractLogicOperatorNode) {
235
            foreach ($innerQuery->getQueries() as $key => $innerRql) {
236
                if ($innerRql instanceof SearchNode) {
237
                    if (!$hasSearch) {
238
                        $queryBuilder = $this->buildSearchTextQuery($queryBuilder, $innerRql);
239
                        $hasSearch = true;
240
                    }
241
                } else {
242
                    $nodes[] = $innerRql;
243
                }
244
            }
245
        } elseif ($innerQuery instanceof SearchNode) {
246
            $queryBuilder = $this->repository->createQueryBuilder();
247
            $queryBuilder = $this->buildSearchTextQuery($queryBuilder, $innerQuery);
248
            $hasSearch = true;
249
        }
250
        // Remove the Search from RQL xiag
251
        if ($hasSearch && $nodes) {
252
            $newXiagQuery = new XiagQuery();
253
            if ($xiagQuery->getLimit()) {
254
                $newXiagQuery->setLimit($xiagQuery->getLimit());
255
            }
256
            if ($xiagQuery->getSelect()) {
257
                $newXiagQuery->setSelect($xiagQuery->getSelect());
258
            }
259
            if ($xiagQuery->getSort()) {
260
                $newXiagQuery->setSort($xiagQuery->getSort());
261
            }
262
            $binderClass = get_class($innerQuery);
263
            /** @var AbstractLogicOperatorNode $newBinder */
264
            $newBinder = new $binderClass();
265
            foreach ($nodes as $node) {
266
                $newBinder->addQuery($node);
267
            }
268
            $newXiagQuery->setQuery($newBinder);
269
            // Reset original query, so that there is no Search param
270
            $xiagQuery = $newXiagQuery;
271
        }
272
        if ($hasSearch) {
273
            $queryBuilder->sortMeta('score', 'textScore');
274
        }
275
        return [
276
            'xiagQuery'     => $xiagQuery,
277
            'queryBuilder'  => $queryBuilder
278
        ];
279
    }
280
281
    /**
282
     * Check if collection has search indexes in DB
283
     *
284
     * @param string $prefix the prefix for custom text search indexes
285
     * @return bool
286
     * @throws \Doctrine\ODM\MongoDB\MongoDBException
287
     */
288
    private function hasCustomSearchIndex($prefix = 'search')
289
    {
290
        $collection = $this->repository->getDocumentManager()->getDocumentCollection($this->repository->getClassName());
291
        $indexesInfo = $collection->getIndexInfo();
292
        foreach ($indexesInfo as $indexInfo) {
293
            if ($indexInfo['name']==$prefix.$collection->getName().'Index') {
294
                return true;
295
            }
296
        }
297
        return false;
298
    }
299
300
    /**
301
     * Build Search text index
302
     *
303
     * @param MongoBuilder $queryBuilder Doctrine mongo query builder object
304
     * @param SearchNode   $searchNode   Graviton Search node
305
     * @return MongoBuilder
306
     */
307
    private function buildSearchTextQuery(MongoBuilder $queryBuilder, SearchNode $searchNode)
308
    {
309
        $searchArr = [];
310
        foreach ($searchNode->getSearchTerms() as $string) {
311
            if (!empty(trim($string))) {
312
                $searchArr[] = (strpos($string, '.') !== false) ? "\"{$string}\"" : $string;
313
            }
314
        }
315
        if (!empty($searchArr)) {
316
            $queryBuilder->addAnd($queryBuilder->expr()->text(implode(' ', $searchArr)));
317 1
        }
318
        return $queryBuilder;
319
    }
320
321
    /**
322
     * @return string the version of the MongoDB as a string
323
     */
324
    private function getMongoDBVersion()
325
    {
326 1
        $buildInfo = $this->repository->getDocumentManager()->getDocumentDatabase(
327
            $this->repository->getClassName()
328
        )->command(['buildinfo'=>1]);
329
        if (isset($buildInfo['version'])) {
330
            return $buildInfo['version'];
331
        } else {
332
            return "unkown";
333
        }
334 4
    }
335
336 4
    /**
337
     * @param object $entity       entity to insert
338
     * @param bool   $returnEntity true to return entity
339
     * @param bool   $doFlush      if we should flush or not after insert
340
     *
341
     * @return Object|null
342
     */
343
    public function insertRecord($entity, $returnEntity = true, $doFlush = true)
344
    {
345
        $this->manager->persist($entity);
346
347
        if ($doFlush) {
348 2
            $this->manager->flush($entity);
349
        }
350 2
        if ($returnEntity) {
351 2
            return $this->find($entity->getId());
352
        }
353 2
    }
354 2
355 1
    /**
356
     * @param string $documentId id of entity to find
357 2
     *
358
     * @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...
359 2
     */
360 2
    public function find($documentId)
361
    {
362 2
        return $this->repository->find($documentId);
363
    }
364
365 2
    /**
366
     * {@inheritDoc}
367
     *
368
     * @param string $documentId   id of entity to update
369
     * @param Object $entity       new entity
370
     * @param bool   $returnEntity true to return entity
371
     *
372
     * @return Object|null
373
     */
374
    public function updateRecord($documentId, $entity, $returnEntity = true)
375
    {
376
        if (!is_null($documentId)) {
377
            $this->deleteById($documentId);
378
            // detach so odm knows it's gone
379
            $this->manager->detach($entity);
380
            $this->manager->clear();
381
        }
382
383
        $entity = $this->manager->merge($entity);
384
385
        $this->manager->persist($entity);
386
        $this->manager->flush($entity);
387
388
        if ($returnEntity) {
389
            return $entity;
390
        }
391
    }
392
393
    /**
394
     * {@inheritDoc}
395
     *
396
     * @param string|object $id id of entity to delete or entity instance
397
     *
398
     * @return null|Object
399
     */
400
    public function deleteRecord($id)
401
    {
402
        if (is_object($id)) {
403
            $entity = $id;
404
        } else {
405
            $entity = $this->find($id);
406
        }
407
408
        $return = $entity;
409
        if ($entity) {
410
            $this->checkIfOriginRecord($entity);
411
            $this->manager->remove($entity);
412 2
            $this->manager->flush();
413
            $return = null;
414 2
        }
415
416 2
        return $return;
417 2
    }
418 2
419 2
    /**
420 2
     * Triggers a flush on the DocumentManager
421
     *
422
     * @param null $document optional document
423
     *
424
     * @return void
425
     */
426
    public function flush($document = null)
427
    {
428
        $this->manager->flush($document);
429 4
    }
430
431 4
    /**
432
     * A low level delete without any checks
433
     *
434
     * @param mixed $id record id
435
     *
436
     * @return void
437
     */
438
    private function deleteById($id)
439
    {
440
        $builder = $this->repository->createQueryBuilder();
441
        $builder
442
            ->remove()
443
            ->field('id')->equals($id)
444
            ->getQuery()
445
            ->execute();
446
    }
447 4
448
    /**
449 4
     * Checks in a performant way if a certain record id exists in the database
450 4
     *
451
     * @param mixed $id record id
452
     *
453 4
     * @return bool true if it exists, false otherwise
454 4
     */
455 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...
456 4
    {
457 4
        return is_array($this->selectSingleFields($id, ['id'], false));
458
    }
459 4
460
    /**
461
     * Returns a set of fields from an existing resource in a performant manner.
462
     * If you need to check certain fields on an object (and don't need everything), this
463
     * is a better way to get what you need.
464
     * If the record is not present, you will receive null. If you don't need an hydrated
465
     * instance, make sure to pass false there.
466
     *
467 4
     * @param mixed $id      record id
468
     * @param array $fields  list of fields you need.
469 4
     * @param bool  $hydrate whether to hydrate object or not
470 4
     *
471
     * @return array|null|object
472
     */
473
    public function selectSingleFields($id, array $fields, $hydrate = true)
474
    {
475
        $builder = $this->repository->createQueryBuilder();
476
        $idField = $this->repository->getClassMetadata()->getIdentifier()[0];
477
478
        $record = $builder
479
            ->field($idField)->equals($id)
480
            ->select($fields)
481
            ->hydrate($hydrate)
482
            ->getQuery()
483
            ->getSingleResult();
484
485
        return $record;
486
    }
487
488
    /**
489
     * get classname of entity
490
     *
491
     * @return string|null
492
     */
493
    public function getEntityClass()
494
    {
495
        if ($this->repository instanceof DocumentRepository) {
496
            return $this->repository->getDocumentName();
497
        }
498
499
        return null;
500
    }
501
502
    /**
503
     * {@inheritDoc}
504
     *
505
     * Currently this is being used to build the route id used for redirecting
506
     * to newly made documents. It might benefit from having a different name
507
     * for those purposes.
508
     *
509
     * We might use a convention based mapping here:
510
     * Graviton\CoreBundle\Document\App -> mongodb://graviton_core
511
     * Graviton\CoreBundle\Entity\Table -> mysql://graviton_core
512
     *
513
     * @todo implement this in a more convention based manner
514
     *
515
     * @return string
516
     */
517
    public function getConnectionName()
518
    {
519
        $bundle = strtolower(substr(explode('\\', get_class($this))[1], 0, -6));
520
521
        return 'graviton.' . $bundle;
522
    }
523
524
    /**
525
     * Does the actual query using the RQL Bundle.
526
     *
527
     * @param Builder $queryBuilder Doctrine ODM QueryBuilder
528
     * @param Query   $query        query from parser
529
     *
530
     * @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...
531
     */
532
    protected function doRqlQuery($queryBuilder, Query $query)
533
    {
534
        $this->visitor->setBuilder($queryBuilder);
535
536
        return $this->visitor->visit($query);
537
    }
538
539
    /**
540
     * Checks the recordOrigin attribute of a record and will throw an exception if value is not allowed
541
     *
542
     * @param Object $record record
543
     *
544
     * @return void
545
     */
546
    protected function checkIfOriginRecord($record)
547
    {
548
        if ($record instanceof RecordOriginInterface
549
            && !$record->isRecordOriginModifiable()
550
        ) {
551
            $values = $this->notModifiableOriginRecords;
552
            $originValue = strtolower(trim($record->getRecordOrigin()));
553
554
            if (in_array($originValue, $values)) {
555 4
                $msg = sprintf("Must not be one of the following keywords: %s", implode(', ', $values));
556
557 4
                throw new RecordOriginModifiedException($msg);
558 4
            }
559 4
        }
560
    }
561
562
    /**
563
     * Determines the configured amount fo data records to be returned in pagination context.
564
     *
565
     * @return int
566
     */
567
    private function getDefaultLimit()
568
    {
569
        if (0 < $this->paginationDefaultLimit) {
570
            return $this->paginationDefaultLimit;
571
        }
572
573
        return 10;
574
    }
575
576
    /**
577
     * @param Boolean $active active
578
     * @param String  $field  field
579
     * @return void
580
     */
581
    public function setFilterByAuthUser($active, $field)
582
    {
583
        $this->filterByAuthUser = is_bool($active) ? $active : false;
584
        $this->filterByAuthField = $field;
585
    }
586
}
587