Completed
Push — feature/EVO-5751-text-index-mo... ( ad25d5...77d107 )
by
unknown
54:22 queued 49:02
created

DocumentModel::flush()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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