Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
156 / 156
100.00% covered (success)
100.00%
24 / 24
CRAP
100.00% covered (success)
100.00%
1 / 1
QueryBuilder
100.00% covered (success)
100.00%
156 / 156
100.00% covered (success)
100.00%
24 / 24
27
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 create
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 formatAs
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 fetch
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 select
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
2
 get
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 update
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 modify
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 insertMultiple
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 addMultiple
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 insert
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 add
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 delete
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 destroy
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 joinOn
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
1
 filterOnFields
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
1
 filterList
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 filterOn
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
1
 filter
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 groupOn
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 orderOn
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 sortOn
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 limit
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 offset
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace Projom\Storage\SQL;
6
7use Projom\Storage\Query\Action;
8use Projom\Storage\Engine\Driver\DriverBase;
9use Projom\Storage\Query\Format;
10use Projom\Storage\SQL\QueryObject;
11use Projom\Storage\SQL\Util\Join;
12use Projom\Storage\SQL\Util\LogicalOperator;
13use Projom\Storage\SQL\Util\Operator;
14use Projom\Storage\SQL\Util\Filter;
15use Psr\Log\LoggerInterface;
16use Psr\Log\NullLogger;
17
18class QueryBuilder
19{
20    private null|DriverBase $driver = null;
21    private LoggerInterface $logger;
22    private array $collections = [];
23    private array $formatting = [];
24    private array $fields = [];
25    private array $filters = [];
26    private array $sorts = [];
27    private array $joins = [];
28    private array $groups = [];
29    private null|int $limit = null;
30    private null|int $offset = null;
31
32    private const DEFAULT_SELECT = '*';
33
34    public function __construct(
35        null|DriverBase $driver,
36        array $collections,
37        LoggerInterface $logger = new NullLogger()
38    ) {
39        $this->driver = $driver;
40        $this->logger = $logger;
41        $this->collections = $collections;
42        $this->fields = [static::DEFAULT_SELECT];
43        $this->formatting = [Format::ARRAY, null];
44    }
45
46    public static function create(
47        null|DriverBase $driver = null,
48        array $collections = [],
49        LoggerInterface $logger = new NullLogger()
50    ): QueryBuilder {
51        return new QueryBuilder($driver, $collections, $logger);
52    }
53
54    /**
55     * Format the query result.
56     * 
57     * * Example use: $database->query('CollectionName')->formatAs(Format::STD_CLASS)
58     * * Example use: $database->query('CollectionName')->formatAs(Format::CUSTOM_OBJECT, ClassName::class)
59     */
60    public function formatAs(Format $format, mixed $args = null): QueryBuilder
61    {
62        $this->logger->debug(
63            'Method: {method} with {format} and {args}.',
64            ['format' => $format->name, 'args' => $args, 'method' => __METHOD__]
65        );
66
67        $this->formatting = [$format, $args];
68        return $this;
69    }
70
71    /**
72     * Simple query mechanism to find record(s) by a field and its value.
73     * 
74     * * Example use: $database->query('CollectionName')->fetch('Name', 'John')
75     * * Example use: $database->query('CollectionName')->fetch('Age', [25, 55], Operator::IN)
76     */
77    public function fetch(string $field, mixed $value, Operator $operator = Operator::EQ): null|array
78    {
79        $this->logger->debug(
80            'Method: {method} with {field} {operator} {value}.',
81            [
82                'field' => $field,
83                'value' => $value,
84                'operator' => $operator->name,
85                'method' => __METHOD__
86            ]
87        );
88
89        $this->filterOn($field, $value, $operator);
90        return $this->select(static::DEFAULT_SELECT);
91    }
92
93    /**
94     * Execute a query finding record(s) and returns the result.
95     * 
96     * * Example use: $database->query('CollectionName')->select('*')
97     * * Example use: $database->query('CollectionName')->select('Name', 'Age')
98     * * Example use: $database->query('CollectionName')->select('Name, Age')
99     * * Example use: $database->query('CollectionName')->select('Name as Username')
100     * * Example use: $database->query('CollectionName')->select([ 'Name', 'Age', 'Username' ])
101     */
102    public function select(string ...$fields): null|array
103    {
104        $this->logger->debug(
105            'Method: {method} with {fields}.',
106            ['fields' => $fields, 'method' => __METHOD__]
107        );
108
109        if ($fields)
110            $this->fields = $fields;
111
112        $queryObject = new QueryObject(
113            collections: $this->collections,
114            fields: $this->fields,
115            filters: $this->filters,
116            sorts: $this->sorts,
117            groups: $this->groups,
118            limit: $this->limit,
119            offset: $this->offset,
120            joins: $this->joins,
121            formatting: $this->formatting
122        );
123        return $this->driver->dispatch(Action::SELECT, $queryObject);
124    }
125
126    /**
127     * Alias for select method.
128     */
129    public function get(string ...$fields): null|array
130    {
131        return $this->select(...$fields);
132    }
133
134    /**
135     * Execute a query updating record(s) and return the number of affected rows.
136     * 
137     * * Example use: $database->query('CollectionName')->update(['Active' => 1])
138     * * Example use: $database->query('CollectionName')->filterOn( ... )->update(['Username' => 'Jane', 'Password' => 'password'])
139     */
140    public function update(array $fieldsWithValues): int
141    {
142        $this->logger->debug(
143            'Method: {method} with {fieldsWithValues}.',
144            ['fieldsWithValues' => $fieldsWithValues, 'method' => __METHOD__]
145        );
146
147        $queryObject = new QueryObject(
148            collections: $this->collections,
149            fieldsWithValues: [$fieldsWithValues],
150            filters: $this->filters,
151            joins: $this->joins
152        );
153        return $this->driver->dispatch(Action::UPDATE, $queryObject);
154    }
155
156    /**
157     * Alias for update method.
158     */
159    public function modify(array $fieldsWithValues): int
160    {
161        return $this->update($fieldsWithValues);
162    }
163
164    /**
165     * Execute a query inserting multiple records in a single statement.
166     * Returns the primary id of the first inserted record.
167     * 
168     * * Example use: $database->query('CollectionName')->insert([['Username' => 'John', 'Password' => '1234']])
169     */
170    public function insertMultiple(array $fieldsWithValues): int
171    {
172        $this->logger->debug(
173            'Method: {method} with {fieldsWithValues}.',
174            ['fieldsWithValues' => $fieldsWithValues, 'method' => __METHOD__]
175        );
176
177        $queryObject = new QueryObject(
178            collections: $this->collections,
179            fieldsWithValues: $fieldsWithValues
180        );
181        return $this->driver->dispatch(Action::INSERT, $queryObject);
182    }
183
184    /**
185     * Alias for insert multiple method.
186     */
187    public function addMultiple(array $fieldsWithValues): int
188    {
189        return $this->insertMultiple($fieldsWithValues);
190    }
191
192    /**
193     * Execute a query inserting a record and returns the corresponding primary id.
194     * 
195     * * Example use: $database->query('CollectionName')->insert(['Username' => 'John', 'Password' => '1234'])
196     */
197    public function insert(array $fieldsWithValues): int
198    {
199        return $this->insertMultiple([$fieldsWithValues]);
200    }
201
202    /**
203     * Alias for insert method.
204     */
205    public function add(array $fieldsWithValues): int
206    {
207        return $this->insertMultiple([$fieldsWithValues]);
208    }
209
210    /**
211     * Execute a query to delete record(s) and returns the number of affected rows.
212     * 
213     * * Example use: $database->query('CollectionName')->filterOn( ... )->delete()
214     */
215    public function delete(): int
216    {
217        $this->logger->debug(
218            'Method: {method}.',
219            ['method' => __METHOD__]
220        );
221
222        $queryObject = new QueryObject(
223            collections: $this->collections,
224            filters: $this->filters,
225            joins: $this->joins
226        );
227        return $this->driver->dispatch(Action::DELETE, $queryObject);
228    }
229
230    /**
231     * Alias for delete method.
232     */
233    public function destroy(): int
234    {
235        return $this->delete();
236    }
237
238    /**
239     * Join collections to the query.
240     * 
241     * * Example use: $database->query('CollectionName')->join('Collection1.Field', Join::INNER, 'Collection2.Field')
242     * * Example use: $database->query('CollectionName')->join('Collection1.Field = Collection2.Field', Join::LEFT)
243     */
244    public function joinOn(
245        string $currentCollectionWithField,
246        Join $join,
247        string|null $onCollectionWithField = null
248    ): QueryBuilder {
249
250        $this->logger->debug(
251            'Method: {method} with {currentCollectionWithField} {join} {onCollectionWithField}.',
252            [
253                'currentCollectionWithField' => $currentCollectionWithField,
254                'join' => $join->name,
255                'onCollectionWithField' => $onCollectionWithField,
256                'method' => __METHOD__
257            ]
258        );
259
260        $this->joins[] = [
261            $currentCollectionWithField,
262            $join,
263            $onCollectionWithField
264        ];
265
266        return $this;
267    }
268
269    /**
270     * Create a filter to be used in the query to be executed.
271     * 
272     * @param array $fieldsWithValues ['Name' => 'John', 'Lastname' => 'Doe', 'UserID' => 25, ..., ...]
273     *
274     * * Example use: $database->query('CollectionName')->filterList(['Name' => 'John', 'Deleted' => 0 ])
275     */
276    public function filterOnFields(
277        array $fieldsWithValues,
278        Operator $operator = Operator::EQ,
279        LogicalOperator $logicalOperator = LogicalOperator::AND
280    ): QueryBuilder {
281
282        $this->logger->debug(
283            'Method: {method} with {fieldsWithValues} {operator} and "{lop}".',
284            [
285                'fieldsWithValues' => $fieldsWithValues,
286                'operator' => $operator->name,
287                'lop' => $logicalOperator->name,
288                'method' => __METHOD__
289            ]
290        );
291
292        $filters = Filter::list($fieldsWithValues, $operator);
293        $this->filterList($filters, $logicalOperator);
294
295        return $this;
296    }
297
298    /**
299     * Add a list of filters to be used in the query to be executed.
300     * 
301     * @param array $filters [['Name', Operator::EQ, 'John', LogicalOperator::AND], [ ... ], [ ... ]]
302     * 
303     * * Example use: $database->query('CollectionName')->filterList([['Name', Operator::EQ, 'John', LogicalOperator::AND]])
304     */
305    public function filterList(array $filters, LogicalOperator $logicalOperator = LogicalOperator::AND): QueryBuilder
306    {
307        $this->logger->debug(
308            'Method: {method} with {filters} and "{lop}".',
309            ['filters' => $filters, 'lop' => $logicalOperator->name, 'method' => __METHOD__]
310        );
311
312        $this->filters[] = [$filters, $logicalOperator];
313        return $this;
314    }
315
316    /**
317     * Create a filter to be used in the query to be executed.
318     * 
319     * @param string $field 'Name'
320     * @param mixed $value 'John'
321     * 
322     * * Example use: $database->query('CollectionName')->filterOn('Name', 'John')
323     */
324    public function filterOn(
325        string $field,
326        mixed $value,
327        Operator $operator = Operator::EQ,
328        LogicalOperator $logicalOperator = LogicalOperator::AND
329    ): QueryBuilder {
330
331        $this->logger->debug(
332            'Method: {method} with {field} {operator} {value} and "{lop}".',
333            [
334                'field' => $field,
335                'value' => $value,
336                'operator' => $operator->name,
337                'lop' => $logicalOperator->name,
338                'method' => __METHOD__
339            ]
340        );
341
342        $filter = Filter::build($field, $value, $operator);
343        $this->filter($filter, $logicalOperator);
344
345        return $this;
346    }
347
348    /**
349     * Add a filter to be used in the query to be executed.
350     * 
351     * @param array $filter ['Name', Operator::EQ, 'John', LogicalOperator::AND]
352     * 
353     * * Example use: $database->query('CollectionName')->filter(['Name', Operator::EQ, 'John', LogicalOperator::AND])
354     */
355    public function filter(array $filter, LogicalOperator $logicalOperator = LogicalOperator::AND): QueryBuilder
356    {
357        $this->logger->debug(
358            'Method: {method} with {filter} and "{lop}".',
359            ['filter' => $filter, 'lop' => $logicalOperator->name, 'method' => __METHOD__]
360        );
361
362        $this->filters[] = [[$filter], $logicalOperator];
363        return $this;
364    }
365
366    /**
367     * Group the query result.
368     * 
369     * * Example use: $database->query('CollectionName')->groupOn('Name')
370     * * Example use: $database->query('CollectionName')->groupOn('Name', 'Age')
371     * * Example use: $database->query('CollectionName')->groupOn('Name, Age')
372     */
373    public function groupOn(string ...$fields): QueryBuilder
374    {
375        $this->logger->debug(
376            'Method: {method} with {fields}.',
377            ['fields' => $fields, 'method' => __METHOD__]
378        );
379
380        if ($fields)
381            $this->groups[] = $fields;
382        return $this;
383    }
384
385    /**
386     * Order the query result.
387     * 
388     * * Example use: $database->query('CollectionName')->sortOn(['Name' => Sorts::DESC])
389     * * Example use: $database->query('CollectionName')->sortOn(['Name' => Sorts::ASC, 'Age' => Sorts::DESC])
390     */
391    public function orderOn(array $sortFields): QueryBuilder
392    {
393        $this->logger->debug(
394            'Method: {method} with {sortFields}.',
395            ['sortFields' => $sortFields, 'method' => __METHOD__]
396        );
397
398        foreach ($sortFields as $field => $sort)
399            $this->sorts[] = [$field, $sort];
400        return $this;
401    }
402
403    /**
404     * Alias for order method.
405     */
406    public function sortOn(array $sortFields): QueryBuilder
407    {
408        return $this->orderOn($sortFields);
409    }
410
411    /**
412     * Limit the query result.
413     * 
414     * * Example use: $database->query('CollectionName')->limit(10)
415     * * Example use: $database->query('CollectionName')->limit('10')
416     */
417    public function limit(int $limit): QueryBuilder
418    {
419        $this->logger->debug(
420            'Method: {method} with {limit}.',
421            ['limit' => $limit, 'method' => __METHOD__]
422        );
423
424        $this->limit = $limit;
425        return $this;
426    }
427
428    /**
429     * Offset the query result.
430     * 
431     * * Example use: $database->query('CollectionName')->offset(10)
432     */
433    public function offset(int $offset): QueryBuilder
434    {
435        $this->logger->debug(
436            'Method: {method} with {offset}.',
437            ['offset' => $offset, 'method' => __METHOD__]
438        );
439
440        $this->offset = $offset;
441        return $this;
442    }
443}