Completed
Push — feature/EVO-5751-text-index-mo... ( 6b6245 )
by
unknown
62:13
created

DocumentModel::updateRecord()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 7

Duplication

Lines 11
Ratio 100 %

Importance

Changes 0
Metric Value
dl 11
loc 11
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 7
nc 1
nop 2
1
<?php
2
/**
3
 * Use doctrine odm as backend
4
 */
5
6
namespace Graviton\RestBundle\Model;
7
8
use Doctrine\ODM\MongoDB\DocumentRepository;
9
use Graviton\RestBundle\Service\RqlTranslator;
10
use Graviton\SchemaBundle\Model\SchemaModel;
11
use Graviton\SecurityBundle\Entities\SecurityUser;
12
use Symfony\Bridge\Monolog\Logger;
13
use Symfony\Component\HttpFoundation\Request;
14
use Doctrine\ODM\MongoDB\Query\Builder;
15
use Graviton\Rql\Visitor\MongoOdm as Visitor;
16
use Xiag\Rql\Parser\Query;
17
use Graviton\ExceptionBundle\Exception\RecordOriginModifiedException;
18
use Xiag\Rql\Parser\Exception\SyntaxErrorException as RqlSyntaxErrorException;
19
use Graviton\SchemaBundle\Document\Schema as SchemaDocument;
20
21
/**
22
 * Use doctrine odm as backend
23
 *
24
 * @author  List of contributors <https://github.com/libgraviton/graviton/graphs/contributors>
25
 * @license http://opensource.org/licenses/gpl-license.php GNU Public License
26
 * @link    http://swisscom.ch
27
 */
