Completed
Push — feature/other-validation ( dcca37...1a6d02 )
by Narcotic
65:32
created

DocumentModel::recordExists()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 1
crap 1
1
<?php
2
/**
3
 * Use doctrine odm as backend
4
 */
5
6
namespace Graviton\RestBundle\Model;
7
8
use Doctrine\ODM\MongoDB\DocumentManager;
9
use Doctrine\ODM\MongoDB\DocumentRepository;
10
use Graviton\RestBundle\Service\RqlTranslator;
11
use Graviton\Rql\Node\SearchNode;
12
use Graviton\SchemaBundle\Model\SchemaModel;
13
use Graviton\SecurityBundle\Entities\SecurityUser;
14
use Symfony\Component\HttpFoundation\Request;
15
use Doctrine\ODM\MongoDB\Query\Builder;
16
use Graviton\Rql\Visitor\MongoOdm as Visitor;
17
use Xiag\Rql\Parser\Node\Query\LogicOperator\AndNode;
18
use Xiag\Rql\Parser\Node\Query\LogicOperator\OrNode;
19
use Xiag\Rql\Parser\Node\Query\ScalarOperator\LikeNode;
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
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
    public function __construct(
98
        Visitor $visitor,
99
        RqlTranslator $translator,
100
        $notModifiableOriginRecords,
101
        $paginationDefaultLimit
102
    ) {
103
        parent::__construct();
104
        $this->visitor = $visitor;
105
        $this->translator = $translator;
106
        $this->notModifiableOriginRecords = $notModifiableOriginRecords;
107
        $this->paginationDefaultLimit = (int) $paginationDefaultLimit;
108
    }
109
110
    /**
111
     * get repository instance
112
     *
113
     * @return DocumentRepository
114
     */
115
    public function getRepository()
