Completed
Push — feature/EVO-5751-text-search-j... ( 60a9de...198b26 )
by
unknown
179:20 queued 174:02
created

DocumentModel::deleteRecord()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 18
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 4
Bugs 0 Features 0
Metric Value
c 4
b 0
f 0
dl 0
loc 18
ccs 0
cts 13
cp 0
rs 9.4285
cc 3
eloc 12
nc 4
nop 1
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\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
151
        /** @var \Doctrine\ODM\MongoDB\Query\Builder $queryBuilder */
152
        $queryBuilder = $this->repository
153
            ->createQueryBuilder();
154
155
        if ($this->filterByAuthUser && $user && $user->hasRole(SecurityUser::ROLE_USER)) {
156
            $queryBuilder->field($this->filterByAuthField)->equals($user->getUser()->getId());
157
        }
158
159
        // *** do we have an RQL expression, do we need to filter data?
160
        if ($request->attributes->get('hasRql', false)) {
161
            $innerQuery = $request->attributes->get('rqlQuery')->getQuery();
162
            $xiagQuery = new XiagQuery();
163
            // can we perform a search in an index instead of filtering?
164
            if ($innerQuery instanceof AbstractLogicOperatorNode) {
165
                foreach ($innerQuery->getQueries() as $innerRql) {
166 View Code Duplication
                    if ($innerRql instanceof SearchNode) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
167
                        $searchString = implode('&', $innerRql->getSearchTerms());
168
                        $queryBuilder->addAnd(
169
                            $queryBuilder->expr()->text($searchString)
170
                        );
171
                    } else {
172
                        $xiagQuery->setQuery($innerRql);
173
                    }
174
                }
175 View Code Duplication
            } elseif ($this->hasCustomSearchIndex() && ($innerQuery instanceof SearchNode)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

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