Passed
Branch master (018ba4)
by Mathieu
06:43
created

AbstractSource::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Charcoal\Source;
4
5
use InvalidArgumentException;
6
7
// From PSR-3
8
use Psr\Log\LoggerAwareInterface;
9
use Psr\Log\LoggerAwareTrait;
10
11
// From 'charcoal-config'
12
use Charcoal\Config\ConfigurableInterface;
13
use Charcoal\Config\ConfigurableTrait;
14
15
// From 'charcoal-property'
16
use Charcoal\Property\PropertyInterface;
17
18
// From 'charcoal-core'
19
use Charcoal\Source\SourceConfig;
20
use Charcoal\Source\SourceInterface;
21
use Charcoal\Source\ModelAwareTrait;
22
use Charcoal\Source\Filter;
23
use Charcoal\Source\FilterInterface;
24
use Charcoal\Source\FilterCollectionInterface;
25
use Charcoal\Source\FilterCollectionTrait;
26
use Charcoal\Source\Order;
27
use Charcoal\Source\OrderInterface;
28
use Charcoal\Source\OrderCollectionInterface;
29
use Charcoal\Source\OrderCollectionTrait;
30
use Charcoal\Source\Pagination;
31
use Charcoal\Source\PaginationInterface;
32
33
/**
34
 * Data Storage Source Handler.
35
 */
36
abstract class AbstractSource implements
37
    SourceInterface,
38
    ConfigurableInterface,
39
    LoggerAwareInterface