28
class DocumentModel extends SchemaModel implements ModelInterface
29
{
30
    /**
31
     * @var string
32
     */
33
    protected $description;
34
    /**
35
     * @var string[]
36
     */
37
    protected $fieldTitles;
38
    /**
39
     * @var string[]
40
     */
41
    protected $fieldDescriptions;
42
    /**
43
     * @var string[]
44
     */
45
    protected $requiredFields = array();
46
    /**
47
     * @var string[]
48
     */
49
    protected $searchableFields = array();
50
    /**
51
     * @var DocumentRepository
52
     */
53
    private $repository;
54
    /**
55
     * @var Visitor
56
     */
57
    private $visitor;
58
    /**
59
     * @var array
60
     */
61
    protected $notModifiableOriginRecords;
62
    /**
63
     * @var  integer
64
     */
65
    private $paginationDefaultLimit;
66
67
    /**
68
     * @var  Logger
69
     */
70
    private $logger;
71
72
    /**
73
     * @var boolean
74
     */
75
    protected $filterByAuthUser;
76
77
    /**
78
     * @var string
79
     */
80
    protected $filterByAuthField;
81
82
    /**
83
     * @var RqlTranslator
84
     */
85
    protected $translator;
86
87
    /**
88
     * @param Visitor       $visitor                    rql query visitor
89
     * @param RqlTranslator $translator                 Translator for query modification
90
     * @param array         $notModifiableOriginRecords strings with not modifiable recordOrigin values
91
     * @param integer       $paginationDefaultLimit     amount of data records to be returned when in pagination context
92
     * @param Logger        $logger                     The defined system logger
93
     */
94
    public function __construct(
95
        Visitor $visitor,
96
        RqlTranslator $translator,
97
        $notModifiableOriginRecords,
98
        $paginationDefaultLimit,
99
        $logger
100
    ) {
101
        parent::__construct();
102
        $this->visitor = $visitor;
103
        $this->translator = $translator;
104
        $this->notModifiableOriginRecords = $notModifiableOriginRecords;
105
        $this->paginationDefaultLimit = (int) $paginationDefaultLimit;
106
        $this->logger = $logger;
107
    }
108
109
    /**
110
     * get repository instance
111
     *
112
     * @return DocumentRepository
113
     */
114
    public function getRepository()
115
    {
116
        return $this->repository;
117
    }
118
119
    /**
120
     * create new app model
121
     *
122
     * @param DocumentRepository $repository Repository of countries
123
     *
124
     * @return \Graviton\RestBundle\Model\DocumentModel
125
     */
126
    public function setRepository(DocumentRepository $repository)
127
    {
128
        $this->repository = $repository;
129
130
        return $this;
131
    }
132
133
    /**
134
     * {@inheritDoc}
135
     *
136
     * @param Request        $request The request object
137
     * @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...
138
     * @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...
139
     *
140
     * @return array
141
     */
142
    public function findAll(Request $request, SecurityUser $user = null, SchemaDocument $schema = null)
143
    {
144
        $pageNumber = $request->query->get('page', 1);
145
        $numberPerPage = (int) $request->query->get('perPage', $this->getDefaultLimit());
146
        $startAt = ($pageNumber - 1) * $numberPerPage;
147
148
        /** @var \Doctrine\ODM\MongoDB\Query\Builder $queryBuilder */
149
        $queryBuilder = $this->repository
150
            ->createQueryBuilder();
151
152
        if ($this->filterByAuthUser && $user && $user->hasRole(SecurityUser::ROLE_USER)) {
153
            $queryBuilder->field($this->filterByAuthField)->equals($user->getUser()->getId());
154
        }
155
156
157
        $searchableFields = $this->getSearchableFields();
158
        if (!is_null($schema)) {
159
            $searchableFields = $schema->getSearchable();
160
        }
161
162
        // *** do we have an RQL expression, do we need to filter data?
163
        if ($request->attributes->get('hasRql', false)) {
164
            $queryBuilder = $this->doRqlQuery(
165
                $queryBuilder,
166
                $this->translator->translateSearchQuery(
167
                    $request->attributes->get('rqlQuery'),
168
                    $searchableFields
0 ignored issues
show
Bug introduced by
It seems like $searchableFields defined by $schema->getSearchable() on line 159 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...
169
                )
170
            );
171
        } else {
172
            // @todo [lapistano]: seems the offset is missing for this query.
173
            /** @var \Doctrine\ODM\MongoDB\Query\Builder $qb */
174
            $queryBuilder->find($this->repository->getDocumentName());
175
        }
176
177
        // define offset and limit
178
        if (!array_key_exists('skip', $queryBuilder->getQuery()->getQuery())) {
179
            $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...
180
        } else {
181
            $startAt = (int) $queryBuilder->getQuery()->getQuery()['skip'];
182
        }
183
184
        if (!array_key_exists('limit', $queryBuilder->getQuery()->getQuery())) {
185
            $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...
186
        } else {
187
            $numberPerPage = (int) $queryBuilder->getQuery()->getQuery()['limit'];
188
        }
189
190
        // Limit can not be negative nor null.
191
        if ($numberPerPage < 1) {
192
            throw new RqlSyntaxErrorException('negative or null limit in rql');
193
        }
194
195
        /**
196
         * add a default sort on id if none was specified earlier
197
         *
198
         * not specifying something to sort on leads to very weird cases when fetching references
199
         */
200
        if (!array_key_exists('sort', $queryBuilder->getQuery()->getQuery())) {
201
            $queryBuilder->sort('_id');
202
        }
203
204
        // on search: check if there is a fulltextsearch Index defined, apply it and search in there
205
        if (strstr($request->attributes->get('rawRql'), 'search')) {
206
            preg_match('/search\((.*?)\)/', $request->attributes->get('rawRql'), $match);
207
            if (!empty($match) && strlen($match[1])) {
208
                // this is performing a fulltextsearch in the text-Index of the collection (mongodb Version>=2.6)
209
                if ((float) $this->getMongoDBVersion()>=2.6) {
210
                    // check if there is an index definition for fulltext-search in schema index and apply it.
211
                    if ($this->getSchema()->textSearchIndex && is_array($this->getSchema()->textSearchIndex)
212
                        && count($this->getSchema()->textSearchIndex)==2 ) {
213
                        if ($this->repository->getDocumentManager()->getDocumentCollection(
214
                            $this->repository->getClassName()
215
                        )->ensureIndex(
216
                            (array) $this->getSchema()->textSearchIndex[0],
217
                            (array) $this->getSchema()->textSearchIndex[1]
218
                        )) {
219
                            $queryBuilder->text($match[1]);
220
                        }
221
                    }
222
                } else {
223
                    $this->logger->addNotice(
224
                        "Couldn't create text Index for Collection ".$this->repository->getClassName()
225
                        .". MongoDB Version < 2.6 (".$this->getMongoDBVersion().")"
226
                    );
227
                }
228
            }
229
        }
230
231
        // run query
232
        $query = $queryBuilder->getQuery();
233
        $records = array_values($query->execute()->toArray());
234
235
        $totalCount = $query->count();
236
        $numPages = (int) ceil($totalCount / $numberPerPage);
237
        $page = (int) ceil($startAt / $numberPerPage) + 1;
238
        if ($numPages > 1) {
239
            $request->attributes->set('paging', true);
240
            $request->attributes->set('page', $page);
241
            $request->attributes->set('numPages', $numPages);
242
            $request->attributes->set('startAt', $startAt);
243
            $request->attributes->set('perPage', $numberPerPage);
244
            $request->attributes->set('totalCount', $totalCount);
245
        }
246
247
        return $records;
248
    }
249
250
    /**
251
     * @return string the version of the MongoDB as a string
252
     */
253
    public function getMongoDBVersion()
254
    {
255
        $buildInfo = $this->repository->getDocumentManager()->getDocumentDatabase(
256
            $this->repository->getClassName()
257
        )->command(['buildinfo'=>1]);
258
        if (isset($buildInfo['version'])) {
259
            return $buildInfo['version'];
260
        } else {
261
            return "unkown";
262
        }
263
    }
264
265
    /**
266
     * @param \Graviton\I18nBundle\Document\Translatable $entity entity to insert
267
     *
268
     * @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...
269
     */
270 View Code Duplication
    public function insertRecord($entity)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
271
    {
272
        $this->checkIfOriginRecord($entity);
273
        $manager = $this->repository->getDocumentManager();
274
        $manager->persist($entity);
275
        $manager->flush($entity);
276
277
        return $this->find($entity->getId());
278
    }
279
280
    /**
281
     * @param string $documentId id of entity to find
282
     *
283
     * @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...
284
     */
285
    public function find($documentId)
286
    {
287
        return $this->repository->find($documentId);
288
    }
289
290
    /**
291
     * {@inheritDoc}
292
     *
293
     * @param string $documentId id of entity to update
294
     * @param Object $entity     new entity
295
     *
296
     * @return Object
297
     */
298 View Code Duplication
    public function updateRecord($documentId, $entity)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
299
    {
300
        $manager = $this->repository->getDocumentManager();
301
        // In both cases the document attribute named originRecord must not be 'core'
302
        $this->checkIfOriginRecord($entity);
303
        $this->checkIfOriginRecord($this->find($documentId));
0 ignored issues
show
Bug introduced by
It seems like $this->find($documentId) targeting Graviton\RestBundle\Model\DocumentModel::find() can also be of type 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...
304
        $entity = $manager->merge($entity);
305
        $manager->flush();
306
307
        return $entity;
308
    }
309
310
    /**
311
     * {@inheritDoc}
312
     *
313
     * @param string $documentId id of entity to delete
314
     *
315
     * @return null|Object
316
     */
317
    public function deleteRecord($documentId)
318
    {
319
        $manager = $this->repository->getDocumentManager();
320
        $entity = $this->find($documentId);
321
322
        $return = $entity;
323
        if ($entity) {
324
            $this->checkIfOriginRecord($entity);
325
            $manager->remove($entity);
326
            $manager->flush();
327
            $return = null;
328
        }
329
330
        return $return;
331
    }
332
333
    /**
334
     * get classname of entity
335
     *
336
     * @return string
337
     */
338
    public function getEntityClass()
339
    {
340
        return $this->repository->getDocumentName();
341
    }
342
343
    /**
344
     * {@inheritDoc}
345
     *
346
     * Currently this is being used to build the route id used for redirecting
347
     * to newly made documents. It might benefit from having a different name
348
     * for those purposes.
349
     *
350
     * We might use a convention based mapping here:
351
     * Graviton\CoreBundle\Document\App -> mongodb://graviton_core
352
     * Graviton\CoreBundle\Entity\Table -> mysql://graviton_core
353
     *
354
     * @todo implement this in a more convention based manner
355
     *
356
     * @return string
357
     */
358
    public function getConnectionName()
359
    {
360
        $bundle = strtolower(substr(explode('\\', get_class($this))[1], 0, -6));
361
362
        return 'graviton.' . $bundle;
363
    }
364
365
    /**
366
     * Does the actual query using the RQL Bundle.
367
     *
368
     * @param Builder $queryBuilder Doctrine ODM QueryBuilder
369
     * @param Query   $query        query from parser
370
     *
371
     * @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...
372
     */
373
    protected function doRqlQuery($queryBuilder, Query $query)
374
    {
375
        $this->visitor->setBuilder($queryBuilder);
376
377
        return $this->visitor->visit($query);
378
    }
379
380
    /**
381
     * Checks the recordOrigin attribute of a record and will throw an exception if value is not allowed
382
     *
383
     * @param Object $record record
384
     *
385
     * @return void
386
     */
387
    protected function checkIfOriginRecord($record)
388
    {
389
        if ($record instanceof RecordOriginInterface
390
            && !$record->isRecordOriginModifiable()
391
        ) {
392
            $values = $this->notModifiableOriginRecords;
393
            $originValue = strtolower(trim($record->getRecordOrigin()));
394
395
            if (in_array($originValue, $values)) {
396
                $msg = sprintf("Must not be one of the following keywords: %s", implode(', ', $values));
397
398
                throw new RecordOriginModifiedException($msg);
399
            }
400
        }
401
    }
402
403
    /**
404
     * Determines the configured amount fo data records to be returned in pagination context.
405
     *
406
     * @return int
407
     */
408
    private function getDefaultLimit()
409
    {
410
        if (0 < $this->paginationDefaultLimit) {
411
            return $this->paginationDefaultLimit;
412
        }
413
414
        return 10;
415
    }
416
417
    /**
418
     * @param Boolean $active active
419
     * @param String  $field  field
420
     * @return void
421
     */
422
    public function setFilterByAuthUser($active, $field)
423
    {
424
        $this->filterByAuthUser = is_bool($active) ? $active : false;
425
        $this->filterByAuthField = $field;
426
    }
427
}
428