Completed
Push — dev ( 6a90d3...5fa4ce )
by James Ekow Abaka
01:48
created

QueryOperations::runDynamicMethod()   B

Complexity

Conditions 6
Paths 10

Size

Total Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 6.2373

Importance

Changes 0
Metric Value
dl 0
loc 22
ccs 13
cts 16
cp 0.8125
rs 8.9457
c 0
b 0
f 0
cc 6
nc 10
nop 1
crap 6.2373
1
<?php
2
3
/*
4
 * The MIT License
5
 *
6
 * Copyright 2014-2018 James Ekow Abaka Ainooson
7
 *
8
 * Permission is hereby granted, free of charge, to any person obtaining a copy
9
 * of this software and associated documentation files (the "Software"), to deal
10
 * in the Software without restriction, including without limitation the rights
11
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
 * copies of the Software, and to permit persons to whom the Software is
13
 * furnished to do so, subject to the following conditions:
14
 *
15
 * The above copyright notice and this permission notice shall be included in
16
 * all copies or substantial portions of the Software.
17
 *
18
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24
 * THE SOFTWARE.
25
 */
26
27
namespace ntentan\nibii;
28
29
use ntentan\atiaa\Driver;
30
use ntentan\nibii\exceptions\ModelNotFoundException;
31
use ntentan\utils\Text;
32
33
/**
34
 * Performs operations that query the database.
35
 */
