Completed
Push — feature/cleanup-search ( 3778bb...6e5c33 )
by
unknown
10:32 queued 32s
created

DocumentModel   F

Complexity

Total Complexity 59

Size/Duplication

Total Lines 531
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 19

Test Coverage

Coverage 31.55%

Importance

Changes 19
Bugs 4 Features 0
Metric Value
wmc 59
c 19
b 4
f 0
lcom 2
cbo 19
dl 0
loc 531
ccs 65
cts 206
cp 0.3155
rs 2.4812

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