Completed
Push — feature/EVO-7278-created-by-pr... ( ab5acd...945e30 )
by
unknown
429:16 queued 423:35
created

DocumentModel::insertRecord()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 11
ccs 0
cts 8
cp 0
rs 9.4285
cc 3
eloc 6
nc 4
nop 3
crap 12
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
     * @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
        Visitor $visitor,
98
        $notModifiableOriginRecords,
99
        $paginationDefaultLimit
100
    ) {
101
        parent::__construct();
102
        $this->visitor = $visitor;
103
        $this->notModifiableOriginRecords = $notModifiableOriginRecords;
104
        $this->paginationDefaultLimit = (int) $paginationDefaultLimit;
105
    }
106
107
    /**
108
     * get repository instance
109
     *
110
     * @return DocumentRepository
111
     */
112
    public function getRepository()
113
    {
114
        return $this->repository;
115
    }
116
117
    /**
118
     * create new app model
119
     *
120
     * @param DocumentRepository $repository Repository of countries
121
     *
122
     * @return \Graviton\RestBundle\Model\DocumentModel
123
     */
124
    public function setRepository(DocumentRepository $repository)
125
    {
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
     *
138
     * @return array
139
     */
140
    public function findAll(Request $request, SecurityUser $user = null)
141
    {
142
        $pageNumber = $request->query->get('page', 1);
143
        $numberPerPage = (int) $request->query->get('perPage', $this->getDefaultLimit());
144
        $startAt = ($pageNumber - 1) * $numberPerPage;
145
146
        /** @var XiagQuery $xiagQuery */
147
        $xiagQuery = $request->attributes->get('rqlQuery');
148
149
        /** @var MongoBuilder $queryBuilder */
150
        $queryBuilder = $this->repository
151
            ->createQueryBuilder();
152
153
        // Setting RQL Query
154
        if ($xiagQuery) {
155
            // Clean up Search rql param and set it as Doctrine query
156
            if ($xiagQuery->getQuery() && $this->hasCustomSearchIndex() && (float) $this->getMongoDBVersion() >= 2.6) {
157
                $searchQueries = $this->buildSearchQuery($xiagQuery, $queryBuilder);
158
                $xiagQuery = $searchQueries['xiagQuery'];
159
                $queryBuilder = $searchQueries['queryBuilder'];
160
            }
161
            $queryBuilder = $this->doRqlQuery(
162
                $queryBuilder,
163
                $xiagQuery
164
            );
165
        } else {
166
            // @todo [lapistano]: seems the offset is missing for this query.
167
            /** @var \Doctrine\ODM\MongoDB\Query\Builder $qb */
168
            $queryBuilder->find($this->repository->getDocumentName());
169
        }
170
171
        /** @var LimitNode $rqlLimit */
172
        $rqlLimit = $xiagQuery instanceof XiagQuery ? $xiagQuery->getLimit() : false;
173
174
        // define offset and limit
175
        if (!$rqlLimit || !$rqlLimit->getOffset()) {
176
            $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...
177
        } else {
178
            $startAt = (int) $rqlLimit->getOffset();
179
            $queryBuilder->skip($startAt);
180
        }
181
182
        if (!$rqlLimit || is_null($rqlLimit->getLimit())) {
183
            $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...
184
        } else {
185
            $numberPerPage = (int) $rqlLimit->getLimit();
186
            $queryBuilder->limit($numberPerPage);
187
        }
188
189
        // Limit can not be negative nor null.
190
        if ($numberPerPage < 1) {
191
            throw new RqlSyntaxErrorException('negative or null limit in rql');
192
        }
193
194
        /**
195
         * add a default sort on id if none was specified earlier
196
         *
197
         * not specifying something to sort on leads to very weird cases when fetching references.
198
         */
199
        if (!array_key_exists('sort', $queryBuilder->getQuery()->getQuery())) {
200
            $queryBuilder->sort('_id');
201
        }
202
203
        // run query
204
        $query = $queryBuilder->getQuery();
205
        $records = array_values($query->execute()->toArray());
206
207
        $totalCount = $query->count();
208
        $numPages = (int) ceil($totalCount / $numberPerPage);
209
        $page = (int) ceil($startAt / $numberPerPage) + 1;
210
        if ($numPages > 1) {
211
            $request->attributes->set('paging', true);
212
            $request->attributes->set('page', $page);
213
            $request->attributes->set('numPages', $numPages);
214
            $request->attributes->set('startAt', $startAt);
215
            $request->attributes->set('perPage', $numberPerPage);
216
            $request->attributes->set('totalCount', $totalCount);
217
        }
218
219
        return $records;
220
    }
221
222
    /**
223
     * @param XiagQuery    $xiagQuery    Xiag Builder
224
     * @param MongoBuilder $queryBuilder Mongo Doctrine query builder
225
     * @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...
226
     */
227
    private function buildSearchQuery(XiagQuery $xiagQuery, MongoBuilder $queryBuilder)
228
    {
229
        $innerQuery = $xiagQuery->getQuery();
230
        $hasSearch = false;
231
        $nodes = [];
232
        if ($innerQuery instanceof AbstractLogicOperatorNode) {
233
            foreach ($innerQuery->getQueries() as $key => $innerRql) {
234
                if ($innerRql instanceof SearchNode) {
235
                    if (!$hasSearch) {
236
                        $queryBuilder = $this->buildSearchTextQuery($queryBuilder, $innerRql);
237
                        $hasSearch = true;
238
                    }
239
                } else {
240
                    $nodes[] = $innerRql;
241
                }
242
            }
243
        } elseif ($innerQuery instanceof SearchNode) {
244
            $queryBuilder = $this->repository->createQueryBuilder();
245
            $queryBuilder = $this->buildSearchTextQuery($queryBuilder, $innerQuery);
246
            $hasSearch = true;
247
        }
248
        // Remove the Search from RQL xiag
249
        if ($hasSearch && $nodes) {
250
            $newXiagQuery = new XiagQuery();
251
            if ($xiagQuery->getLimit()) {
252
                $newXiagQuery->setLimit($xiagQuery->getLimit());
253
            }
254
            if ($xiagQuery->getSelect()) {
255
                $newXiagQuery->setSelect($xiagQuery->getSelect());
256
            }
257
            if ($xiagQuery->getSort()) {
258
                $newXiagQuery->setSort($xiagQuery->getSort());
259
            }
260
            $binderClass = get_class($innerQuery);
261
            /** @var AbstractLogicOperatorNode $newBinder */
262
            $newBinder = new $binderClass();
263
            foreach ($nodes as $node) {
264
                $newBinder->addQuery($node);
265
            }
266
            $newXiagQuery->setQuery($newBinder);
267
            // Reset original query, so that there is no Search param
268
            $xiagQuery = $newXiagQuery;
269
        }
270
        if ($hasSearch) {
271
            $queryBuilder->sortMeta('score', 'textScore');
272
        }
273
        return [
274
            'xiagQuery'     => $xiagQuery,
275
            'queryBuilder'  => $queryBuilder
276
        ];
277
    }
278
279
    /**
280
     * Check if collection has search indexes in DB
281
     *
282
     * @param string $prefix the prefix for custom text search indexes
283
     * @return bool
284
     */
285
    private function hasCustomSearchIndex($prefix = 'search')
286
    {
287
        $metadata = $this->repository->getClassMetadata();
288
        $indexes = $metadata->getIndexes();
289
        if (count($indexes) < 1) {
290
            return false;
291
        }
292
        $collectionsName = substr($metadata->getName(), strrpos($metadata->getName(), '\\') + 1);
293
        $searchIndexName = $prefix.$collectionsName.'Index';
294
        // We reverse as normally the search index is the last.
295
        foreach (array_reverse($indexes) as $index) {
296
            if (array_key_exists('keys', $index) && array_key_exists($searchIndexName, $index['keys'])) {
297
                return true;
298
            }
299
        }
300
        return false;
301
    }
302
303
    /**
304
     * Build Search text index
305
     *
306
     * @param MongoBuilder $queryBuilder Doctrine mongo query builder object
307
     * @param SearchNode   $searchNode   Graviton Search node
308
     * @return MongoBuilder
309
     */
310
    private function buildSearchTextQuery(MongoBuilder $queryBuilder, SearchNode $searchNode)
311
    {
312
        $searchArr = [];
313
        foreach ($searchNode->getSearchTerms() as $string) {
314
            if (!empty(trim($string))) {
315
                $searchArr[] = (strpos($string, '.') !== false) ? "\"{$string}\"" : $string;
316
            }
317
        }
318
        if (!empty($searchArr)) {
319
            $queryBuilder->addAnd($queryBuilder->expr()->text(implode(' ', $searchArr)));
320
        }
321
        return $queryBuilder;
322
    }
323
324
    /**
325
     * @return string the version of the MongoDB as a string
326
     */
327
    private function getMongoDBVersion()
328
    {
329
        $buildInfo = $this->repository->getDocumentManager()->getDocumentDatabase(
330
            $this->repository->getClassName()
331
        )->command(['buildinfo'=>1]);
332
        if (isset($buildInfo['version'])) {
333
            return $buildInfo['version'];
334
        } else {
335
            return "unkown";
336
        }
337
    }
338
339
    /**
340
     * @param object $entity       entity to insert
341
     * @param bool   $returnEntity true to return entity
342
     * @param bool   $doFlush      if we should flush or not after insert
343
     *
344
     * @return Object|null
345
     */
346
    public function insertRecord($entity, $returnEntity = true, $doFlush = true)
347
    {
348
        $this->manager->persist($entity);
349
350
        if ($doFlush) {
351
            $this->manager->flush($entity);
352
        }
353
        if ($returnEntity) {
354
            return $this->find($entity->getId());
355
        }
356
    }
357
358
    /**
359
     * @param string  $documentId id of entity to find
360
     * @param Request $request    request
0 ignored issues
show
Documentation introduced by
Should the type for parameter $request not be null|Request?

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...
361
     *
362
     * @return Object
363
     */
364
    public function find($documentId, Request $request = null)
365
    {
366
        if ($request instanceof Request) {
367
            // if we are provided a Request, we apply RQL
368
369
            /** @var MongoBuilder $queryBuilder */
370
            $queryBuilder = $this->repository
371
                ->createQueryBuilder();
372
373
            /** @var XiagQuery $query */
374
            $query = $request->attributes->get('rqlQuery');
375
376
            if ($query instanceof XiagQuery) {
377
                $queryBuilder = $this->doRqlQuery(
378
                    $queryBuilder,
379
                    $query
380
                );
381
            }
382
383
            $queryBuilder->field('id')->equals($documentId);
384
385
            $query = $queryBuilder->getQuery();
386
            return $query->getSingleResult();
387
        }
388
389
        return $this->repository->find($documentId);
390
    }
391
392
    /**
393
     * {@inheritDoc}
394
     *
395
     * @param string $documentId   id of entity to update
396
     * @param Object $entity       new entity
397
     * @param bool   $returnEntity true to return entity
398
     *
399
     * @return Object|null
400
     */
401
    public function updateRecord($documentId, $entity, $returnEntity = true)
402
    {
403
        if (!is_null($documentId)) {
404
            $this->deleteById($documentId);
405
            // detach so odm knows it's gone
406
            $this->manager->detach($entity);
407
            $this->manager->clear();
408
        }
409
410
        $entity = $this->manager->merge($entity);
411
412
        $this->manager->persist($entity);
413
        $this->manager->flush($entity);
414
415
        if ($returnEntity) {
416
            return $entity;
417
        }
418
    }
419
420
    /**
421
     * {@inheritDoc}
422
     *
423
     * @param string|object $id id of entity to delete or entity instance
424
     *
425
     * @return null|Object
426
     */
427
    public function deleteRecord($id)
428
    {
429
        if (is_object($id)) {
430
            $entity = $id;
431
        } else {
432
            $entity = $this->find($id);
433
        }
434
435
        $this->checkIfOriginRecord($entity);
436
        $return = $entity;
437
438
        if (is_callable([$entity, 'getId']) && $entity->getId() != null) {
439
            $this->deleteById($entity->getId());
440
            // detach so odm knows it's gone
441
            $this->manager->detach($entity);
442
            $this->manager->clear();
443
            $return = null;
444
        }
445
446
        return $return;
447
    }
448
449
    /**
450
     * Triggers a flush on the DocumentManager
451
     *
452
     * @param null $document optional document
453
     *
454
     * @return void
455
     */
456
    public function flush($document = null)
457
    {
458
        $this->manager->flush($document);
459
    }
460
461
    /**
462
     * A low level delete without any checks
463
     *
464
     * @param mixed $id record id
465
     *
466
     * @return void
467
     */
468
    private function deleteById($id)
469
    {
470
        $builder = $this->repository->createQueryBuilder();
471
        $builder
472
            ->remove()
473
            ->field('id')->equals($id)
474
            ->getQuery()
475
            ->execute();
476
    }
477
478
    /**
479
     * Checks in a performant way if a certain record id exists in the database
480
     *
481
     * @param mixed $id record id
482
     *
483
     * @return bool true if it exists, false otherwise
484
     */
485
    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...
486
    {
487
        return is_array($this->selectSingleFields($id, ['id'], false));
488
    }
489
490
    /**
491
     * Returns a set of fields from an existing resource in a performant manner.
492
     * If you need to check certain fields on an object (and don't need everything), this
493
     * is a better way to get what you need.
494
     * If the record is not present, you will receive null. If you don't need an hydrated
495
     * instance, make sure to pass false there.
496
     *
497
     * @param mixed $id      record id
498
     * @param array $fields  list of fields you need.
499
     * @param bool  $hydrate whether to hydrate object or not
500
     *
501
     * @return array|null|object
502
     */
503
    public function selectSingleFields($id, array $fields, $hydrate = true)
504
    {
505
        $builder = $this->repository->createQueryBuilder();
506
        $idField = $this->repository->getClassMetadata()->getIdentifier()[0];
507
508
        $record = $builder
509
            ->field($idField)->equals($id)
510
            ->select($fields)
511
            ->hydrate($hydrate)
512
            ->getQuery()
513
            ->getSingleResult();
514
515
        return $record;
516
    }
517
518
    /**
519
     * get classname of entity
520
     *
521
     * @return string|null
522
     */
523
    public function getEntityClass()
524
    {
525
        if ($this->repository instanceof DocumentRepository) {
526
            return $this->repository->getDocumentName();
527
        }
528
529
        return null;
530
    }
531
532
    /**
533
     * {@inheritDoc}
534
     *
535
     * Currently this is being used to build the route id used for redirecting
536
     * to newly made documents. It might benefit from having a different name
537
     * for those purposes.
538
     *
539
     * We might use a convention based mapping here:
540
     * Graviton\CoreBundle\Document\App -> mongodb://graviton_core
541
     * Graviton\CoreBundle\Entity\Table -> mysql://graviton_core
542
     *
543
     * @todo implement this in a more convention based manner
544
     *
545
     * @return string
546
     */
547
    public function getConnectionName()
548
    {
549
        $bundle = strtolower(substr(explode('\\', get_class($this))[1], 0, -6));
550
551
        return 'graviton.' . $bundle;
552
    }
553
554
    /**
555
     * Does the actual query using the RQL Bundle.
556
     *
557
     * @param Builder $queryBuilder Doctrine ODM QueryBuilder
558
     * @param Query   $query        query from parser
559
     *
560
     * @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...
561
     */
562
    protected function doRqlQuery($queryBuilder, Query $query)
563
    {
564
        $this->visitor->setBuilder($queryBuilder);
565
566
        return $this->visitor->visit($query);
567
    }
568
569
    /**
570
     * Checks the recordOrigin attribute of a record and will throw an exception if value is not allowed
571
     *
572
     * @param Object $record record
573
     *
574
     * @return void
575
     */
576
    protected function checkIfOriginRecord($record)
577
    {
578
        if ($record instanceof RecordOriginInterface
579
            && !$record->isRecordOriginModifiable()
580
        ) {
581
            $values = $this->notModifiableOriginRecords;
582
            $originValue = strtolower(trim($record->getRecordOrigin()));
583
584
            if (in_array($originValue, $values)) {
585
                $msg = sprintf("Must not be one of the following keywords: %s", implode(', ', $values));
586
587
                throw new RecordOriginModifiedException($msg);
588
            }
589
        }
590
    }
591
592
    /**
593
     * Determines the configured amount fo data records to be returned in pagination context.
594
     *
595
     * @return int
596
     */
597
    private function getDefaultLimit()
598
    {
599
        if (0 < $this->paginationDefaultLimit) {
600
            return $this->paginationDefaultLimit;
601
        }
602
603
        return 10;
604
    }
605
606
    /**
607
     * @param Boolean $active active
608
     * @param String  $field  field
609
     * @return void
610
     */
611
    public function setFilterByAuthUser($active, $field)
612
    {
613
        $this->filterByAuthUser = is_bool($active) ? $active : false;
614
        $this->filterByAuthField = $field;
615
    }
616
}
617