Completed
Pull Request — master (#5)
by Jacob
03:37
created

Query   A

Complexity

Total Complexity 28

Size/Duplication

Total Lines 270
Duplicated Lines 7.78 %

Coupling/Cohesion

Components 1
Dependencies 8

Importance

Changes 1
Bugs 0 Features 1
Metric Value
wmc 28
c 1
b 0
f 1
lcom 1
cbo 8
dl 21
loc 270
rs 10

14 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A createQueryBuilder() 0 4 1
A executeDelete() 10 10 1
A executeFind() 0 15 1
A executeInsert() 0 9 1
A executeUpdate() 11 11 1
A getFormatter() 0 4 1
A getInverseCriteria() 0 16 3
A getModelCollection() 0 7 2
A getRetrieveCritiera() 0 14 4
A appendFields() 0 9 3
A appendLimitAndOffset() 0 14 3
A appendSort() 0 7 2
A prepareFields() 0 15 4

How to fix   Duplicated Code   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

1
<?php
2
3
namespace As3\Modlr\Persister\MongoDb;
4
5
use As3\Modlr\Metadata\EntityMetadata;
6
use As3\Modlr\Persister\PersisterException;
7
use As3\Modlr\Store\Store;
8
use Doctrine\MongoDB\Connection;
9
use Doctrine\MongoDB\Query\Builder as QueryBuilder;
10
11
/**
12
 * Handles query operations for a MongoDB database connection.
13
 *
14
 * @author Jacob Bare <[email protected]>
15
 */
16
final class Query
17
{
18
    /**
19
     * The Doctine MongoDB connection.
20
     *
21
     * @var Connection
22
     */
23
    private $connection;
24
25
    /**
26
     * The query/database operations formatter.
27
     *
28
     * @var Formatter
29
     */
30
    private $formatter;
31
32
    /**
33
     * Constructor.
34
     *
35
     * @param   Connection  $connection
36
     * @param   Formatter   $formatter
37
     */
38
    public function __construct(Connection $connection, Formatter $formatter)
39
    {
40
        $this->connection = $connection;
41
        $this->formatter = $formatter;
42
    }
43
44
    /**
45
     * Creates a builder object for querying MongoDB based on the provided metadata.
46
     *
47
     * @param   EntityMetadata  $metadata
48
     * @return  QueryBuilder
49
     */
50
    public function createQueryBuilder(EntityMetadata $metadata)
51
    {
52
        return $this->getModelCollection($metadata)->createQueryBuilder();
53
    }
54
55
    /**
56
     * Executes a delete for the provided metadata and criteria.
57
     *
58
     * @param   EntityMetadata  $metadata
59
     * @param   Store           $store
60
     * @param   array           $toInsert
0 ignored issues
show
Bug introduced by
There is no parameter named $toInsert. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
61
     * @return  array|bool
62
     */
63 View Code Duplication
    public function executeDelete(EntityMetadata $metadata, Store $store, array $criteria)
1 ignored issue
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...
64
    {
65
        $criteria = $this->getFormatter()->formatQuery($metadata, $store, $criteria);
66
        return $this->createQueryBuilder($metadata)
67
            ->remove()
68
            ->setQueryArray($criteria)
69
            ->getQuery()
70
            ->execute();
71
        ;
72
    }
73
74
    /**
75
     * Finds records from the database based on the provided metadata and criteria.
76
     *
77
     * @param   EntityMetadata  $metadata   The model metadata that the database should query against.
78
     * @param   Store           $store      The store.
79
     * @param   array           $criteria   The query criteria.
80
     * @param   array           $fields     Fields to include/exclude.
81
     * @param   array           $sort       The sort criteria.
82
     * @param   int             $offset     The starting offset, aka the number of Models to skip.
83
     * @param   int             $limit      The number of Models to limit.
84
     * @return  \Doctrine\MongoDB\Cursor
85
     */
86
    public function executeFind(EntityMetadata $metadata, Store $store, array $criteria, array $fields = [], array $sort = [], $offset = 0, $limit = 0)
87
    {
88
        $criteria = $this->getFormatter()->formatQuery($metadata, $store, $criteria);
89
90
        $builder = $this->createQueryBuilder($metadata)
91
            ->find()
92
            ->setQueryArray($criteria)
93
        ;
94
95
        $this->appendFields($builder, $fields);
96
        $this->appendSort($builder, $sort);
97
        $this->appendLimitAndOffset($builder, $limit, $offset);
98
99
        return $builder->getQuery()->execute();
100
    }
101
102
    /**
103
     * Executes an insert for the provided metadata.
104
     *
105
     * @param   EntityMetadata  $metadata
106
     * @param   array           $toInsert
107
     * @return  array|bool
108
     */
109
    public function executeInsert(EntityMetadata $metadata, array $toInsert)
110
    {
111
        return $this->createQueryBuilder($metadata)
112
            ->insert()
113
            ->setNewObj($toInsert)
114
            ->getQuery()
115
            ->execute()
116
        ;
117
    }
118
119
    /**
120
     * Updates a record from the database based on the provided metadata and criteria.
121
     *
122
     * @param   EntityMetadata  $metadata   The model metadata that the database should query against.
123
     * @param   Store           $store      The store.
124
     * @param   array           $criteria   The query criteria.
125
     * @param   array           $toUpdate   The data to update.
126
     * @return  array|bool
127
     */
128 View Code Duplication
    public function executeUpdate(EntityMetadata $metadata, Store $store, array $criteria, array $toUpdate)
1 ignored issue
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...
129
    {
130
        $criteria = $this->getFormatter()->formatQuery($metadata, $store, $criteria);
131
        return $this->createQueryBuilder($metadata)
132
            ->update()
133
            ->setQueryArray($criteria)
134
            ->setNewObj($toUpdate)
135
            ->getQuery()
136
            ->execute();
137
        ;
138
    }
139
140
    /**
141
     * @return  Formatter
142
     */
143
    public function getFormatter()
144
    {
145
        return $this->formatter;
146
    }
147
148
    /**
149
     * Gets standard database retrieval criteria for an inverse relationship.
150
     *
151
     * @param   EntityMetadata  $metadata       The entity to retrieve database records for.
0 ignored issues
show
Bug introduced by
There is no parameter named $metadata. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
152
     * @param   string|array    $identifiers    The IDs to query.
153
     * @return  array
154
     */
155
    public function getInverseCriteria(EntityMetadata $owner, EntityMetadata $related, $identifiers, $inverseField)
156
    {
157
        $criteria[$inverseField] = (array) $identifiers;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$criteria was never initialized. Although not strictly required by PHP, it is generally a good practice to add $criteria = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
158
        if (true === $owner->isChildEntity()) {
159
            // The owner is owned by a polymorphic model. Must include the type with the inverse field criteria.
160
            $criteria[$inverseField] = [
161
                Persister::IDENTIFIER_KEY   => $criteria[$inverseField],
162
                Persister::POLYMORPHIC_KEY  => $owner->type,
163
            ];
164
        }
165
        if (true === $related->isChildEntity()) {
166
            // The relationship is owned by a polymorphic model. Must include the type in the root criteria.
167
            $criteria[Persister::POLYMORPHIC_KEY] = $related->type;
168
        }
169
        return $criteria;
170
    }
171
172
    /**
173
     * Gets the MongoDB Collection object for a Model.
174
     *
175
     * @param   EntityMetadata  $metadata
176
     * @return  \Doctrine\MongoDB\Collection
177
     */
178
    public function getModelCollection(EntityMetadata $metadata)
179
    {
180
        if (!$metadata->persistence instanceof StorageMetadata) {
181
            throw PersisterException::badRequest('Wrong StorageMetadata type');
182
        }
183
        return $this->connection->selectCollection($metadata->persistence->db, $metadata->persistence->collection);
184
    }
185
186
    /**
187
     * Gets standard database retrieval criteria for an entity and the provided identifiers.
188
     *
189
     * @param   EntityMetadata      $metadata       The entity to retrieve database records for.
190
     * @param   string|array|null   $identifiers    The IDs to query.
191
     * @return  array
192
     */
193
    public function getRetrieveCritiera(EntityMetadata $metadata, $identifiers = null)
194
    {
195
        $criteria = [];
196
        if (true === $metadata->isChildEntity()) {
197
            $criteria[Persister::POLYMORPHIC_KEY] = $metadata->type;
198
        }
199
200
        $identifiers = (array) $identifiers;
201
        if (empty($identifiers)) {
202
            return $criteria;
203
        }
204
        $criteria[Persister::IDENTIFIER_KEY] = (1 === count($identifiers)) ? reset($identifiers) : $identifiers;
205
        return $criteria;
206
    }
207
208
    /**
209
     * Appends projection fields to a Query Builder.
210
     *
211
     * @param   QueryBuilder    $builder
212
     * @param   array           $fields
213
     * @return  self
214
     */
215
    private function appendFields(QueryBuilder $builder, array $fields)
216
    {
217
        list($fields, $include) = $this->prepareFields($fields);
218
        if (!empty($fields)) {
219
            $method = (true === $include) ? 'select' : 'exclude';
220
            $builder->$method(array_keys($fields));
221
        }
222
        return $this;
223
    }
224
225
    /**
226
     * Appends offset and limit criteria to a Query Builder
227
     *
228
     * @param   QueryBuilder    $builder
229
     * @param   int             $limit
230
     * @param   int             $offset
231
     * @return  self
232
     */
233
    private function appendLimitAndOffset(QueryBuilder $builder, $limit, $offset)
234
    {
235
        $limit = (int) $limit;
236
        $offset = (int) $offset;
237
238
        if ($limit > 0) {
239
            $builder->limit($limit);
240
        }
241
242
        if ($offset > 0) {
243
            $builder->skip($offset);
244
        }
245
        return $this;
246
    }
247
248
    /**
249
     * Appends sorting criteria to a Query Builder.
250
     *
251
     * @param   QueryBuilder    $builder
252
     * @param   array           $sort
253
     * @return  self
254
     */
255
    private function appendSort(QueryBuilder $builder, array $sort)
256
    {
257
        if (!empty($sort)) {
258
            $builder->sort($sort);
259
        }
260
        return $this;
261
    }
262
263
    /**
264
     * Prepares projection fields for a query and returns as a tuple.
265
     *
266
     * @param   array   $fields
267
     * @return  array
268
     * @throws  PersisterException
269
     */
270
    private function prepareFields(array $fields)
271
    {
272
        $include = null;
273
        foreach ($fields as $key => $type) {
274
            $type = (bool) $type;
275
            if (null === $include) {
276
                $include = $type;
277
            }
278
            if ($type !== $include) {
279
                PersisterException::badRequest('Field projection mismatch. You cannot both exclude and include fields.');
280
            }
281
            $fields[$key] = $type;
282
        }
283
        return [$fields, $include];
284
    }
285
}
286