40
{
41
    use ConfigurableTrait;
42
    use LoggerAwareTrait;
43
    use ModelAwareTrait;
44
    use FilterCollectionTrait {
45
        FilterCollectionTrait::addFilter as pushFilter;
46
    }
47
    use OrderCollectionTrait {
48
        OrderCollectionTrait::addOrder as pushOrder;
49
    }
50
51
    /**
52
     * The {@see self::$model}'s properties.
53
     *
54
     * Stored as an associative array to maintain uniqueness.
55
     *
56
     * @var array<string,boolean>
57
     */
58
    private $properties = [];
59
60
    /**
61
     * Store the source query pagination.
62
     *
63
     * @var PaginationInterface
64
     */
65
    protected $pagination;
66
67
    /**
68
     * Create a new source handler.
69
     *
70
     * @param array $data Class dependencies.
71
     */
72
    public function __construct(array $data)
73
    {
74
        $this->setLogger($data['logger']);
75
    }
76
77
    /**
78
     * Reset everything but the model.
79
     *
80
     * @return self
81
     */
82
    public function reset()
83
    {
84
        $this->properties = [];
85
        $this->filters    = [];
86
        $this->orders     = [];
87
        $this->pagination = null;
88
89
        return $this;
90
    }
91
92
    /**
93
     * Set the source's settings.
94
     *
95
     * @param  array $data Data to assign to the source.
96
     * @return self
97
     */
98 View Code Duplication
    public function setData(array $data)
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...
99
    {
100
        foreach ($data as $key => $val) {
101
            $setter = $this->setter($key);
102
            if (is_callable([ $this, $setter ])) {
103
                $this->{$setter}($val);
104
            } else {
105
                $this->{$key} = $val;
106
            }
107
        }
108
109
        return $this;
110
    }
111
112
    /**
113
     * Set the properties of the source to fetch.
114
     *
115
     * This method accepts an array of property identifiers
116
     * that will, if supported, be fetched from the source.
117
     *
118
     * If no properties are set, it is assumed that
119
     * all model propertiesare to be fetched.
120
     *
121
     * @param  (string|PropertyInterface)[] $properties One or more property keys to set.
122
     * @return self
123
     */
124
    public function setProperties(array $properties)
125
    {
126
        $this->properties = [];
127
        $this->addProperties($properties);
128
129
        return $this;
130
    }
131
132
    /**
133
     * Determine if the source has any properties to fetch.
134
     *
135
     * @return boolean TRUE if properties are defined, otherwise FALSE.
136
     */
137
    public function hasProperties()
138
    {
139
        return !empty($this->properties);
140
    }
141
142
    /**
143
     * Get the properties of the source to fetch.
144
     *
145
     * @return string[]
146
     */
147
    public function properties()
148
    {
149
        return array_keys($this->properties);
150
    }
151
152
    /**
153
     * Add properties of the source to fetch.
154
     *
155
     * @param  (string|PropertyInterface)[] $properties One or more property keys to append.
156
     * @return self
157
     */
158
    public function addProperties(array $properties)
159
    {
160
        foreach ($properties as $property) {
161
            $this->addProperty($property);
162
        }
163
164
        return $this;
165
    }
166
167
    /**
168
     * Add a property of the source to fetch.
169
     *
170
     * @param  string|PropertyInterface $property A property key to append.
171
     * @return self
172
     */
173
    public function addProperty($property)
174
    {
175
        $property = $this->resolvePropertyName($property);
176
        $this->properties[$property] = true;
177
        return $this;
178
    }
179
180
    /**
181
     * Remove a property of the source to fetch.
182
     *
183
     * @param  string|PropertyInterface $property A property key.
184
     * @return self
185
     */
186
    public function removeProperty($property)
187
    {
188
        $property = $this->resolvePropertyName($property);
189
        unset($this->properties[$property]);
190
        return $this;
191
    }
192
193
    /**
194
     * Resolve the name for the given property, throws an Exception if not.
195
     *
196
     * @param  mixed $property Property to resolve.
197
     * @throws InvalidArgumentException If property is not a string, empty, or invalid.
198
     * @return string The property name.
199
     */
200
    protected function resolvePropertyName($property)
201
    {
202
        if ($property instanceof PropertyInterface) {
203
            $property = $property->ident();
204
        }
205
206
        if (!is_string($property)) {
207
            throw new InvalidArgumentException(
208
                'Property must be a string.'
209
            );
210
        }
211
212
        if ($property === '') {
213
            throw new InvalidArgumentException(
214
                'Property can not be empty.'
215
            );
216
        }
217
218
        return $property;
219
    }
220
221
    /**
222
     * Append a query filter on the source.
223
     *
224
     * There are 3 different ways of adding a filter:
225
     * - as a `Filter` object, in which case it will be added directly.
226
     *   - `addFilter($obj);`
227
     * - as an array of options, which will be used to build the `Filter` object
228
     *   - `addFilter(['property' => 'foo', 'value' => 42, 'operator' => '<=']);`
229
     * - as 3 parameters: `property`, `value` and `options`
230
     *   - `addFilter('foo', 42, ['operator' => '<=']);`
231
     *
232
     * @deprecated 0.3 To be replaced with FilterCollectionTrait::addFilter()
233
     *
234
     * @uses   self::parseFilterWithModel()
235
     * @uses   FilterCollectionTrait::processFilter()
236
     * @param  mixed $param   The property to filter by,
237
     *     a {@see FilterInterface} object,
238
     *     or a filter array structure.
239
     * @param  mixed $value   Optional value for the property to compare against.
240
     *     Only used if the first argument is a string.
241
     * @param  array $options Optional extra settings to apply on the filter.
242
     * @throws InvalidArgumentException If the $param argument is invalid.
243
     * @return self
244
     */
245 View Code Duplication
    public function addFilter($param, $value = null, array $options = null)
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...
246
    {
247
        if (is_string($param) && $value !== null) {
248
            $expr = $this->createFilter();
249
            $expr->setProperty($param);
250
            $expr->setValue($value);
251
        } else {
252
            $expr = $param;
253
        }
254
255
        $expr = $this->processFilter($expr);
256
257
        /** @deprecated 0.3 */
258
        if (is_array($param) && isset($param['options'])) {
259
            $expr->setData($param['options']);
0 ignored issues
show
Bug introduced by
The method setData() does not exist on Charcoal\Source\FilterInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Charcoal\Source\FilterInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

259
            $expr->/** @scrutinizer ignore-call */ 
260
                   setData($param['options']);
Loading history...
260
        }
261
262
        if (is_array($options)) {
263
            $expr->setData($options);
264
        }
265
266
        $this->filters[] = $this->parseFilterWithModel($expr);
267
        return $this;
268
    }
269
270
    /**
271
     * Process a query filter with the current model.
272
     *
273
     * @param  FilterInterface $filter The expression object.
274
     * @return FilterInterface The parsed expression object.
275
     */
276
    protected function parseFilterWithModel(FilterInterface $filter)
277
    {
278
        if ($this->hasModel()) {
279
            if ($filter->hasProperty()) {
280
                $model    = $this->model();
281
                $property = $filter->property();
282
                if (is_string($property) && $model->hasProperty($property)) {
283
                    $property = $model->property($property);
284
285
                    if ($property->l10n()) {
286
                        $filter->setProperty($property->l10nIdent());
287
                    }
288
289
                    if ($property->multiple()) {
290
                        $filter->setOperator('FIND_IN_SET');
291
                    }
292
                }
293
            }
294
295
            if ($filter instanceof FilterCollectionInterface) {
296
                $filter->traverseFilters(function (FilterInterface $expr) {
0 ignored issues
show
Bug introduced by
The method traverseFilters() does not exist on Charcoal\Source\FilterInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Charcoal\Source\FilterInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

296
                $filter->/** @scrutinizer ignore-call */ 
297
                         traverseFilters(function (FilterInterface $expr) {
Loading history...
Bug introduced by
The method traverseFilters() does not exist on Charcoal\Source\FilterCollectionInterface. It seems like you code against a sub-type of said class. However, the method does not exist in Charcoal\Source\SourceInterface. Are you sure you never get one of those? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

296
                $filter->/** @scrutinizer ignore-call */ 
297
                         traverseFilters(function (FilterInterface $expr) {
Loading history...
297
                    $this->parseFilterWithModel($expr);
298
                });
299
            }
300
        }
301
302
        return $filter;
303
    }
304
305
    /**
306
     * Create a new query filter expression.
307
     *
308
     * @see    FilterCollectionTrait::createFilter()
309
     * @param  array $data Optional expression data.
310
     * @return FilterInterface A new filter expression object.
311
     */
312
    protected function createFilter(array $data = null)
313
    {
314
        $filter = new Filter();
315
        if ($data !== null) {
316
            $filter->setData($data);
317
        }
318
        return $filter;
319
    }
320
321
    /**
322
     * Append a query order on the source.
323
     *
324
     * @deprecated 0.3 To be replaced with OrderCollectionTrait::addOrder()
325
     *
326
     * @uses   self::parseOrderWithModel()
327
     * @uses   OrderCollectionTrait::processOrder()
328
     * @param  mixed  $param   The property to sort by,
329
     *     a {@see OrderInterface} object,
330
     *     or a order array structure.
331
     * @param  string $mode    Optional sorting mode.
332
     *     Defaults to ascending if a property is provided.
333
     * @param  array  $options Optional extra settings to apply on the order.
334
     * @throws InvalidArgumentException If the $param argument is invalid.
335
     * @return self
336
     */
337 View Code Duplication
    public function addOrder($param, $mode = 'asc', array $options = null)
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...
338
    {
339
        if (is_string($param) && $mode !== null) {
340
            $expr = $this->createOrder();
341
            $expr->setProperty($param);
342
            $expr->setMode($mode);
343
        } else {
344
            $expr = $param;
345
        }
346
347
        $expr = $this->processOrder($expr);
348
349
        /** @deprecated 0.3 */
350
        if (is_array($param) && isset($param['options'])) {
351
            $expr->setData($param['options']);
0 ignored issues
show
Bug introduced by
The method setData() does not exist on Charcoal\Source\OrderInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Charcoal\Source\OrderInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

351
            $expr->/** @scrutinizer ignore-call */ 
352
                   setData($param['options']);
Loading history...
352
        }
353
354
        if (is_array($options)) {
355
            $expr->setData($options);
356
        }
357
358
        $this->orders[] = $this->parseOrderWithModel($expr);
359
        return $this;
360
    }
361
362
    /**
363
     * Process a query order with the current model.
364
     *
365
     * @param  OrderInterface $order The expression object.
366
     * @return OrderInterface The parsed expression object.
367
     */
368
    protected function parseOrderWithModel(OrderInterface $order)
369
    {
370
        if ($this->hasModel()) {
371
            if ($order->hasProperty()) {
372
                $model    = $this->model();
373
                $property = $order->property();
374
                if (is_string($property) && $model->hasProperty($property)) {
375
                    $property = $model->property($property);
376
377
                    if ($property->l10n()) {
378
                        $order->setProperty($property->l10nIdent());
379
                    }
380
                }
381
            }
382
383
            if ($order instanceof OrderCollectionInterface) {
384
                $order->traverseOrders(function (OrderInterface $expr) {
0 ignored issues
show
Bug introduced by
The method traverseOrders() does not exist on Charcoal\Source\OrderInterface. It seems like you code against a sub-type of Charcoal\Source\OrderInterface such as Charcoal\Tests\Mock\OrderTree. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

384
                $order->/** @scrutinizer ignore-call */ 
385
                        traverseOrders(function (OrderInterface $expr) {
Loading history...
Bug introduced by
The method traverseOrders() does not exist on Charcoal\Source\OrderCollectionInterface. It seems like you code against a sub-type of said class. However, the method does not exist in Charcoal\Source\SourceInterface. Are you sure you never get one of those? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

384
                $order->/** @scrutinizer ignore-call */ 
385
                        traverseOrders(function (OrderInterface $expr) {
Loading history...
385
                    $this->parseOrderWithModel($expr);
386
                });
387
            }
388
        }
389
390
        return $order;
391
    }
392
393
    /**
394
     * Create a new query order expression.
395
     *
396
     * @param  array $data Optional expression data.
397
     * @return OrderInterface
398
     */
399
    protected function createOrder(array $data = null)
400
    {
401
        $order = new Order();
402
        if ($data !== null) {
403
            $order->setData($data);
404
        }
405
        return $order;
406
    }
407
408
    /**
409
     * Set query pagination.
410
     *
411
     * @param  mixed        $param The pagination object or array.
412
     * @param  integer|null $limit The number of results to fetch if $param is a page number.
413
     * @throws InvalidArgumentException If the $param argument is invalid.
414
     * @return self
415
     */
416
    public function setPagination($param, $limit = null)
417
    {
418
        if ($param instanceof PaginationInterface) {
419
            $pager = $param;
420
        } elseif (is_numeric($param)) {
421
            $pager = $this->createPagination();
422
            $pager->setPage($param);
423
            $pager->setNumPerPage($limit);
424
        } elseif (is_array($param)) {
425
            $pager = $this->createPagination();
426
            $pager->setData($param);
427
        } else {
428
            throw new InvalidArgumentException(
429
                'Can not set pagination, invalid argument.'
430
            );
431
        }
432
433
        $this->pagination = $pager;
434
435
        return $this;
436
    }
437
438
    /**
439
     * Determine if the source has defined a query pagination.
440
     *
441
     * @return boolean
442
     */
443
    public function hasPagination()
444
    {
445
        return ($this->pagination !== null);
446
    }
447
448
    /**
449
     * Get query pagination.
450
     *
451
     * If the pagination wasn't previously define, a new Pagination object will be created.
452
     *
453
     * @return PaginationInterface
454
     */
455
    public function pagination()
456
    {
457
        if ($this->pagination === null) {
458
            $this->pagination = $this->createPagination();
459
        }
460
461
        return $this->pagination;
462
    }
463
464
    /**
465
     * Create a new pagination clause.
466
     *
467
     * @param  array $data Optional clause data.
468
     * @return PaginationInterface
469
     */
470
    protected function createPagination(array $data = null)
471
    {
472
        $pagination = new Pagination();
473
        if ($data !== null) {
474
            $pagination->setData($data);
475
        }
476
        return $pagination;
477
    }
478
479
    /**
480
     * Alias for {@see Pagination::setPage()}.
481
     *
482
     * @param  integer $page The current page.
483
     *     Pages should start at 1.
484
     * @return self
485
     */
486
    public function setPage($page)
487
    {
488
        $this->pagination()->setPage($page);
489
        return $this;
490
    }
491
492
    /**
493
     * Alias for {@see Pagination::page()}.
494
     *
495
     * @return integer
496
     */
497
    public function page()
498
    {
499
        return $this->pagination()->page();
500
    }
501
502
    /**
503
     * Alias for {@see Pagination::setNumPerPage()}.
504
     *
505
     * @param  integer $count The number of results to return, per page.
506
     *     Use 0 to request all results.
507
     * @return self
508
     */
509
    public function setNumPerPage($count)
510
    {
511
        $this->pagination()->setNumPerPage($count);
512
        return $this;
513
    }
514
515
    /**
516
     * Alias for {@see Pagination::numPerPage()}.
517
     *
518
     * @return integer
519
     */
520
    public function numPerPage()
521
    {
522
        return $this->pagination()->numPerPage();
523
    }
524
525
    /**
526
     * Create a new database source config.
527
     *
528
     * @see    \Charcoal\Config\ConfigurableTrait
529
     * @param  array $data Optional data.
530
     * @return SourceConfig
531
     */
532
    public function createConfig(array $data = null)
533
    {
534
        $config = new SourceConfig($data);
535
        return $config;
536
    }
537
538
    /**
539
     * Load item by the primary key.
540
     *
541
     * @param  mixed             $ident Ident can be any scalar value.
542
     * @param  StorableInterface $item  Optional item to load into.
543
     * @return StorableInterface
544
     */
545
    abstract public function loadItem($ident, StorableInterface $item = null);
546
547
    /**
548
     * Load items for the given model.
549
     *
550
     * @param  StorableInterface|null $item Optional model.
551
     * @return StorableInterface[]
552
     */
553
    abstract public function loadItems(StorableInterface $item = null);
554
555
    /**
556
     * Save an item (create a new row) in storage.
557
     *
558
     * @param  StorableInterface $item The object to save.
559
     * @throws \Exception If a storage error occurs.
560
     * @return mixed The created item ID, otherwise FALSE.
561
     */
562
    abstract public function saveItem(StorableInterface $item);
563
564
    /**
565
     * Update an item in storage.
566
     *
567
     * @param  StorableInterface $item       The object to update.
568
     * @param  array             $properties The list of properties to update, if not all.
569
     * @return boolean TRUE if the item was updated, otherwise FALSE.
570
     */
571
    abstract public function updateItem(StorableInterface $item, array $properties = null);
572
573
    /**
574
     * Delete an item from storage.
575
     *
576
     * @param  StorableInterface $item Optional item to delete. If none, the current model object will be used.
577
     * @throws UnexpectedValueException If the item does not have an ID.
578
     * @return boolean TRUE if the item was deleted, otherwise FALSE.
579
     */
580
    abstract public function deleteItem(StorableInterface $item = null);
581
582
    /**
583
     * Allow an object to define how the key getter are called.
584
     *
585
     * @param  string $key The key to get the getter from.
586
     * @return string The getter method name, for a given key.
587
     */
588
    protected function getter($key)
589
    {
590
        $getter = $key;
591
        return $this->camelize($getter);
592
    }
593
594
    /**
595
     * Allow an object to define how the key setter are called.
596
     *
597
     * @param  string $key The key to get the setter from.
598
     * @return string The setter method name, for a given key.
599
     */
600
    protected function setter($key)
601
    {
602
        $setter = 'set_'.$key;
603
        return $this->camelize($setter);
604
    }
605
606
    /**
607
     * Transform a snake_case string to camelCase.
608
     *
609
     * @param  string $str The snake_case string to camelize.
610
     * @return string The camelcase'd string.
611
     */
612
    protected function camelize($str)
613
    {
614
        return lcfirst(implode('', array_map('ucfirst', explode('_', $str))));
615
    }
616
}
617