Completed
Push — master ( d63ace...093a73 )
by Jacob
8s
created

Query::appendSearch()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 9
rs 9.6666
cc 2
eloc 6
nc 2
nop 2
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           $criteria
61
     * @return  array|bool
62
     */
63 View Code Duplication
    public function executeDelete(EntityMetadata $metadata, Store $store, array $criteria)
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->appendSearch($builder, $criteria);
96
        $this->appendFields($builder, $fields);
97
        $this->appendSort($builder, $sort);
98
        $this->appendLimitAndOffset($builder, $limit, $offset);
99
100
        return $builder->getQuery()->execute();
101
    }
102
103
    /**
104
     * Executes an insert for the provided metadata.
105
     *
106
     * @param   EntityMetadata  $metadata
107
     * @param   array           $toInsert
108
     * @return  array|bool
109
     */
110
    public function executeInsert(EntityMetadata $metadata, array $toInsert)
111
    {
112
        return $this->createQueryBuilder($metadata)
113
            ->insert()
114
            ->setNewObj($toInsert)
115
            ->getQuery()
116
            ->execute()
117
        ;
118
    }
119
120
    /**
121
     * Updates a record from the database based on the provided metadata and criteria.
122
     *
123
     * @param   EntityMetadata  $metadata   The model metadata that the database should query against.
124
     * @param   Store           $store      The store.
125
     * @param   array           $criteria   The query criteria.
126
     * @param   array           $toUpdate   The data to update.
127
     * @return  array|bool
128
     */
129 View Code Duplication
    public function executeUpdate(EntityMetadata $metadata, Store $store, array $criteria, array $toUpdate)
130
    {
131
        $criteria = $this->getFormatter()->formatQuery($metadata, $store, $criteria);
132
        return $this->createQueryBuilder($metadata)
133
            ->update()
134
            ->setQueryArray($criteria)
135
            ->setNewObj($toUpdate)
136
            ->getQuery()
137
            ->execute();
138
        ;
139
    }
140
141
    /**
142
     * @return  Formatter
143
     */
144
    public function getFormatter()
145
    {
146
        return $this->formatter;
147
    }
148
149
    /**
150
     * Gets standard database retrieval criteria for an inverse relationship.
151
     *
152
     * @param   EntityMetadata  $owner
153
     * @param   EntityMetadata  $related
154
     * @param   string|array    $identifiers
155
     * @param   string          $inverseField
156
     * @return  array
157
     */
158
    public function getInverseCriteria(EntityMetadata $owner, EntityMetadata $related, $identifiers, $inverseField)
159
    {
160
        $criteria = [
161
            $inverseField   => (array) $identifiers,
162
        ];
163
        if (true === $owner->isChildEntity()) {
164
            // The owner is owned by a polymorphic model. Must include the type with the inverse field criteria.
165
            $criteria[$inverseField] = [
166
                Persister::IDENTIFIER_KEY   => $criteria[$inverseField],
167
                Persister::POLYMORPHIC_KEY  => $owner->type,
168
            ];
169
        }
170
        if (true === $related->isChildEntity()) {
171
            // The relationship is owned by a polymorphic model. Must include the type in the root criteria.
172
            $criteria[Persister::POLYMORPHIC_KEY] = $related->type;
173
        }
174
        return $criteria;
175
    }
176
177
    /**
178
     * Gets the MongoDB Collection object for a Model.
179
     *
180
     * @param   EntityMetadata  $metadata
181
     * @return  \Doctrine\MongoDB\Collection
182
     */
183
    public function getModelCollection(EntityMetadata $metadata)
184
    {
185
        if (!$metadata->persistence instanceof StorageMetadata) {
186
            throw PersisterException::badRequest('Wrong StorageMetadata type');
187
        }
188
        return $this->connection->selectCollection($metadata->persistence->db, $metadata->persistence->collection);
189
    }
190
191
    /**
192
     * Gets standard database retrieval criteria for an entity and the provided identifiers.
193
     *
194
     * @param   EntityMetadata      $metadata       The entity to retrieve database records for.
195
     * @param   string|array|null   $identifiers    The IDs to query.
196
     * @return  array
197
     */
