Completed
Push — feature/EVO-5751-text-search-j... ( 978d8e...b40468 )
by
unknown
134:41 queued 69:47
created

DocumentModel::getConnectionName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

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 6
ccs 0
cts 1
cp 0
rs 9.4285
cc 1
eloc 3
nc 1
nop 0
crap 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\Rql\Node\SearchNode;
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\Node\Query\AbstractLogicOperatorNode;
18
use Xiag\Rql\Parser\Node\Query\AbstractScalarOperatorNode;
19
use Xiag\Rql\Parser\Query;
20
use Graviton\ExceptionBundle\Exception\RecordOriginModifiedException;
21
use Xiag\Rql\Parser\Exception\SyntaxErrorException as RqlSyntaxErrorException;
22
use Graviton\SchemaBundle\Document\Schema as SchemaDocument;
23
use Xiag\Rql\Parser\Query as XiagQuery;
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
     * @param Visitor       $visitor                    rql query visitor
88
     * @param RqlTranslator $translator                 Translator for query modification
89
     * @param array         $notModifiableOriginRecords strings with not modifiable recordOrigin values
90
     * @param integer       $paginationDefaultLimit     amount of data records to be returned when in pagination context
91
     */
92
    public function __construct(
93
        Visitor $visitor,
94
        RqlTranslator $translator,
95
        $notModifiableOriginRecords,
96
        $paginationDefaultLimit
97
    ) {
98 4
        parent::__construct();
99
        $this->visitor = $visitor;
100
        $this->translator = $translator;
101
        $this->notModifiableOriginRecords = $notModifiableOriginRecords;
102
        $this->paginationDefaultLimit = (int) $paginationDefaultLimit;
103
    }
104 4
105 4
    /**
106 4
     * get repository instance
107 4
     *
108 4
     * @return DocumentRepository
109 4
     */
110
    public function getRepository()
111
    {
112
        return $this->repository;
113
    }
114
115
    /**
116 2
     * create new app model
117
     *
118 2
     * @param DocumentRepository $repository Repository of countries
119
     *
120
     * @return \Graviton\RestBundle\Model\DocumentModel
121
     */
122
    public function setRepository(DocumentRepository $repository)
123
    {
124
        $this->repository = $repository;
125
126
        return $this;
127
    }
128 4
129
    /**
130 4
     * {@inheritDoc}
131 4
     *
132
     * @param Request        $request The request object
133 4
     * @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...
134
     * @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...
135
     *
136
     * @return array
137
     */
138
    public function findAll(Request $request, SecurityUser $user = null, SchemaDocument $schema = null)
