Completed
Push — master ( 11b317...37df4d )
by Lucas
09:27
created

DocumentModel::findAll()   D

Complexity

Conditions 11
Paths 160

Size

Total Lines 80
Code Lines 44

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 132

Importance

Changes 3
Bugs 0 Features 0
Metric Value
c 3
b 0
f 0
dl 0
loc 80
rs 4.9629
ccs 0
cts 49
cp 0
cc 11
eloc 44
nc 160
nop 3
crap 132

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\Component\HttpFoundation\Request;
14
use Doctrine\ODM\MongoDB\Query\Builder;
15
use Graviton\Rql\Visitor\MongoOdm as Visitor;
16
use Xiag\Rql\Parser\Node\Query\LogicOperator\AndNode;
17
use Xiag\Rql\Parser\Node\Query\LogicOperator\OrNode;
18
use Xiag\Rql\Parser\Node\Query\ScalarOperator\LikeNode;
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
24
/**
25
 * Use doctrine odm as backend
26
 *
27
 * @author  List of contributors <https://github.com/libgraviton/graviton/graphs/contributors>
28
 * @license http://opensource.org/licenses/gpl-license.php GNU Public License
29
 * @link    http://swisscom.ch
30
 */
31
class DocumentModel extends SchemaModel implements ModelInterface
32
{
33
    /**
34
     * @var string
35
     */
36
    protected $description;
37
    /**
38
     * @var string[]
39
     */
40
    protected $fieldTitles;
41
    /**
42
     * @var string[]
43
     */
44
    protected $fieldDescriptions;
45
    /**
46
     * @var string[]
47
     */
48
    protected $requiredFields = array();
49
    /**
50
     * @var string[]
51
     */
52
    protected $searchableFields = array();
53
    /**
54
     * @var DocumentRepository
55
     */
56
    private $repository;
57
    /**
58
     * @var Visitor
59
     */
60
    private $visitor;
61
    /**
62
     * @var array
63
     */
64
    protected $notModifiableOriginRecords;
65
    /**
66
     * @var  integer
67
     */
68
    private $paginationDefaultLimit;
69
70
    /**
71
     * @var boolean
72
     */
73
    protected $filterByAuthUser;
74
75
    /**
76
     * @var string
77
     */
78
    protected $filterByAuthField;
79
80
    /**
81
     * @var RqlTranslator
82
     */
83
    protected $translator;
84
85
    /**
86
     * @param Visitor       $visitor                    rql query visitor
87
     * @param RqlTranslator $translator                 Translator for query modification
88
     * @param array         $notModifiableOriginRecords strings with not modifiable recordOrigin values
89
     * @param integer       $paginationDefaultLimit     amount of data records to be returned when in pagination context
90
     */
91
    public function __construct(
92
        Visitor $visitor,
93
        RqlTranslator $translator,
94
        $notModifiableOriginRecords,
95
        $paginationDefaultLimit
96
    ) {
97
        parent::__construct();
98
        $this->visitor = $visitor;
99
        $this->translator = $translator;
100
        $this->notModifiableOriginRecords = $notModifiableOriginRecords;
101
        $this->paginationDefaultLimit = (int) $paginationDefaultLimit;
102
    }
103
104
    /**
105
     * get repository instance
106
     *
107
     * @return DocumentRepository
108
     */
109
    public function getRepository()
110
    {
111
        return $this->repository;
112
    }
113
114
    /**
115
     * create new app model
116
     *
117
     * @param DocumentRepository $repository Repository of countries
118
     *
119
     * @return \Graviton\RestBundle\Model\DocumentModel
120
     */
121
    public function setRepository(DocumentRepository $repository)
122
    {
123
        $this->repository = $repository;
124
125
        return $this;
126
    }
127
128
    /**
129
     * {@inheritDoc}
130
     *
131
     * @param Request        $request The request object
132
     * @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...
133
     * @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...
134
     *
135
     * @return array
136
     */
137
    public function findAll(Request $request, SecurityUser $user = null, SchemaDocument $schema = null)
138
    {
139
        $pageNumber = $request->query->get('page', 1);
140
        $numberPerPage = (int) $request->query->get('perPage', $this->getDefaultLimit());
141
        $startAt = ($pageNumber - 1) * $numberPerPage;
142
143
        /** @var \Doctrine\ODM\MongoDB\Query\Builder $queryBuilder */
144
        $queryBuilder = $this->repository
145
            ->createQueryBuilder();
146
147
        if ($this->filterByAuthUser && $user && $user->hasRole(SecurityUser::ROLE_USER)) {
148
            $queryBuilder->field($this->filterByAuthField)->equals($user->getUser()->getId());
149
        }
150
151
152
        $searchableFields = $this->getSearchableFields();
153
        if (!is_null($schema)) {
154
            $searchableFields = $schema->getSearchable();
155
        }
156
157
        // *** do we have an RQL expression, do we need to filter data?
158
        if ($request->attributes->get('hasRql', false)) {
159
            $queryBuilder = $this->doRqlQuery(
160
                $queryBuilder,
161
                $this->translator->translateSearchQuery(
162
                    $request->attributes->get('rqlQuery'),
163
                    $searchableFields
0 ignored issues
show
Bug introduced by
It seems like $searchableFields defined by $schema->getSearchable() on line 154 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...
164
                )
165
            );
166
        } else {
167
            // @todo [lapistano]: seems the offset is missing for this query.
168
            /** @var \Doctrine\ODM\MongoDB\Query\Builder $qb */
169
            $queryBuilder->find($this->repository->getDocumentName());
170
        }
171
172
        // define offset and limit
173
        if (!array_key_exists('skip', $queryBuilder->getQuery()->getQuery())) {
174
            $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...
175
        } else {
176
            $startAt = (int) $queryBuilder->getQuery()->getQuery()['skip'];
177
        }
178
179
        if (!array_key_exists('limit', $queryBuilder->getQuery()->getQuery())) {
180
            $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...
181
        } else {
182
            $numberPerPage = (int) $queryBuilder->getQuery()->getQuery()['limit'];
183
        }
184
185
        // Limit can not be negative nor null.
186
        if ($numberPerPage < 1) {
187
            throw new RqlSyntaxErrorException('negative or null limit in rql');
188
        }
189
190
        /**
191
         * add a default sort on id if none was specified earlier
192
         *
193
         * not specifying something to sort on leads to very weird cases when fetching references
194
         */
195
        if (!array_key_exists('sort', $queryBuilder->getQuery()->getQuery())) {
196
            $queryBuilder->sort('_id');
197
        }
198
199
        // run query
200
        $query = $queryBuilder->getQuery();
201
        $records = array_values($query->execute()->toArray());
202
203
        $totalCount = $query->count();
204
        $numPages = (int) ceil($totalCount / $numberPerPage);
205
        $page = (int) ceil($startAt / $numberPerPage) + 1;
206
        if ($numPages > 1) {
207
            $request->attributes->set('paging', true);
208
            $request->attributes->set('page', $page);
209
            $request->attributes->set('numPages', $numPages);
210
            $request->attributes->set('startAt', $startAt);
211
            $request->attributes->set('perPage', $numberPerPage);
212
            $request->attributes->set('totalCount', $totalCount);
213
        }
214
215
        return $records;
216
    }
217
218
    /**
219
     * @param \Graviton\I18nBundle\Document\Translatable $entity entity to insert
220
     *
221
     * @return Object
222
     */
223 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...
224
    {
225
        $this->checkIfOriginRecord($entity);
226
        $manager = $this->repository->getDocumentManager();
227
        $manager->persist($entity);
228
        $manager->flush($entity);
229
230
        return $this->find($entity->getId());
231
    }
232
233
    /**
234
     * @param string $documentId id of entity to find
235
     *
236
     * @return Object
237
     */
238
    public function find($documentId)
239
    {
240
        return $this->repository->find($documentId);
241
    }
242
243
    /**
244
     * {@inheritDoc}
245
     *
246
     * @param string $documentId id of entity to update
247
     * @param Object $entity     new entity
248
     *
249
     * @return Object
250
     */
251 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...
252
    {
253
        $manager = $this->repository->getDocumentManager();
254
        // In both cases the document attribute named originRecord must not be 'core'
255
        $this->checkIfOriginRecord($entity);
256
        $this->checkIfOriginRecord($this->find($documentId));
257
        $entity = $manager->merge($entity);
258
        $manager->flush();
259
260
        return $entity;
261
    }
262
263
    /**
264
     * {@inheritDoc}
265
     *
266
     * @param string $documentId id of entity to delete
267
     *
268
     * @return null|Object
269
     */
270
    public function deleteRecord($documentId)
271
    {
272
        $manager = $this->repository->getDocumentManager();
273
        $entity = $this->find($documentId);
274
275
        $return = $entity;
276
        if ($entity) {
277
            $this->checkIfOriginRecord($entity);
278
            $manager->remove($entity);
279
            $manager->flush();
280
            $return = null;
281
        }
282
283
        return $return;
284
    }
285
286
    /**
287
     * get classname of entity
288
     *
289
     * @return string
290
     */
291
    public function getEntityClass()
292
    {
293
        return $this->repository->getDocumentName();
294
    }
295
296
    /**
297
     * {@inheritDoc}
298
     *
299
     * Currently this is being used to build the route id used for redirecting
300
     * to newly made documents. It might benefit from having a different name
301
     * for those purposes.
302
     *
303
     * We might use a convention based mapping here:
304
     * Graviton\CoreBundle\Document\App -> mongodb://graviton_core
305
     * Graviton\CoreBundle\Entity\Table -> mysql://graviton_core
306
     *
307
     * @todo implement this in a more convention based manner
308
     *
309
     * @return string
310
     */
311
    public function getConnectionName()
312
    {
313
        $bundle = strtolower(substr(explode('\\', get_class($this))[1], 0, -6));
314
315
        return 'graviton.' . $bundle;
316
    }
317
318
    /**
319
     * Does the actual query using the RQL Bundle.
320
     *
321
     * @param Builder $queryBuilder Doctrine ODM QueryBuilder
322
     * @param Query   $query        query from parser
323
     *
324
     * @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...
325
     */
326
    protected function doRqlQuery($queryBuilder, Query $query)
327
    {
328
        $this->visitor->setBuilder($queryBuilder);
329
330
        return $this->visitor->visit($query);
331
    }
332
333
    /**
334
     * Checks the recordOrigin attribute of a record and will throw an exception if value is not allowed
335
     *
336
     * @param Object $record record
337
     *
338
     * @return void
339
     */
340 12
    protected function checkIfOriginRecord($record)
341
    {
342 6
        if ($record instanceof RecordOriginInterface
343 12
            && !$record->isRecordOriginModifiable()
344 6
        ) {
345 6
            $values = $this->notModifiableOriginRecords;
346 6
            $originValue = strtolower(trim($record->getRecordOrigin()));
347
348 6
            if (in_array($originValue, $values)) {
349 2
                $msg = sprintf("Must not be one of the following keywords: %s", implode(', ', $values));
350
351 2
                throw new RecordOriginModifiedException($msg);
352
            }
353 2
        }
354 10
    }
355
356
    /**
357
     * Determines the configured amount fo data records to be returned in pagination context.
358
     *
359
     * @return int
360
     */
361
    private function getDefaultLimit()
362
    {
363
        if (0 < $this->paginationDefaultLimit) {
364
            return $this->paginationDefaultLimit;
365
        }
366
367
        return 10;
368
    }
369
370
    /**
371
     * @param Boolean $active active
372
     * @param String  $field  field
373
     * @return void
374
     */
375
    public function setFilterByAuthUser($active, $field)
376
    {
377
        $this->filterByAuthUser = is_bool($active) ? $active : false;
378
        $this->filterByAuthField = $field;
379
    }
380
}
381