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

DocumentModel::getRepository()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
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 (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
                    $queryBuilder->text($match[1]);
212
                } else {
213
                    $this->logger->addNotice(
214
                        "Couldn't create text Index for Collection ".$this->repository->getClassName()
215
                        .". MongoDB Version < 2.6 (".$this->getMongoDBVersion().")"
216
                    );
217
                }
218
            }
219
        }
220
221
        // run query
222
        $query = $queryBuilder->getQuery();
223
        $records = array_values($query->execute()->toArray());
224
225
        $totalCount = $query->count();
226
        $numPages = (int) ceil($totalCount / $numberPerPage);
227
        $page = (int) ceil($startAt / $numberPerPage) + 1;
228
        if ($numPages > 1) {
229
            $request->attributes->set('paging', true);
230
            $request->attributes->set('page', $page);
231
            $request->attributes->set('numPages', $numPages);
232
            $request->attributes->set('startAt', $startAt);
233
            $request->attributes->set('perPage', $numberPerPage);
234
            $request->attributes->set('totalCount', $totalCount);
235
        }
236
237
        return $records;
238
    }
239
240
    
241
242
    /**
243
     * @return string the version of the MongoDB as a string
244
     */
245
    public function getMongoDBVersion()
246
    {
247
        $buildInfo = $this->repository->getDocumentManager()->getDocumentDatabase(
248
            $this->repository->getClassName()
249
        )->command(['buildinfo'=>1]);
250
        if (isset($buildInfo['version'])) {
251
            return $buildInfo['version'];
252
        } else {
253
            return "unkown";
254
        }
255
    }
256
257
    /**
258
     * @param \Graviton\I18nBundle\Document\Translatable $entity entity to insert
259
     *
260
     * @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...
261
     */
262 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...
263
    {
264
        $this->checkIfOriginRecord($entity);
265
        $manager = $this->repository->getDocumentManager();
266
        $manager->persist($entity);
267
        $manager->flush($entity);
268
269
        return $this->find($entity->getId());
270
    }
271
272
    /**
273
     * @param string $documentId id of entity to find
274
     *
275
     * @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...
276
     */
277
    public function find($documentId)
278
    {
279
        return $this->repository->find($documentId);
280
    }
281
282
    /**
283
     * {@inheritDoc}
284
     *
285
     * @param string $documentId id of entity to update
286
     * @param Object $entity     new entity
287
     *
288
     * @return Object
289
     */
290 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...
291
    {
292
        $manager = $this->repository->getDocumentManager();
293
        // In both cases the document attribute named originRecord must not be 'core'
294
        $this->checkIfOriginRecord($entity);
295
        $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...
296
        $entity = $manager->merge($entity);
297
        $manager->flush();
298
299
        return $entity;
300
    }
301
302
    /**
303
     * {@inheritDoc}
304
     *
305
     * @param string $documentId id of entity to delete
306
     *
307
     * @return null|Object
308
     */
309
    public function deleteRecord($documentId)
310
    {
311
        $manager = $this->repository->getDocumentManager();
312
        $entity = $this->find($documentId);
313
314
        $return = $entity;
315
        if ($entity) {
316
            $this->checkIfOriginRecord($entity);
317
            $manager->remove($entity);
318
            $manager->flush();
319
            $return = null;
320
        }
321
322
        return $return;
323
    }
324
325
    /**
326
     * get classname of entity
327
     *
328
     * @return string
329
     */
330
    public function getEntityClass()
331
    {
332
        return $this->repository->getDocumentName();
333
    }
334
335
    /**
336
     * {@inheritDoc}
337
     *
338
     * Currently this is being used to build the route id used for redirecting
339
     * to newly made documents. It might benefit from having a different name
340
     * for those purposes.
341
     *
342
     * We might use a convention based mapping here:
343
     * Graviton\CoreBundle\Document\App -> mongodb://graviton_core
344
     * Graviton\CoreBundle\Entity\Table -> mysql://graviton_core
345
     *
346
     * @todo implement this in a more convention based manner
347
     *
348
     * @return string
349
     */
350
    public function getConnectionName()
351
    {
352
        $bundle = strtolower(substr(explode('\\', get_class($this))[1], 0, -6));
353
354
        return 'graviton.' . $bundle;
355
    }
356
357
    /**
358
     * Does the actual query using the RQL Bundle.
359
     *
360
     * @param Builder $queryBuilder Doctrine ODM QueryBuilder
361
     * @param Query   $query        query from parser
362
     *
363
     * @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...
364
     */
365
    protected function doRqlQuery($queryBuilder, Query $query)
366
    {
367
        $this->visitor->setBuilder($queryBuilder);
368
369
        return $this->visitor->visit($query);
370
    }
371
372
    /**
373
     * Checks the recordOrigin attribute of a record and will throw an exception if value is not allowed
374
     *
375
     * @param Object $record record
376
     *
377
     * @return void
378
     */
379
    protected function checkIfOriginRecord($record)
380
    {
381
        if ($record instanceof RecordOriginInterface
382
            && !$record->isRecordOriginModifiable()
383
        ) {
384
            $values = $this->notModifiableOriginRecords;
385
            $originValue = strtolower(trim($record->getRecordOrigin()));
386
387
            if (in_array($originValue, $values)) {
388
                $msg = sprintf("Must not be one of the following keywords: %s", implode(', ', $values));
389
390
                throw new RecordOriginModifiedException($msg);
391
            }
392
        }
393
    }
394
395
    /**
396
     * Determines the configured amount fo data records to be returned in pagination context.
397
     *
398
     * @return int
399
     */
400
    private function getDefaultLimit()
401
    {
402
        if (0 < $this->paginationDefaultLimit) {
403
            return $this->paginationDefaultLimit;
404
        }
405
406
        return 10;
407
    }
408
409
    /**
410
     * @param Boolean $active active
411
     * @param String  $field  field
412
     * @return void
413
     */
414
    public function setFilterByAuthUser($active, $field)
415
    {
416
        $this->filterByAuthUser = is_bool($active) ? $active : false;
417
        $this->filterByAuthField = $field;
418
    }
419
}
420