139
    {
140
        $pageNumber = $request->query->get('page', 1);
141
        $numberPerPage = (int) $request->query->get('perPage', $this->getDefaultLimit());
142
        $startAt = ($pageNumber - 1) * $numberPerPage;
143
        // Only 1 search text node allowed.
144
        $hasSearch = false;
145
146
        /** @var \Doctrine\ODM\MongoDB\Query\Builder $queryBuilder */
147
        $queryBuilder = $this->repository
148
            ->createQueryBuilder();
149
150
        if ($this->filterByAuthUser && $user && $user->hasRole(SecurityUser::ROLE_USER)) {
151
            $queryBuilder->field($this->filterByAuthField)->equals($user->getUser()->getId());
152
        }
153
154
        // *** do we have an RQL expression, do we need to filter data?
155
        if ($request->attributes->get('hasRql', false)) {
156
            $innerQuery = $request->attributes->get('rqlQuery')->getQuery();
157
            $xiagQuery = new XiagQuery();
158
            // can we perform a search in an index instead of filtering?
159
            if ($innerQuery instanceof AbstractLogicOperatorNode) {
160
                foreach ($innerQuery->getQueries() as $innerRql) {
161
                    if (!$hasSearch && $innerRql instanceof SearchNode) {
162
                        $searchString = implode('&', $innerRql->getSearchTerms());
163
                        $queryBuilder->addAnd(
164
                            $queryBuilder->expr()->text($searchString)
165
                        );
166
                        $hasSearch = true;
167
                    } else {
168
                        $xiagQuery->setQuery($innerRql);
169
                    }
170
                }
171
            } elseif ($this->hasCustomSearchIndex() && ($innerQuery instanceof SearchNode)) {
172
                $searchString = implode('&', $innerQuery->getSearchTerms());
173
                $queryBuilder->addAnd(
174
                    $queryBuilder->expr()->text($searchString)
175
                );
176
                $hasSearch = true;
177
            } else {
178
                if ($innerQuery instanceof AbstractScalarOperatorNode) {
179
                    $xiagQuery->setQuery($innerQuery);
180
                } else {
181
                    /** @var AbstractLogicOperatorNode $innerQuery */
182
                    foreach ($innerQuery->getQueries() as $innerRql) {
183
                        if (!$innerRql instanceof SearchNode) {
184
                            $xiagQuery->setQuery($innerRql);
185
                        }
186
                    }
187
                }
188
            }
189
190
            $queryBuilder = $this->doRqlQuery(
191
                $queryBuilder,
192
                $xiagQuery
193
            );
194
        } else {
195
            // @todo [lapistano]: seems the offset is missing for this query.
196
            /** @var \Doctrine\ODM\MongoDB\Query\Builder $qb */
197
            $queryBuilder->find($this->repository->getDocumentName());
198
        }
199
200
        // define offset and limit
201
        if (!array_key_exists('skip', $queryBuilder->getQuery()->getQuery())) {
202
            $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...
203
        } else {
204
            $startAt = (int) $queryBuilder->getQuery()->getQuery()['skip'];
205
        }
206
207
        if (!array_key_exists('limit', $queryBuilder->getQuery()->getQuery())) {
208
            $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...
209
        } else {
210
            $numberPerPage = (int) $queryBuilder->getQuery()->getQuery()['limit'];
211
        }
212
213
        // Limit can not be negative nor null.
214
        if ($numberPerPage < 1) {
215
            throw new RqlSyntaxErrorException('negative or null limit in rql');
216
        }
217
218
        /**
219
         * add a default sort on id if none was specified earlier
220
         *
221
         * not specifying something to sort on leads to very weird cases when fetching references
222
         * If search node, sort by Score
223
         * TODO Review this sorting, not 100% sure
224
         */
225
        if ($hasSearch && !array_key_exists('sort', $queryBuilder->getQuery()->getQuery())) {
226
            $queryBuilder->sortMeta('score', 'textScore');
0 ignored issues
show
Bug introduced by
The method sortMeta 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...
227
        } elseif (!array_key_exists('sort', $queryBuilder->getQuery()->getQuery())) {
228
            $queryBuilder->sort('_id');
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
     * @param string $prefix the prefix for custom text search indexes
252
     * @return bool
253
     * @throws \Doctrine\ODM\MongoDB\MongoDBException
254
     */
255
    private function hasCustomSearchIndex($prefix = 'search')
256
    {
257
        $collection = $this->repository->getDocumentManager()->getDocumentCollection($this->repository->getClassName());
258
        $indexesInfo = $collection->getIndexInfo();
259
        foreach ($indexesInfo as $indexInfo) {
260
            if ($indexInfo['name']==$prefix.$collection->getName().'Index') {
261
                return true;
262
            }
263
        }
264
        return false;
265
    }
266
267
    /**
268
     * @return string the version of the MongoDB as a string
269
     */
270
    private function getMongoDBVersion()
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
271
    {
272
        $buildInfo = $this->repository->getDocumentManager()->getDocumentDatabase(
273
            $this->repository->getClassName()
274
        )->command(['buildinfo'=>1]);
275
        if (isset($buildInfo['version'])) {
276
            return $buildInfo['version'];
277
        } else {
278
            return "unkown";
279
        }
280
    }
281
282
    /**
283
     * @param \Graviton\I18nBundle\Document\Translatable $entity entity to insert
284
     *
285
     * @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...
286
     */
287 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...
288
    {
289
        $this->checkIfOriginRecord($entity);
290
        $manager = $this->repository->getDocumentManager();
291
        $manager->persist($entity);
292
        $manager->flush($entity);
293
294
        return $this->find($entity->getId());
295
    }
296
297
    /**
298
     * @param string $documentId id of entity to find
299
     *
300
     * @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...
301
     */
302
    public function find($documentId)
303
    {
304
        return $this->repository->find($documentId);
305
    }
306
307
    /**
308
     * {@inheritDoc}
309
     *
310
     * @param string $documentId id of entity to update
311
     * @param Object $entity     new entity
312
     *
313
     * @return Object
314 4
     */
315 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...
316 4
    {
317
        $manager = $this->repository->getDocumentManager();
318
        // In both cases the document attribute named originRecord must not be 'core'
319
        $this->checkIfOriginRecord($entity);
320
        $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...
321
        $entity = $manager->merge($entity);
322
        $manager->flush();
323
324
        return $entity;
325
    }
326
327
    /**
328 2
     * {@inheritDoc}
329
     *
330
     * @param string $documentId id of entity to delete
331 2
     *
332 2
     * @return null|Object
333
     */
334 2
    public function deleteRecord($documentId)
335 2
    {
336
        $manager = $this->repository->getDocumentManager();
337 2
        $entity = $this->find($documentId);
338 2
339 1
        $return = $entity;
340
        if ($entity) {
341 2
            $this->checkIfOriginRecord($entity);
342
            $manager->remove($entity);
343 2
            $manager->flush();
344 2
            $return = null;
345
        }
346 2
347
        return $return;
348
    }
349 2
350
    /**
351
     * get classname of entity
352
     *
353
     * @return string|null
354
     */
355
    public function getEntityClass()
356
    {
357
        if ($this->repository instanceof DocumentRepository) {
358
            return $this->repository->getDocumentName();
359
        }
360
361
        return null;
362
    }
363
364
    /**
365
     * {@inheritDoc}
366
     *
367
     * Currently this is being used to build the route id used for redirecting
368
     * to newly made documents. It might benefit from having a different name
369
     * for those purposes.
370
     *
371
     * We might use a convention based mapping here:
372
     * Graviton\CoreBundle\Document\App -> mongodb://graviton_core
373
     * Graviton\CoreBundle\Entity\Table -> mysql://graviton_core
374
     *
375
     * @todo implement this in a more convention based manner
376
     *
377
     * @return string
378
     */
379
    public function getConnectionName()
380
    {
381
        $bundle = strtolower(substr(explode('\\', get_class($this))[1], 0, -6));
382
383
        return 'graviton.' . $bundle;
384
    }
385
386
    /**
387
     * Does the actual query using the RQL Bundle.
388
     *
389
     * @param Builder $queryBuilder Doctrine ODM QueryBuilder
390
     * @param Query   $query        query from parser
391
     *
392
     * @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...
393
     */
394
    protected function doRqlQuery($queryBuilder, Query $query)
395
    {
396 2
        $this->visitor->setBuilder($queryBuilder);
397
398 2
        return $this->visitor->visit($query);
399
    }
400 2
401 2
    /**
402 2
     * Checks the recordOrigin attribute of a record and will throw an exception if value is not allowed
403 2
     *
404 2
     * @param Object $record record
405
     *
406
     * @return void
407
     */
408
    protected function checkIfOriginRecord($record)
409
    {
410
        if ($record instanceof RecordOriginInterface
411
            && !$record->isRecordOriginModifiable()
412
        ) {
413 4
            $values = $this->notModifiableOriginRecords;
414
            $originValue = strtolower(trim($record->getRecordOrigin()));
415 4
416
            if (in_array($originValue, $values)) {
417
                $msg = sprintf("Must not be one of the following keywords: %s", implode(', ', $values));
418
419
                throw new RecordOriginModifiedException($msg);
420
            }
421
        }
422
    }
423
424
    /**
425
     * Determines the configured amount fo data records to be returned in pagination context.
426
     *
427
     * @return int
428
     */
429
    private function getDefaultLimit()
430
    {
431 4
        if (0 < $this->paginationDefaultLimit) {
432
            return $this->paginationDefaultLimit;
433 4
        }
434 4
435
        return 10;
436
    }
437 4
438 4
    /**
439 4
     * @param Boolean $active active
440 4
     * @param String  $field  field
441 4
     * @return void
442
     */
443 4
    public function setFilterByAuthUser($active, $field)
444
    {
445
        $this->filterByAuthUser = is_bool($active) ? $active : false;
446
        $this->filterByAuthField = $field;
447
    }
448
}
449