Completed
Push — feature/EVO-5751-text-search-j... ( 4dd2c3 )
by
unknown
66:00 queued 03:58
created

DocumentModel::findAll()   D

Complexity

Conditions 14
Paths 160

Size

Total Lines 87
Code Lines 50

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
c 5
b 0
f 0
dl 0
loc 87
rs 4.6283
cc 14
eloc 50
nc 160
nop 3

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\Query;
19
use Graviton\ExceptionBundle\Exception\RecordOriginModifiedException;
20
use Xiag\Rql\Parser\Exception\SyntaxErrorException as RqlSyntaxErrorException;
21
use Graviton\SchemaBundle\Document\Schema as SchemaDocument;
22
23
/**
24
 * Use doctrine odm as backend
25
 *
26
 * @author  List of contributors <https://github.com/libgraviton/graviton/graphs/contributors>
27
 * @license http://opensource.org/licenses/gpl-license.php GNU Public License
28
 * @link    http://swisscom.ch
29
 */
30
class DocumentModel extends SchemaModel implements ModelInterface
31
{
32
    /**
33
     * @var string
34
     */
35
    protected $description;
36
    /**
37
     * @var string[]
38
     */
39
    protected $fieldTitles;
40
    /**
41
     * @var string[]
42
     */
43
    protected $fieldDescriptions;
44
    /**
45
     * @var string[]
46
     */
47
    protected $requiredFields = array();
48
    /**
49
     * @var string[]
50
     */
51
    protected $searchableFields = array();
52
    /**
53
     * @var DocumentRepository
54
     */
55
    private $repository;
56
    /**
57
     * @var Visitor
58
     */
59
    private $visitor;
60
    /**
61
     * @var array
62
     */
63
    protected $notModifiableOriginRecords;
64
    /**
65
     * @var  integer
66
     */
67
    private $paginationDefaultLimit;
68
69
    /**
70
     * @var boolean
71
     */
72
    protected $filterByAuthUser;
73
74
    /**
75
     * @var string
76
     */
77
    protected $filterByAuthField;
78
79
    /**
80
     * @var RqlTranslator
81
     */
82
    protected $translator;
83
84
    /**
85
     * @var DocumentManager
86
     */
87
    protected $manager;
88
89
    /**
90
     * @param Visitor       $visitor                    rql query visitor
91
     * @param RqlTranslator $translator                 Translator for query modification
92
     * @param array         $notModifiableOriginRecords strings with not modifiable recordOrigin values
93
     * @param integer       $paginationDefaultLimit     amount of data records to be returned when in pagination context
94
     */
95
    public function __construct(
96
        Visitor $visitor,
97
        RqlTranslator $translator,
98
        $notModifiableOriginRecords,
99
        $paginationDefaultLimit
100
    ) {
101
        parent::__construct();
102
        $this->visitor = $visitor;
103
        $this->translator = $translator;
104
        $this->notModifiableOriginRecords = $notModifiableOriginRecords;
105
        $this->paginationDefaultLimit = (int) $paginationDefaultLimit;
106
    }
107
108
    /**
109
     * get repository instance
110
     *
111
     * @return DocumentRepository
112
     */
113
    public function getRepository()
114
    {
115
        return $this->repository;
116
    }
117
118
    /**
119
     * create new app model
120
     *
121
     * @param DocumentRepository $repository Repository of countries
122
     *
123
     * @return \Graviton\RestBundle\Model\DocumentModel
124
     */
125
    public function setRepository(DocumentRepository $repository)
126
    {
127
        $this->repository = $repository;
128
        $this->manager = $repository->getDocumentManager();
129
130
        return $this;
131
    }
132
133
    /**
134
     * {@inheritDoc}
135
     *
136
     * @param Request        $request The request object
137
     * @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...
138
     * @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...
139
     *
140
     * @return array
141
     */
142
    public function findAll(Request $request, SecurityUser $user = null, SchemaDocument $schema = null)
143
    {
144
        $pageNumber = $request->query->get('page', 1);
145
        $numberPerPage = (int) $request->query->get('perPage', $this->getDefaultLimit());
146
        $startAt = ($pageNumber - 1) * $numberPerPage;
147
148
        /** @var \Doctrine\ODM\MongoDB\Query\Builder $queryBuilder */
149
        $queryBuilder = $this->repository
150
            ->createQueryBuilder();
151
152
        if ($this->filterByAuthUser && $user && $user->hasRole(SecurityUser::ROLE_USER)) {
153
            $queryBuilder->field($this->filterByAuthField)->equals($user->getUser()->getId());
154
        }
155
156
        // *** do we have an RQL expression, do we need to filter data?
157
        if ($request->attributes->get('hasRql', false)) {
158
            $innerQuery = $request->attributes->get('rqlQuery')->getQuery();
159
            // can we perform a search in an index instead of filtering?
160
            if ($innerQuery instanceof SearchNode &&
161
                (float) $this->getMongoDBVersion()>=2.6
162
                && $this->hasCustomSearchIndex()) {
163
                $queryBuilder->text(implode(" ", $innerQuery->getSearchTerms()));
164
            } else {
165
                // do rql filtering
166
                $searchableFields = $this->getSearchableFields();
167
                if (!is_null($schema)) {
168
                    $searchableFields = $schema->getSearchable();
169
                }
170
                $queryBuilder = $this->doRqlQuery(
171
                    $queryBuilder,
172
                    $this->translator->translateSearchQuery(
173
                        $request->attributes->get('rqlQuery'),
174
                        $searchableFields
0 ignored issues
show
Bug introduced by
It seems like $searchableFields defined by $schema->getSearchable() on line 168 can also be of type null; however, Graviton\RestBundle\Serv...:translateSearchQuery() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
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
        // define offset and limit
185
        if (!array_key_exists('skip', $queryBuilder->getQuery()->getQuery())) {
186
            $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...
187
        } else {
188
            $startAt = (int) $queryBuilder->getQuery()->getQuery()['skip'];
189
        }
190
191
        if (!array_key_exists('limit', $queryBuilder->getQuery()->getQuery())) {
192
            $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...
193
        } else {
194
            $numberPerPage = (int) $queryBuilder->getQuery()->getQuery()['limit'];
195
        }
196
197
        // Limit can not be negative nor null.
198
        if ($numberPerPage < 1) {
199
            throw new RqlSyntaxErrorException('negative or null limit in rql');
200
        }
201
202
        /**
203
         * add a default sort on id if none was specified earlier
204
         *
205
         * not specifying something to sort on leads to very weird cases when fetching references
206
         */
207
        if (!array_key_exists('sort', $queryBuilder->getQuery()->getQuery())) {
208
            $queryBuilder->sort('_id');
209
        }
210
211
        // run query
212
        $query = $queryBuilder->getQuery();
213
        $records = array_values($query->execute()->toArray());
214
215
        $totalCount = $query->count();
216
        $numPages = (int) ceil($totalCount / $numberPerPage);
217
        $page = (int) ceil($startAt / $numberPerPage) + 1;
218
        if ($numPages > 1) {
219
            $request->attributes->set('paging', true);
220
            $request->attributes->set('page', $page);
221
            $request->attributes->set('numPages', $numPages);
222
            $request->attributes->set('startAt', $startAt);
223
            $request->attributes->set('perPage', $numberPerPage);
224
            $request->attributes->set('totalCount', $totalCount);
225
        }
226
227
        return $records;
228
    }
229
230
    /**
231
     * @param string $prefix the prefix for custom text search indexes
232
     * @return bool
233
     * @throws \Doctrine\ODM\MongoDB\MongoDBException
234
     */
235
    private function hasCustomSearchIndex($prefix = 'search')
236
    {
237
        $collection = $this->repository->getDocumentManager()->getDocumentCollection($this->repository->getClassName());
238
        $indexesInfo = $collection->getIndexInfo();
239
        foreach ($indexesInfo as $indexInfo) {
240
            if ($indexInfo['name']==$prefix.$collection->getName()) {
241
                return true;
242
            }
243
        }
244
        return false;
245
    }
246
247
    /**
248
     * @return string the version of the MongoDB as a string
249
     */
250
    private function getMongoDBVersion()
251
    {
252
        $buildInfo = $this->repository->getDocumentManager()->getDocumentDatabase(
253
            $this->repository->getClassName()
254
        )->command(['buildinfo'=>1]);
255
        if (isset($buildInfo['version'])) {
256
            return $buildInfo['version'];
257
        } else {
258
            return "unkown";
259
        }
260
    }
261
262
    /**
263
     * @param object $entity       entity to insert
264
     * @param bool   $returnEntity true to return entity
265
     * @param bool   $doFlush      if we should flush or not after insert
266
     *
267
     * @return Object|null
268
     */
269
    public function insertRecord($entity, $returnEntity = true, $doFlush = true)
270
    {
271
        $this->checkIfOriginRecord($entity);
272
        $this->manager->persist($entity);
273
274
        if ($doFlush) {
275
            $this->manager->flush($entity);
276
        }
277
        if ($returnEntity) {
278
            return $this->find($entity->getId());
279
        }
280
    }
281
282
    /**
283
     * @param string $documentId id of entity to find
284
     *
285
     * @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...
286
     */
287
    public function find($documentId)
288
    {
289
        return $this->repository->find($documentId);
290
    }
291
292
    /**
293
     * {@inheritDoc}
294
     *
295
     * @param string $documentId   id of entity to update
296
     * @param Object $entity       new entity
297
     * @param bool   $returnEntity true to return entity
298
     *
299
     * @return Object|null
300
     */
301
    public function updateRecord($documentId, $entity, $returnEntity = true)
302
    {
303
        // In both cases the document attribute named originRecord must not be 'core'
304
        $this->checkIfOriginRecord($entity);
305
        $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...
306
307
        if (!is_null($documentId)) {
308
            $this->deleteById($documentId);
309
            // detach so odm knows it's gone
310
            $this->manager->detach($entity);
311
            $this->manager->clear();
312
        }
313
314
        $entity = $this->manager->merge($entity);
315
316
        $this->manager->persist($entity);
317
        $this->manager->flush($entity);
318
319
        if ($returnEntity) {
320
            return $entity;
321
        }
322
    }
323
324
    /**
325
     * {@inheritDoc}
326
     *
327
     * @param string|object $id id of entity to delete or entity instance
328
     *
329
     * @return null|Object
330
     */
331
    public function deleteRecord($id)
332
    {
333
        if (is_object($id)) {
334
            $entity = $id;
335
        } else {
336
            $entity = $this->find($id);
337
        }
338
339
        $return = $entity;
340
        if ($entity) {
341
            $this->checkIfOriginRecord($entity);
342
            $this->manager->remove($entity);
343
            $this->manager->flush();
344
            $return = null;
345
        }
346
347
        return $return;
348
    }
349
350
    /**
351
     * Triggers a flush on the DocumentManager
352
     *
353
     * @param null $document optional document
354
     *
355
     * @return void
356
     */
357
    public function flush($document = null)
358
    {
359
        $this->manager->flush($document);
360
    }
361
362
    /**
363
     * A low level delete without any checks
364
     *
365
     * @param mixed $id record id
366
     *
367
     * @return void
368
     */
369
    private function deleteById($id)
370
    {
371
        $builder = $this->repository->createQueryBuilder();
372
        $builder
373
            ->remove()
374
            ->field('id')->equals($id)
375
            ->getQuery()
376
            ->execute();
377
    }
378
379
    /**
380
     * Checks in a performant way if a certain record id exists in the database
381
     *
382
     * @param mixed $id record id
383
     *
384
     * @return bool true if it exists, false otherwise
385
     */
386
    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...
387
    {
388
        return is_array($this->selectSingleFields($id, ['id'], false));
389
    }
390
391
    /**
392
     * Returns a set of fields from an existing resource in a performant manner.
393
     * If you need to check certain fields on an object (and don't need everything), this
394
     * is a better way to get what you need.
395
     * If the record is not present, you will receive null. If you don't need an hydrated
396
     * instance, make sure to pass false there.
397
     *
398
     * @param mixed $id      record id
399
     * @param array $fields  list of fields you need.
400
     * @param bool  $hydrate whether to hydrate object or not
401
     *
402
     * @return array|null|object
403
     */
404
    public function selectSingleFields($id, array $fields, $hydrate = true)
405
    {
406
        $builder = $this->repository->createQueryBuilder();
407
        $idField = $this->repository->getClassMetadata()->getIdentifier()[0];
408
409
        $record = $builder
410
            ->field($idField)->equals($id)
411
            ->select($fields)
412
            ->hydrate($hydrate)
413
            ->getQuery()
414
            ->getSingleResult();
415
416
        return $record;
417
    }
418
419
    /**
420
     * get classname of entity
421
     *
422
     * @return string
423
     */
424
    public function getEntityClass()
425
    {
426
        return $this->repository->getDocumentName();
427
    }
428
429
    /**
430
     * {@inheritDoc}
431
     *
432
     * Currently this is being used to build the route id used for redirecting
433
     * to newly made documents. It might benefit from having a different name
434
     * for those purposes.
435
     *
436
     * We might use a convention based mapping here:
437
     * Graviton\CoreBundle\Document\App -> mongodb://graviton_core
438
     * Graviton\CoreBundle\Entity\Table -> mysql://graviton_core
439
     *
440
     * @todo implement this in a more convention based manner
441
     *
442
     * @return string
443
     */
444
    public function getConnectionName()
445
    {
446
        $bundle = strtolower(substr(explode('\\', get_class($this))[1], 0, -6));
447
448
        return 'graviton.' . $bundle;
449
    }
450
451
    /**
452
     * Does the actual query using the RQL Bundle.
453
     *
454
     * @param Builder $queryBuilder Doctrine ODM QueryBuilder
455
     * @param Query   $query        query from parser
456
     *
457
     * @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...
458
     */
459
    protected function doRqlQuery($queryBuilder, Query $query)
460
    {
461
        $this->visitor->setBuilder($queryBuilder);
462
463
        return $this->visitor->visit($query);
464
    }
465
466
    /**
467
     * Checks the recordOrigin attribute of a record and will throw an exception if value is not allowed
468
     *
469
     * @param Object $record record
470
     *
471
     * @return void
472
     */
473
    protected function checkIfOriginRecord($record)
474
    {
475
        if ($record instanceof RecordOriginInterface
476
            && !$record->isRecordOriginModifiable()
477
        ) {
478
            $values = $this->notModifiableOriginRecords;
479
            $originValue = strtolower(trim($record->getRecordOrigin()));
480
481
            if (in_array($originValue, $values)) {
482
                $msg = sprintf("Must not be one of the following keywords: %s", implode(', ', $values));
483
484
                throw new RecordOriginModifiedException($msg);
485
            }
486
        }
487
    }
488
489
    /**
490
     * Determines the configured amount fo data records to be returned in pagination context.
491
     *
492
     * @return int
493
     */
494
    private function getDefaultLimit()
495
    {
496
        if (0 < $this->paginationDefaultLimit) {
497
            return $this->paginationDefaultLimit;
498
        }
499
500
        return 10;
501
    }
502
503
    /**
504
     * @param Boolean $active active
505
     * @param String  $field  field
506
     * @return void
507
     */
508
    public function setFilterByAuthUser($active, $field)
509
    {
510
        $this->filterByAuthUser = is_bool($active) ? $active : false;
511
        $this->filterByAuthField = $field;
512
    }
513
}
514