Completed
Push — feature/EVO-5751-text-search-t... ( e9be3c...f38c9e )
by
unknown
12:11
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\LimitNode;
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
25
/**
26
 * Use doctrine odm as backend
27
 *
28
 * @author  List of contributors <https://github.com/libgraviton/graviton/graphs/contributors>
29
 * @license http://opensource.org/licenses/gpl-license.php GNU Public License
30
 * @link    http://swisscom.ch
31
 */
32
class DocumentModel extends SchemaModel implements ModelInterface
33
{
34
    /**
35
     * @var string
36
     */
37
    protected $description;
38
    /**
39
     * @var string[]
40
     */
41
    protected $fieldTitles;
42
    /**
43
     * @var string[]
44
     */
45
    protected $fieldDescriptions;
46
    /**
47
     * @var string[]
48
     */
49
    protected $requiredFields = array();
50
    /**
51
     * @var string[]
52
     */
53
    protected $searchableFields = array();
54
    /**
55
     * @var DocumentRepository
56
     */
57
    private $repository;
58
    /**
59
     * @var Visitor
60
     */
61
    private $visitor;
62
    /**
63
     * @var array
64
     */
65
    protected $notModifiableOriginRecords;
66
    /**
67
     * @var  integer
68
     */
69
    private $paginationDefaultLimit;
70
71
    /**
72
     * @var boolean
73
     */
74
    protected $filterByAuthUser;
75
76
    /**
77
     * @var string
78
     */
79
    protected $filterByAuthField;
80
81
    /**
82
     * @var RqlTranslator
83
     */
84
    protected $translator;
85
86
    /**
87
     * @var DocumentManager
88
     */
89
    protected $manager;
90
91
    /**
92
     * @param Visitor       $visitor                    rql query visitor
93
     * @param RqlTranslator $translator                 Translator for query modification
94
     * @param array         $notModifiableOriginRecords strings with not modifiable recordOrigin values
95
     * @param integer       $paginationDefaultLimit     amount of data records to be returned when in pagination context
96
     */
97 4
    public function __construct(
98
        Visitor $visitor,
99
        RqlTranslator $translator,
100
        $notModifiableOriginRecords,
101
        $paginationDefaultLimit
102
    ) {
103 4
        parent::__construct();
104 4
        $this->visitor = $visitor;
105 4
        $this->translator = $translator;
106 4
        $this->notModifiableOriginRecords = $notModifiableOriginRecords;
107 4
        $this->paginationDefaultLimit = (int) $paginationDefaultLimit;
108 4
    }
109
110
    /**
111
     * get repository instance
112
     *
113
     * @return DocumentRepository
114
     */
115 2
    public function getRepository()
116
    {
117 2
        return $this->repository;
118
    }
119
120
    /**
121
     * create new app model
122
     *
123
     * @param DocumentRepository $repository Repository of countries
124
     *
125
     * @return \Graviton\RestBundle\Model\DocumentModel
126
     */
127 4
    public function setRepository(DocumentRepository $repository)
128
    {
129 4
        $this->repository = $repository;
130 4
        $this->manager = $repository->getDocumentManager();
131
132 4
        return $this;
133
    }
134
135
    /**
136
     * {@inheritDoc}
137
     *
138
     * @param Request        $request The request object
139
     * @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...
140
     * @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...
141
     *
142
     * @return array
143
     */
144
    public function findAll(Request $request, SecurityUser $user = null, SchemaDocument $schema = null)
145
    {
146
        $pageNumber = $request->query->get('page', 1);
147
        $numberPerPage = (int) $request->query->get('perPage', $this->getDefaultLimit());
148
        $startAt = ($pageNumber - 1) * $numberPerPage;
149
        // Only 1 search text node allowed.
150
        $hasSearch = false;
151
152
        /** @var \Doctrine\ODM\MongoDB\Query\Builder $queryBuilder */
153
        $queryBuilder = $this->repository
154
            ->createQueryBuilder();
155
156
        if ($this->filterByAuthUser && $user && $user->hasRole(SecurityUser::ROLE_USER)) {
157
            $queryBuilder->field($this->filterByAuthField)->equals($user->getUser()->getId());
158
        }
159
160
        // *** do we have an RQL expression, do we need to filter data?
161
        if ($request->attributes->get('hasRql', false)) {
162
            $innerQuery = $request->attributes->get('rqlQuery')->getQuery();
163
164
            if ($innerQuery instanceof SearchNode &&
165
                (float) $this->getMongoDBVersion() >= 2.6
166
                && $this->hasCustomSearchIndex()
167
            ) {
168
                $queryBuilder->addAnd(
169
                    $queryBuilder->expr()->text(implode("&", $innerQuery->getSearchTerms()))
170
                );
171
                $hasSearch = true;
172
            } else {
173
                // do rql filtering
174
                $searchableFields = $this->getSearchableFields();
175
                if (!is_null($schema)) {
176
                    $searchableFields = $schema->getSearchable();
177
                }
178
                $queryBuilder = $this->doRqlQuery(
179
                    $queryBuilder,
180
                    $this->translator->translateSearchQuery(
181
                        $request->attributes->get('rqlQuery'),
182
                        $searchableFields
0 ignored issues
show
Bug introduced by
It seems like $searchableFields defined by $schema->getSearchable() on line 176 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...
183
                    )
184
                );
185
            }
186
        } else {
187
            // @todo [lapistano]: seems the offset is missing for this query.
188
            /** @var \Doctrine\ODM\MongoDB\Query\Builder $qb */
189
            $queryBuilder->find($this->repository->getDocumentName());
190
        }
191
192
        // define offset and limit
193
        if (!array_key_exists('skip', $queryBuilder->getQuery()->getQuery())) {
194
            $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...
195
        } else {
196
            $startAt = (int) $queryBuilder->getQuery()->getQuery()['skip'];
197
        }
198
199
        if (!array_key_exists('limit', $queryBuilder->getQuery()->getQuery())) {
200
            $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...
201
        } else {
202
            $numberPerPage = (int) $queryBuilder->getQuery()->getQuery()['limit'];
203
        }
204
205
        // Limit can not be negative nor null.
206
        if ($numberPerPage < 1) {
207
            throw new RqlSyntaxErrorException('negative or null limit in rql');
208
        }
209
210
        /**
211
         * add a default sort on id if none was specified earlier
212
         *
213
         * not specifying something to sort on leads to very weird cases when fetching references
214
         * If search node, sort by Score
215
         * TODO Review this sorting, not 100% sure
216
         */
217
        if ($hasSearch && !array_key_exists('sort', $queryBuilder->getQuery()->getQuery())) {
218
            $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...
219
        } elseif (!array_key_exists('sort', $queryBuilder->getQuery()->getQuery())) {
220
            $queryBuilder->sort('_id');
221
        }
222
223
        // run query
224
        $query = $queryBuilder->getQuery();
225
        $records = array_values($query->execute()->toArray());
226
227
        $totalCount = $query->count();
228
        $numPages = (int) ceil($totalCount / $numberPerPage);
229
        $page = (int) ceil($startAt / $numberPerPage) + 1;
230
        if ($numPages > 1) {
231
            $request->attributes->set('paging', true);
232
            $request->attributes->set('page', $page);
233
            $request->attributes->set('numPages', $numPages);
234
            $request->attributes->set('startAt', $startAt);
235
            $request->attributes->set('perPage', $numberPerPage);
236
            $request->attributes->set('totalCount', $totalCount);
237
        }
238
239
        return $records;
240
    }
241
242
    /**
243
     * @param string $prefix the prefix for custom text search indexes
244
     * @return bool
245
     * @throws \Doctrine\ODM\MongoDB\MongoDBException
246
     */
247
    private function hasCustomSearchIndex($prefix = 'search')
248
    {
249
        $collection = $this->repository->getDocumentManager()->getDocumentCollection($this->repository->getClassName());
250
        $indexesInfo = $collection->getIndexInfo();
251
        foreach ($indexesInfo as $indexInfo) {
252
            if ($indexInfo['name']==$prefix.$collection->getName().'Index') {
253
                return true;
254
            }
255
        }
256
        return false;
257
    }
258
259
    /**
260
     * @return string the version of the MongoDB as a string
261
     */
262
    private function getMongoDBVersion()
263
    {
264
        $buildInfo = $this->repository->getDocumentManager()->getDocumentDatabase(
265
            $this->repository->getClassName()
266
        )->command(['buildinfo'=>1]);
267
        if (isset($buildInfo['version'])) {
268
            return $buildInfo['version'];
269
        } else {
270
            return "unkown";
271
        }
272
    }
273
274
    /**
275
     * @param object $entity       entity to insert
276
     * @param bool   $returnEntity true to return entity
277
     * @param bool   $doFlush      if we should flush or not after insert
278
     *
279
     * @return Object|null
280
     */
281
    public function insertRecord($entity, $returnEntity = true, $doFlush = true)
282
    {
283
        $this->checkIfOriginRecord($entity);
284
        $this->manager->persist($entity);
285
286
        if ($doFlush) {
287
            $this->manager->flush($entity);
288
        }
289
        if ($returnEntity) {
290
            return $this->find($entity->getId());
291
        }
292
    }
293
294
    /**
295
     * @param string $documentId id of entity to find
296
     *
297
     * @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...
298
     */
299 4
    public function find($documentId)
300
    {
301 4
        return $this->repository->find($documentId);
302
    }
303
304
    /**
305
     * {@inheritDoc}
306
     *
307
     * @param string $documentId   id of entity to update
308
     * @param Object $entity       new entity
309
     * @param bool   $returnEntity true to return entity
310
     *
311
     * @return Object|null
312
     */
313 3
    public function updateRecord($documentId, $entity, $returnEntity = true)
314
    {
315
        // In both cases the document attribute named originRecord must not be 'core'
316 2
        $this->checkIfOriginRecord($entity);
317 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...
318
319 2
        if (!is_null($documentId)) {
320 2
            $this->deleteById($documentId);
321
            // detach so odm knows it's gone
322 2
            $this->manager->detach($entity);
323 2
            $this->manager->clear();
324 1
        }
325
326 3
        $entity = $this->manager->merge($entity);
327
328 2
        $this->manager->persist($entity);
329 2
        $this->manager->flush($entity);
330
331 2
        if ($returnEntity) {
332
            return $entity;
333
        }
334 2
    }
335
336
    /**
337
     * {@inheritDoc}
338
     *
339
     * @param string|object $id id of entity to delete or entity instance
340
     *
341
     * @return null|Object
342
     */
343
    public function deleteRecord($id)
344
    {
345
        if (is_object($id)) {
346
            $entity = $id;
347
        } else {
348
            $entity = $this->find($id);
349
        }
350
351
        $return = $entity;
352
        if ($entity) {
353
            $this->checkIfOriginRecord($entity);
354
            $this->manager->remove($entity);
355
            $this->manager->flush();
356
            $return = null;
357
        }
358
359
        return $return;
360
    }
361
362
    /**
363
     * Triggers a flush on the DocumentManager
364
     *
365
     * @param null $document optional document
366
     *
367
     * @return void
368
     */
369
    public function flush($document = null)
370
    {
371
        $this->manager->flush($document);
372
    }
373
374
    /**
375
     * A low level delete without any checks
376
     *
377
     * @param mixed $id record id
378
     *
379
     * @return void
380
     */
381 2
    private function deleteById($id)
382
    {
383 2
        $builder = $this->repository->createQueryBuilder();
384
        $builder
385 2
            ->remove()
386 2
            ->field('id')->equals($id)
387 2
            ->getQuery()
388 2
            ->execute();
389 2
    }
390
391
    /**
392
     * Checks in a performant way if a certain record id exists in the database
393
     *
394
     * @param mixed $id record id
395
     *
396
     * @return bool true if it exists, false otherwise
397
     */
398 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...
399
    {
400 4
        return is_array($this->selectSingleFields($id, ['id'], false));
401
    }
402
403
    /**
404
     * Returns a set of fields from an existing resource in a performant manner.
405
     * If you need to check certain fields on an object (and don't need everything), this
406
     * is a better way to get what you need.
407
     * If the record is not present, you will receive null. If you don't need an hydrated
408
     * instance, make sure to pass false there.
409
     *
410
     * @param mixed $id      record id
411
     * @param array $fields  list of fields you need.
412
     * @param bool  $hydrate whether to hydrate object or not
413
     *
414
     * @return array|null|object
415
     */
416 4
    public function selectSingleFields($id, array $fields, $hydrate = true)
417
    {
418 4
        $builder = $this->repository->createQueryBuilder();
419 4
        $idField = $this->repository->getClassMetadata()->getIdentifier()[0];
420
421
        $record = $builder
422 4
            ->field($idField)->equals($id)
423 4
            ->select($fields)
424 4
            ->hydrate($hydrate)
425 4
            ->getQuery()
426 4
            ->getSingleResult();
427
428 4
        return $record;
429
    }
430
431
    /**
432
     * get classname of entity
433
     *
434
     * @return string|null
435
     */
436 4
    public function getEntityClass()
437
    {
438 4
        if ($this->repository instanceof DocumentRepository) {
439 4
            return $this->repository->getDocumentName();
440
        }
441
442
        return null;
443
    }
444
445
    /**
446
     * {@inheritDoc}
447
     *
448
     * Currently this is being used to build the route id used for redirecting
449
     * to newly made documents. It might benefit from having a different name
450
     * for those purposes.
451
     *
452
     * We might use a convention based mapping here:
453
     * Graviton\CoreBundle\Document\App -> mongodb://graviton_core
454
     * Graviton\CoreBundle\Entity\Table -> mysql://graviton_core
455
     *
456
     * @todo implement this in a more convention based manner
457
     *
458
     * @return string
459
     */
460
    public function getConnectionName()
461
    {
462
        $bundle = strtolower(substr(explode('\\', get_class($this))[1], 0, -6));
463
464
        return 'graviton.' . $bundle;
465
    }
466
467
    /**
468
     * Does the actual query using the RQL Bundle.
469
     *
470
     * @param Builder $queryBuilder Doctrine ODM QueryBuilder
471
     * @param Query   $query        query from parser
472
     *
473
     * @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...
474
     */
475
    protected function doRqlQuery($queryBuilder, Query $query)
476
    {
477
        $this->visitor->setBuilder($queryBuilder);
478
479
        return $this->visitor->visit($query);
480
    }
481
482
    /**
483
     * Checks the recordOrigin attribute of a record and will throw an exception if value is not allowed
484
     *
485
     * @param Object $record record
486
     *
487
     * @return void
488
     */
489 14
    protected function checkIfOriginRecord($record)
490
    {
491 7
        if ($record instanceof RecordOriginInterface
492 14
            && !$record->isRecordOriginModifiable()
493 7
        ) {
494 6
            $values = $this->notModifiableOriginRecords;
495 6
            $originValue = strtolower(trim($record->getRecordOrigin()));
496
497 6
            if (in_array($originValue, $values)) {
498 2
                $msg = sprintf("Must not be one of the following keywords: %s", implode(', ', $values));
499
500 2
                throw new RecordOriginModifiedException($msg);
501
            }
502 2
        }
503 12
    }
504
505
    /**
506
     * Determines the configured amount fo data records to be returned in pagination context.
507
     *
508
     * @return int
509
     */
510
    private function getDefaultLimit()
511
    {
512
        if (0 < $this->paginationDefaultLimit) {
513
            return $this->paginationDefaultLimit;
514
        }
515
516
        return 10;
517
    }
518
519
    /**
520
     * @param Boolean $active active
521
     * @param String  $field  field
522
     * @return void
523
     */
524 4
    public function setFilterByAuthUser($active, $field)
525
    {
526 4
        $this->filterByAuthUser = is_bool($active) ? $active : false;
527 4
        $this->filterByAuthField = $field;
528 4
    }
529
}
530