36
class QueryOperations
37
{
38
    /**
39
     * An instance of the record wrapper being used.
40
     *
41
     * @var RecordWrapper
42
     */
43
    private $wrapper;
44
45
    /**
46
     * An instance of the driver adapter used in the database connection.
47
     *
48
     * @var DriverAdapter
49
     */
50
    private $adapter;
51
52
    /**
53
     * An instance of query parameters used to perform the various queries.
54
     *
55
     * @var QueryParameters
56
     */
57
    private $queryParameters;
58
59
    /**
60
     * The name of a method initialized through a dynamic method waiting to be executed.
61
     *
62
     * @var string
63
     */
64
    private $pendingMethod;
65
66
    /**
67
     * Regular expressions for matching dynamic methods.
68
     *
69
     * @var array
70
     */
71
    private $dynamicMethods = [
72
        '/(?<method>filterBy)(?<variable>[A-Z][A-Za-z]+){1}/',
73
        '/(?<method>sort)(?<direction>Asc|Desc)?(By)(?<variable>[A-Z][A-Za-z]+){1}/',
74
        '/(?<method>fetch)(?<first>First)?(With)(?<variable>[A-Za-z]+)/',
75
    ];
76
77
    /**
78
     * An instance of the DataOperations class used for filtered deletes.
79
     *
80
     * @var DataOperations
81
     */
82
    private $dataOperations;
83
84
    /**
85
     * An instance of the Driver class used for establishing database connections.
86
     *
87
     * @var Driver
88
     */
89
    private $driver;
90
91
    private $defaultQueryParameters = null;
92
93
    /**
94
     * QueryOperations constructor.
95
     *
96
     * @param RecordWrapper  $wrapper
97
     * @param DataOperations $dataOperations
98
     * @param Driver         $driver
99
     *
100
     * @internal param DriverAdapter $adapter
101
     */
102 30
    public function __construct(RecordWrapper $wrapper, DataOperations $dataOperations, Driver $driver)
103
    {
104 30
        $this->wrapper = $wrapper;
105 30
        $this->adapter = $wrapper->getAdapter();
106 30
        $this->dataOperations = $dataOperations;
107 30
        $this->driver = $driver;
108 30
    }
109
110
    /**
111
     * Fetches items from the database.
112
     *
113
     * @param int|array|QueryParameters $query
114
     *
115
     * @return RecordWrapper
0 ignored issues
show
Documentation introduced by
Should the return type not be RecordWrapper|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...
116
     * @throws \ReflectionException
117
     * @throws \ntentan\atiaa\exceptions\ConnectionException
118
     * @throws exceptions\NibiiException
119
     */
120 20
    public function doFetch($query = null)
121
    {
122 20
        $parameters = $this->buildFetchQueryParameters($query);
123 20
        $data = $this->adapter->select($parameters);
124 20
        if (empty($data)) {
125 4
            return;
126
        } else {
127 16
            $results = $this->wrapper->fromArray($data);
128 16
            $results->fix($parameters);
129 16
            $this->resetQueryParameters();
130 16
            return $results;
131
        }
132
    }
133
134 16
    public function doFix(QueryParameters $queryParameters)
135
    {
136 16
        $this->defaultQueryParameters = clone $queryParameters;
137 16
    }
138
139
    /**
140
     * The method takes multiple types of arguments and converts it to a QueryParametersObject.
141
     * When this method receives null, it returns a new instance of QueryParameters. When it receives an integer, it
142
     * returns a QueryParameters object that points the primary key to the integer. When it receives an associative
143
     * array, it builds a series of conditions with array key-value pairs.
144
     *
145
     * @param int|array|QueryParameters $arg
146
     * @param bool $instantiate
147
     *
148
     * @return QueryParameters
149
     * @throws \ReflectionException
150
     * @throws \ntentan\atiaa\exceptions\ConnectionException
151
     * @throws exceptions\NibiiException
152
     */
153 22
    private function buildFetchQueryParameters($arg, $instantiate = true)
154
    {
155 22
        if ($arg instanceof QueryParameters) {
156 8
            $this->queryParameters = $arg;
157 8
            return $arg;
158
        }
159
160 22
        $parameters = $this->getQueryParameters($instantiate);
161
162 22
        if (is_numeric($arg)) {
163 4
            $description = $this->wrapper->getDescription();
164 4
            $parameters->addFilter($description->getPrimaryKey()[0], $arg);
165 4
            $parameters->setFirstOnly(true);
166 20
        } elseif (is_array($arg)) {
167 6
            foreach ($arg as $field => $value) {
168 6
                $parameters->addFilter($field, $value);
169
            }
170
        }
171
172 22
        return $parameters;
173
    }
174
175
    /**
176
     * Creates a new instance of the QueryParameters if required or just returns an already instance.
177
     *
178
     * @param bool $forceInstantiation
179
     *
180
     * @return QueryParameters
181
     */
182 28
    private function getQueryParameters($forceInstantiation = true)
183
    {
184 28
        if ($this->queryParameters === null && $forceInstantiation) {
185 28
            $this->queryParameters = new QueryParameters($this->wrapper->getDBStoreInformation()['quoted_table']);
186
        }
187 28
        return $this->queryParameters;
188
    }
189
190
    /**
191
     * Clears up the query parameters.
192
     */
193 24
    private function resetQueryParameters()
194
    {
195 24
        $this->queryParameters = $this->defaultQueryParameters ? clone $this->defaultQueryParameters : null;
196 24
    }
197
198
    /**
199
     * Performs the fetch operation and returns just the first item.
200
     *
201
     * @param mixed $id
202
     *
203
     * @return RecordWrapper
0 ignored issues
show
Documentation introduced by
Should the return type not be RecordWrapper|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...
204
     * @throws \ReflectionException
205
     * @throws \ntentan\atiaa\exceptions\ConnectionException
206
     * @throws exceptions\NibiiException
207
     */
208 10
    public function doFetchFirst($id = null)
209
    {
210 10
        $this->getQueryParameters()->setFirstOnly(true);
211
212 10
        return $this->doFetch($id);
213
    }
214
215
    /**
216
     * Set the fields that should be returned for each record.
217
     *
218
     * @return RecordWrapper
219
     */
220 12
    public function doFields()
221
    {
222 12
        $fields = [];
223 12
        $arguments = func_get_args();
224 12
        foreach ($arguments as $argument) {
225 12
            if (is_array($argument)) {
226 6
                $fields = array_merge($fields, $argument);
227
            } else {
228 6
                $fields[] = $argument;
229
            }
230
        }
231 12
        $this->getQueryParameters()->setFields($fields);
232
233 12
        return $this->wrapper;
234
    }
235
236
//    public function doFix($query)
237
//    {
238
//        $this->defaultQueryParameters = $query;
239
//    }
240
241
    /**
242
     * Sort the query by a given field in a given directory.
243
     *
244
     * @param string $field
245
     * @param string $direction
246
     */
247
    public function doSortBy($field, $direction = 'ASC')
248
    {
249
        $this->getQueryParameters()->addSort($field, $direction);
250
    }
251
252
    /**
253
     * @param mixed $arguments
254
     *
255
     * @return array
256
     */
257 10
    private function getFilter($arguments)
258
    {
259 10
        if (count($arguments) == 2 && is_array($arguments[1])) {
260 2
            $filter = $arguments[0];
261 2
            $data = $arguments[1];
262
        } else {
263 10
            $filter = array_shift($arguments);
264 10
            $data = $arguments;
265
        }
266
267 10
        return ['filter' => $filter, 'data' => $data];
268
    }
269
270 6
    public function doFilter()
271
    {
272 6
        $arguments = func_get_args();
273 6
        if (count($arguments) == 1 && is_array($arguments[0])) {
274
            foreach ($arguments[0] as $field => $value) {
275
                $this->getQueryParameters()->addFilter($field, $value);
276
            }
277
        } else {
278 6
            $details = $this->getFilter($arguments);
279 6
            $this->getQueryParameters()->setFilter($details['filter'], $details['data']);
280
        }
281
282 6
        return $this->wrapper;
283
    }
284
285 4
    public function doFilterBy()
286
    {
287 4
        $arguments = func_get_args();
288 4
        $details = $this->getFilter($arguments);
289 4
        $this->getQueryParameters()->addFilter($details['filter'], $details['data']);
290
291 4
        return $this->wrapper;
292
    }
293
294 6
    public function doUpdate($data)
295
    {
296 6
        $this->driver->beginTransaction();
297 6
        $parameters = $this->getQueryParameters();
298 6
        $this->adapter->bulkUpdate($data, $parameters);
299 6
        $this->driver->commit();
300 6
        $this->resetQueryParameters();
301 6
    }
302
303 2
    public function doDelete($args = null)
304
    {
305 2
        $this->driver->beginTransaction();
306 2
        $parameters = $this->buildFetchQueryParameters($args);
307
308 2
        if ($parameters === null) {
309
            $primaryKey = $this->wrapper->getDescription()->getPrimaryKey();
310
            $parameters = $this->getQueryParameters();
311
            $data = $this->wrapper->getData();
312
            $keys = [];
313
314
            foreach ($data as $datum) {
315
                if ($this->dataOperations->isItemDeletable($primaryKey, $datum)) {
0 ignored issues
show
Documentation introduced by
$primaryKey is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
316
                    $keys[] = $datum[$primaryKey[0]];
317
                }
318
            }
319
320
            $parameters->addFilter($primaryKey[0], $keys);
321
            $this->adapter->delete($parameters);
322
        } else {
323 2
            $this->adapter->delete($parameters);
324
        }
325
326 2
        $this->driver->commit();
327 2
        $this->resetQueryParameters();
328 2
    }
329
330 10
    public function runDynamicMethod($arguments)
331
    {
332 10
        $arguments = count($arguments) > 1 ? $arguments : ($arguments[0] ?? null);
333 10
        switch ($this->pendingMethod['method']) {
334 10
            case 'filterBy':
335 4
                $this->getQueryParameters()->addFilter(Text::deCamelize($this->pendingMethod['variable']), $arguments);
336
337 4
                return $this->wrapper;
338 8
            case 'sort':
339
                $this->getQueryParameters()->addSort(Text::deCamelize($this->pendingMethod['variable']), $this->pendingMethod['direction']);
340
341
                return $this->wrapper;
342 8
            case 'fetch':
343 8
                $parameters = $this->getQueryParameters();
344 8
                $parameters->addFilter(Text::deCamelize($this->pendingMethod['variable']), $arguments);
345 8
                if ($this->pendingMethod['first'] === 'First') {
346 8
                    $parameters->setFirstOnly(true);
347
                }
348
349 8
                return $this->doFetch();
350
        }
351
    }
352
353 10
    public function initDynamicMethod($method)
354
    {
355 10
        $return = false;
356
357 10
        foreach ($this->dynamicMethods as $regexp) {
358 10
            if (preg_match($regexp, $method, $matches)) {
359 10
                $return = true;
360 10
                $this->pendingMethod = $matches;
0 ignored issues
show
Documentation Bug introduced by
It seems like $matches of type array<integer,string> is incompatible with the declared type string of property $pendingMethod.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
361 10
                break;
362
            }
363
        }
364
365 10
        return $return;
366
    }
367
368
    public function doCount($query = null)
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
369
    {
370
        return $this->adapter->count($this->buildFetchQueryParameters($query));
371
    }
372
373
    public function doLimit($numItems)
374
    {
375
        $this->getQueryParameters()->setLimit($numItems);
376
377
        return $this->wrapper;
378
    }
379
380
    public function doOffset($offset)
381
    {
382
        $this->getQueryParameters()->setOffset($offset);
383
384
        return $this->wrapper;
385
    }
386
387
    public function doWith($model)
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
388
    {
389
        if (!isset($this->wrapper->getDescription()->getRelationships()[$model])) {
390
            throw new ModelNotFoundException("Could not find related model [$model]");
391
        }
392
        $relationship = $this->wrapper->getDescription()->getRelationships()[$model];
393
394
        return $relationship->createQuery();
395
    }
396
}
397