AbstractSource   F
last analyzed

Complexity

Total Complexity 64

Size/Duplication

Total Lines 579
Duplicated Lines 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 144
dl 0
loc 579
rs 3.28
c 4
b 0
f 0
wmc 64

28 Methods

Rating   Name   Duplication   Size   Complexity  
A properties() 0 3 1
A hasPagination() 0 3 1
A hasProperties() 0 3 1
A setPage() 0 4 1
A setter() 0 4 1
A numPerPage() 0 3 1
A setNumPerPage() 0 4 1
A createConfig() 0 4 1
A getter() 0 4 1
A page() 0 3 1
A camelize() 0 3 1
A __construct() 0 3 1
A resolvePropertyName() 0 19 4
A addProperties() 0 7 2
B parseOrderWithModel() 0 23 7
B parseFilterWithModel() 0 27 8
A removeProperty() 0 5 1
A addOrder() 0 23 6
A createPagination() 0 7 2
A setProperties() 0 6 1
A setPagination() 0 20 4
A reset() 0 8 1
A createOrder() 0 7 2
A setData() 0 12 3
A addFilter() 0 23 6
A pagination() 0 7 2
A addProperty() 0 5 1
A createFilter() 0 7 2

How to fix   Complexity   

Complex Class

Complex classes like AbstractSource often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AbstractSource, and based on these observations, apply Extract Interface, too.

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
    public function setData(array $data)
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
    public function addFilter($param, $value = null, array $options = null)
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 Charcoal\Source\FilterCollectionInterface such as Charcoal\Source\Filter or Charcoal\Tests\Mock\FilterCollectionClass or Charcoal\Source\AbstractSource. ( 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
    public function addOrder($param, $mode = 'asc', array $options = null)
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 Charcoal\Source\OrderCollectionInterface such as Charcoal\Tests\Mock\OrderCollectionClass or Charcoal\Tests\Mock\OrderTree or Charcoal\Source\AbstractSource. ( 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