198
    public function getRetrieveCritiera(EntityMetadata $metadata, $identifiers = null)
199
    {
200
        $criteria = [];
201
        if (true === $metadata->isChildEntity()) {
202
            $criteria[Persister::POLYMORPHIC_KEY] = $metadata->type;
203
        }
204
205
        $identifiers = (array) $identifiers;
206
        if (empty($identifiers)) {
207
            return $criteria;
208
        }
209
        $criteria[Persister::IDENTIFIER_KEY] = (1 === count($identifiers)) ? reset($identifiers) : $identifiers;
210
        return $criteria;
211
    }
212
213
    /**
214
     * Appends projection fields to a Query Builder.
215
     *
216
     * @param   QueryBuilder    $builder
217
     * @param   array           $fields
218
     * @return  self
219
     */
220
    private function appendFields(QueryBuilder $builder, array $fields)
221
    {
222
        list($fields, $include) = $this->prepareFields($fields);
223
        if (!empty($fields)) {
224
            $method = (true === $include) ? 'select' : 'exclude';
225
            $builder->$method(array_keys($fields));
226
        }
227
        return $this;
228
    }
229
230
    /**
231
     * Appends offset and limit criteria to a Query Builder
232
     *
233
     * @param   QueryBuilder    $builder
234
     * @param   int             $limit
235
     * @param   int             $offset
236
     * @return  self
237
     */
238
    private function appendLimitAndOffset(QueryBuilder $builder, $limit, $offset)
239
    {
240
        $limit = (int) $limit;
241
        $offset = (int) $offset;
242
243
        if ($limit > 0) {
244
            $builder->limit($limit);
245
        }
246
247
        if ($offset > 0) {
248
            $builder->skip($offset);
249
        }
250
        return $this;
251
    }
252
253
    /**
254
     * Appends text search score and sorting to a Query Builder.
255
     *
256
     * @param   QueryBuilder    $builder
257
     * @param   array           $criteria
258
     * @return  self
259
     */
260
    private function appendSearch(QueryBuilder $builder, array $criteria)
261
    {
262
        if (false === $this->isSearchQuery($criteria)) {
263
            return $this;
264
        }
265
        $builder->selectMeta('searchScore', 'textScore');
266
        $builder->sortMeta('searchScore', 'textScore');
267
        return $this;
268
    }
269
270
    /**
271
     * Appends sorting criteria to a Query Builder.
272
     *
273
     * @param   QueryBuilder    $builder
274
     * @param   array           $sort
275
     * @return  self
276
     */
277
    private function appendSort(QueryBuilder $builder, array $sort)
278
    {
279
        if (!empty($sort)) {
280
            $builder->sort($sort);
281
        }
282
        return $this;
283
    }
284
285
    /**
286
     * Determines if the provided query criteria contains text search.
287
     *
288
     * @param   array   $criteria
289
     * @return  bool
290
     */
291
    private function isSearchQuery(array $criteria)
292
    {
293
        if (isset($criteria['$text'])) {
294
            return true;
295
        }
296
        foreach ($criteria as $key => $value) {
297
            if (is_array($value) && true === $this->isSearchQuery($value)) {
298
                return true;
299
            }
300
        }
301
        return false;
302
    }
303
304
    /**
305
     * Prepares projection fields for a query and returns as a tuple.
306
     *
307
     * @param   array   $fields
308
     * @return  array
309
     * @throws  PersisterException
310
     */
311
    private function prepareFields(array $fields)
312
    {
313
        $include = null;
314
        foreach ($fields as $key => $type) {
315
            $type = (bool) $type;
316
            if (null === $include) {
317
                $include = $type;
318
            }
319
            if ($type !== $include) {
320
                PersisterException::badRequest('Field projection mismatch. You cannot both exclude and include fields.');
321
            }
322
            $fields[$key] = $type;
323
        }
324
        return [$fields, $include];
325
    }
326
}
327