Completed
Push — feature/EVO-5751-text-index-mo... ( 0ab3ac...8fecb1 )
by
unknown
62:16 queued 57:01
created

DocumentModel::setRepository()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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