Completed
Push — feature/EVO-5751-text-search-j... ( 552952...2bbfdd )
by
unknown
241:23 queued 236:05
created

DocumentModel::deleteById()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 1

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 9
ccs 7
cts 7
cp 1
rs 9.6666
cc 1
eloc 7
nc 1
nop 1
crap 1
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\Node\Query\AbstractLogicOperatorNode;
19
use Xiag\Rql\Parser\Node\Query\AbstractScalarOperatorNode;
20
use Xiag\Rql\Parser\Query;
21
use Graviton\ExceptionBundle\Exception\RecordOriginModifiedException;
22
use Xiag\Rql\Parser\Exception\SyntaxErrorException as RqlSyntaxErrorException;
23
use Graviton\SchemaBundle\Document\Schema as SchemaDocument;
24
use Xiag\Rql\Parser\Query as XiagQuery;
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 RqlTranslator
84
     */
85
    protected $translator;
86
87
    /**
88
     * @var DocumentManager
89
     */
90
    protected $manager;
91
92
    /**
93
     * @param Visitor       $visitor                    rql query visitor
94
     * @param RqlTranslator $translator                 Translator for query modification
95
     * @param array         $notModifiableOriginRecords strings with not modifiable recordOrigin values
96
     * @param integer       $paginationDefaultLimit     amount of data records to be returned when in pagination context
97
     */
98 4
    public function __construct(
99
        Visitor $visitor,
100
        RqlTranslator $translator,
101
        $notModifiableOriginRecords,
102
        $paginationDefaultLimit
103
    ) {
104 4
        parent::__construct();
105 4
        $this->visitor = $visitor;
106 4
        $this->translator = $translator;
107 4
        $this->notModifiableOriginRecords = $notModifiableOriginRecords;
108 4
        $this->paginationDefaultLimit = (int) $paginationDefaultLimit;
109 4
    }
110
111
    /**
112
     * get repository instance
113
     *
114
     * @return DocumentRepository
115
     */
116 2
    public function getRepository()
117
    {
118 2
        return $this->repository;
119
    }
120
121
    /**
122
     * create new app model
123
     *
124
     * @param DocumentRepository $repository Repository of countries
125
     *
126
     * @return \Graviton\RestBundle\Model\DocumentModel
127
     */
128 4
    public function setRepository(DocumentRepository $repository)
129
    {
130 4
        $this->repository = $repository;
131 4
        $this->manager = $repository->getDocumentManager();
132
133 4
        return $this;
134
    }
135
136
    /**
137
     * {@inheritDoc}
138
     *
139
     * @param Request        $request The request object
140
     * @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...
141
     * @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...
142
     *
143
     * @return array
144
     */
145
    public function findAll(Request $request, SecurityUser $user = null, SchemaDocument $schema = null)
146
    {
147
        $pageNumber = $request->query->get('page', 1);
148
        $numberPerPage = (int) $request->query->get('perPage', $this->getDefaultLimit());
149
        $startAt = ($pageNumber - 1) * $numberPerPage;
150
        // Only 1 search text node allowed.
151
        $hasSearch = false;
152
153
        /** @var \Doctrine\ODM\MongoDB\Query\Builder $queryBuilder */
154
        $queryBuilder = $this->repository
155
            ->createQueryBuilder();
156
157
        if ($this->filterByAuthUser && $user && $user->hasRole(SecurityUser::ROLE_USER)) {
158
            $queryBuilder->field($this->filterByAuthField)->equals($user->getUser()->getId());
159
        }
160
161
        // *** do we have an RQL expression, do we need to filter data?
162
        if ($request->attributes->get('hasRql', false)) {
163
            $innerQuery = $request->attributes->get('rqlQuery')->getQuery();
164
            $xiagQuery = new XiagQuery();
165
            // can we perform a search in an index instead of filtering?
166
            if ($innerQuery instanceof AbstractLogicOperatorNode) {
167
                foreach ($innerQuery->getQueries() as $innerRql) {
168
                    if (!$hasSearch && $innerRql instanceof SearchNode) {
169
                        $searchString = implode('&', $innerRql->getSearchTerms());
170
                        $queryBuilder->addAnd(
171
                            $queryBuilder->expr()->text($searchString)
172
                        );
173
                        $hasSearch = true;
174
                    } else {
175
                        $xiagQuery->setQuery($innerRql);
176
                    }
177
                }
178
            } elseif ($this->hasCustomSearchIndex() && ($innerQuery instanceof SearchNode)) {
179
                $searchString = implode('&', $innerQuery->getSearchTerms());
180
                $queryBuilder->addAnd(
181
                    $queryBuilder->expr()->text($searchString)
182
                );
183
                $hasSearch = true;
184
            } else {
185
                if ($innerQuery instanceof AbstractScalarOperatorNode) {
186
                    $xiagQuery->setQuery($innerQuery);
187
                } else {
188
                    /** @var AbstractLogicOperatorNode $innerQuery */
189
                    foreach ($innerQuery->getQueries() as $innerRql) {
190
                        if (!$innerRql instanceof SearchNode) {
191
                            $xiagQuery->setQuery($innerRql);
192
                        }
193
                    }
194
                }
195
            }
196
197
            $queryBuilder = $this->doRqlQuery(
198
                $queryBuilder,
199
                $xiagQuery
200
            );
201
        } else {
202
            // @todo [lapistano]: seems the offset is missing for this query.
203
            /** @var \Doctrine\ODM\MongoDB\Query\Builder $qb */
204
            $queryBuilder->find($this->repository->getDocumentName());
205
        }
206
207
        // define offset and limit
208
        if (!array_key_exists('skip', $queryBuilder->getQuery()->getQuery())) {
209
            $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...
210
        } else {
211
            $startAt = (int) $queryBuilder->getQuery()->getQuery()['skip'];
212
        }
213
214
        if (!array_key_exists('limit', $queryBuilder->getQuery()->getQuery())) {
215
            $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...
216
        } else {
217
            $numberPerPage = (int) $queryBuilder->getQuery()->getQuery()['limit'];
218
        }
219
220
        // Limit can not be negative nor null.
221
        if ($numberPerPage < 1) {
222
            throw new RqlSyntaxErrorException('negative or null limit in rql');
223
        }
224
225
        /**
226
         * add a default sort on id if none was specified earlier
227
         *
228
         * not specifying something to sort on leads to very weird cases when fetching references
229
         * If search node, sort by Score
230
         * TODO Review this sorting, not 100% sure
231
         */
232
        if ($hasSearch && !array_key_exists('sort', $queryBuilder->getQuery()->getQuery())) {
233
            $queryBuilder->sortMeta('score', 'textScore');
0 ignored issues
show
Bug introduced by
The method sortMeta does only exist in Doctrine\ODM\MongoDB\Query\Builder, but not in Doctrine\ODM\MongoDB\Query\Expr.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

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