116
    {
117
        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
    public function setRepository(DocumentRepository $repository)
128
    {
129
        $this->repository = $repository;
130
        $this->manager = $repository->getDocumentManager();
131
132
        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
150
        /** @var \Doctrine\ODM\MongoDB\Query\Builder $queryBuilder */
151
        $queryBuilder = $this->repository
152
            ->createQueryBuilder();
153
154
        if ($this->filterByAuthUser && $user && $user->hasRole(SecurityUser::ROLE_USER)) {
155
            $queryBuilder->field($this->filterByAuthField)->equals($user->getUser()->getId());
156
        }
157
158
159
        $searchableFields = $this->getSearchableFields();
160
        if (!is_null($schema)) {
161
            $searchableFields = $schema->getSearchable();
162
        }
163
164
        // *** do we have an RQL expression, do we need to filter data?
165
        if ($request->attributes->get('hasRql', false)) {
166
            $queryBuilder = $this->doRqlQuery(
167
                $queryBuilder,
168
                $this->translator->translateSearchQuery(
169
                    $request->attributes->get('rqlQuery'),
170
                    $searchableFields
0 ignored issues
show
Bug introduced by
It seems like $searchableFields defined by $schema->getSearchable() on line 161 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...
171
                )
172
            );
173
        } else {
174
            // @todo [lapistano]: seems the offset is missing for this query.
175
            /** @var \Doctrine\ODM\MongoDB\Query\Builder $qb */
176
            $queryBuilder->find($this->repository->getDocumentName());
177
        }
178
179
        // define offset and limit
180
        if (!array_key_exists('skip', $queryBuilder->getQuery()->getQuery())) {
181
            $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...
182
        } else {
183
            $startAt = (int) $queryBuilder->getQuery()->getQuery()['skip'];
184
        }
185
186
        if (!array_key_exists('limit', $queryBuilder->getQuery()->getQuery())) {
187
            $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...
188
        } else {
189
            $numberPerPage = (int) $queryBuilder->getQuery()->getQuery()['limit'];
190
        }
191
192
        // Limit can not be negative nor null.
193
        if ($numberPerPage < 1) {
194
            throw new RqlSyntaxErrorException('negative or null limit in rql');
195
        }
196
197
        /**
198
         * add a default sort on id if none was specified earlier
199
         *
200
         * not specifying something to sort on leads to very weird cases when fetching references
201
         */
202
        if (!array_key_exists('sort', $queryBuilder->getQuery()->getQuery())) {
203
            $queryBuilder->sort('_id');
204
        }
205
206
        // run query
207
        $query = $queryBuilder->getQuery();
208
        $records = array_values($query->execute()->toArray());
209
210
        $totalCount = $query->count();
211
        $numPages = (int) ceil($totalCount / $numberPerPage);
212
        $page = (int) ceil($startAt / $numberPerPage) + 1;
213
        if ($numPages > 1) {
214
            $request->attributes->set('paging', true);
215
            $request->attributes->set('page', $page);
216
            $request->attributes->set('numPages', $numPages);
217
            $request->attributes->set('startAt', $startAt);
218
            $request->attributes->set('perPage', $numberPerPage);
219
            $request->attributes->set('totalCount', $totalCount);
220
        }
221
222
        return $records;
223
    }
224
225
    /**
226
     * @param object $entity       entity to insert
227
     * @param bool   $returnEntity true to return entity
228
     * @param bool   $doFlush      if we should flush or not after insert
229
     *
230
     * @return Object|null
231
     */
232
    public function insertRecord($entity, $returnEntity = true, $doFlush = true)
233
    {
234
        $this->checkIfOriginRecord($entity);
235
        $this->manager->persist($entity);
236
237
        if ($doFlush) $this->manager->flush($entity);
0 ignored issues
show
Coding Style introduced by
Please always use braces to surround the code block of IF statements.
Loading history...
Coding Style Best Practice introduced by
It is generally a best practice to always use braces with control structures.

Adding braces to control structures avoids accidental mistakes as your code changes:

// Without braces (not recommended)
if (true)
    doSomething();

// Recommended
if (true) {
    doSomething();
}
Loading history...
238
        if ($returnEntity) return $this->find($entity->getId());
0 ignored issues
show
Coding Style introduced by
Please always use braces to surround the code block of IF statements.
Loading history...
Coding Style Best Practice introduced by
It is generally a best practice to always use braces with control structures.

Adding braces to control structures avoids accidental mistakes as your code changes:

// Without braces (not recommended)
if (true)
    doSomething();

// Recommended
if (true) {
    doSomething();
}
Loading history...
239
    }
240
241
    /**
242
     * @param string $documentId id of entity to find
243
     *
244
     * @return Object
245
     */
246
    public function find($documentId)
247
    {
248
        return $this->repository->find($documentId);
249
    }
250
251
    /**
252
     * {@inheritDoc}
253
     *
254
     * @param string $documentId   id of entity to update
255
     * @param Object $entity       new entity
256
     * @param bool   $returnEntity true to return entity
257
     *
258
     * @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...
259
     */
260
    public function updateRecord($documentId, $entity, $returnEntity = true)
261
    {
262
        // In both cases the document attribute named originRecord must not be 'core'
263
        $this->checkIfOriginRecord($entity);
264
        $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...
265
        /*
266
         * @TODO @hairmare i remember this fixed something important - why do we do it? can't we just delete the old
267
         * document and insert the new one? we always have the entire object(?) i guess would be faster
268
         * and we surely wouldn't have any array update problems as in the past..
269
         */
270
        /*
271
        $manager = $this->repository->getDocumentManager();
272
        $entity = $manager->merge($entity);
273
        $manager->flush();
274
        */
275
276
        $this->deleteById($documentId);
277
        $this->manager->persist($entity);
278
        $this->manager->flush($entity);
279
280
        if ($returnEntity) return $entity;
0 ignored issues
show
Coding Style introduced by
Please always use braces to surround the code block of IF statements.
Loading history...
Coding Style Best Practice introduced by
It is generally a best practice to always use braces with control structures.

Adding braces to control structures avoids accidental mistakes as your code changes:

// Without braces (not recommended)
if (true)
    doSomething();

// Recommended
if (true) {
    doSomething();
}
Loading history...
281
    }
282
283
    /**
284
     * {@inheritDoc}
285
     *
286
     * @param string|object $id id of entity to delete or entity instance
287
     *
288
     * @return null|Object
289
     */
290
    public function deleteRecord($id)
291
    {
292
        if (is_object($id)) {
293
            $entity = $id;
294
        } else {
295
            $entity = $this->find($id);
296
        }
297
298
        $return = $entity;
299
        if ($entity) {
300
            $this->checkIfOriginRecord($entity);
301
            $this->manager->remove($entity);
302
            $this->manager->flush();
303
            $return = null;
304
        }
305
306
        return $return;
307
    }
308
309
    /**
310
     * Triggers a flush on the DocumentManager
311
     *
312
     * @param null $document optional document
313
     *
314
     * @return void
315
     */
316
    public function flush($document = null)
317
    {
318
        $this->manager->flush($document);
319
    }
320
321
    /**
322
     * A low level delete without any checks
323
     *
324
     * @param mixed $id record id
325
     *
326
     * @return void
327
     */
328
    private function deleteById($id)
329
    {
330
        $builder = $this->repository->createQueryBuilder();
331
        $builder
332
            ->remove()
333
            ->field('id')->equals($id)
334
            ->getQuery()
335
            ->execute();
336
    }
337
338
    /**
339
     * Checks in a performant way if a certain record id exists in the database
340 12
     *
341
     * @param mixed $id record id
342 6
     *
343 12
     * @return bool true if it exists, false otherwise
344 6
     */
345 6
    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...
346 6
    {
347
        return is_array($this->selectSingleFields($id, ['id'], false));
348 6
    }
349 2
350
    /**
351 2
     * Returns a set of fields from an existing resource in a performant manner.
352
     * If you need to check certain fields on an object (and don't need everything), this
353 2
     * is a better way to get what you need.
354 10
     * If the record is not present, you will receive null. If you don't need an hydrated
355
     * instance, make sure to pass false there.
356
     *
357
     * @param mixed $id      record id
358
     * @param array $fields  list of fields you need.
359
     * @param bool  $hydrate whether to hydrate object or not
360
     *
361
     * @return array|null|object
362
     */
363
    public function selectSingleFields($id, array $fields, $hydrate = true)
364
    {
365
        $builder = $this->repository->createQueryBuilder();
366
        $idField = $this->repository->getClassMetadata()->getIdentifier()[0];
367
368
        $record = $builder
369
            ->field($idField)->equals($id)
370
            ->select($fields)
371
            ->hydrate($hydrate)
372
            ->getQuery()
373
            ->getSingleResult();
374
375
        return $record;
376
    }
377
378
    /**
379
     * get classname of entity
380
     *
381
     * @return string
382
     */
383
    public function getEntityClass()
384
    {
385
        return $this->repository->getDocumentName();
386
    }
387
388
    /**
389
     * {@inheritDoc}
390
     *
391
     * Currently this is being used to build the route id used for redirecting
392
     * to newly made documents. It might benefit from having a different name
393
     * for those purposes.
394
     *
395
     * We might use a convention based mapping here:
396
     * Graviton\CoreBundle\Document\App -> mongodb://graviton_core
397
     * Graviton\CoreBundle\Entity\Table -> mysql://graviton_core
398
     *
399
     * @todo implement this in a more convention based manner
400
     *
401
     * @return string
402
     */
403
    public function getConnectionName()
404
    {
405
        $bundle = strtolower(substr(explode('\\', get_class($this))[1], 0, -6));
406
407
        return 'graviton.' . $bundle;
408
    }
409
410
    /**
411
     * Does the actual query using the RQL Bundle.
412
     *
413
     * @param Builder $queryBuilder Doctrine ODM QueryBuilder
414
     * @param Query   $query        query from parser
415
     *
416
     * @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...
417
     */
418
    protected function doRqlQuery($queryBuilder, Query $query)
419
    {
420
        $this->visitor->setBuilder($queryBuilder);
421
422
        return $this->visitor->visit($query);
423
    }
424
425
    /**
426
     * Checks the recordOrigin attribute of a record and will throw an exception if value is not allowed
427
     *
428
     * @param Object $record record
429
     *
430
     * @return void
431
     */
432
    protected function checkIfOriginRecord($record)
433
    {
434
        if ($record instanceof RecordOriginInterface
435
            && !$record->isRecordOriginModifiable()
436
        ) {
437
            $values = $this->notModifiableOriginRecords;
438
            $originValue = strtolower(trim($record->getRecordOrigin()));
439
440
            if (in_array($originValue, $values)) {
441
                $msg = sprintf("Must not be one of the following keywords: %s", implode(', ', $values));
442
443
                throw new RecordOriginModifiedException($msg);
444
            }
445
        }
446
    }
447
448
    /**
449
     * Determines the configured amount fo data records to be returned in pagination context.
450
     *
451
     * @return int
452
     */
453
    private function getDefaultLimit()
454
    {
455
        if (0 < $this->paginationDefaultLimit) {
456
            return $this->paginationDefaultLimit;
457
        }
458
459
        return 10;
460
    }
461
462
    /**
463
     * @param Boolean $active active
464
     * @param String  $field  field
465
     * @return void
466
     */
467
    public function setFilterByAuthUser($active, $field)
468
    {
469
        $this->filterByAuthUser = is_bool($active) ? $active : false;
470
        $this->filterByAuthField = $field;
471
    }
472
}
473