Test Setup Failed
Push — develop ( 13e851...151a72 )
by Jimmy
14:05
created

Builder   B

Complexity

Total Complexity 45

Size/Duplication

Total Lines 451
Duplicated Lines 0 %

Test Coverage

Coverage 93.8%

Importance

Changes 5
Bugs 0 Features 0
Metric Value
wmc 45
eloc 140
c 5
b 0
f 0
dl 0
loc 451
ccs 121
cts 129
cp 0.938
rs 8.8

26 Methods

Rating   Name   Duplication   Size   Complexity  
A create() 0 5 1
A __call() 0 12 4
A debug() 0 5 1
A __get() 0 12 2
A newInstance() 0 10 2
A setParent() 0 5 1
A whereId() 0 3 1
B get() 0 38 6
A limit() 0 3 1
A orderByDesc() 0 3 1
A make() 0 5 1
A find() 0 5 1
A where() 0 7 2
A oldest() 0 5 2
A newInstanceForModel() 0 4 1
A setClass() 0 9 2
A orderBy() 0 4 1
A take() 0 3 1
A pageinate() 0 3 1
A getPath() 0 7 2
A whereNot() 0 3 1
A paginate() 0 4 1
A page() 0 4 1
A latest() 0 5 2
A getModel() 0 11 3
A peelWrapperPropertyIfNeeded() 0 23 3

How to fix   Complexity   

Complex Class

Complex classes like Builder 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 Builder, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Spinen\Halo\Support;
4
5
use BadMethodCallException;
6
use GuzzleHttp\Exception\GuzzleException;
7
use Illuminate\Support\Arr;
8
use Illuminate\Support\Collection as LaravelCollection;
9
use Illuminate\Support\Str;
10
use Illuminate\Support\Traits\Conditionable;
11
use Spinen\Halo\Action;
12
use Spinen\Halo\Agent;
13
use Spinen\Halo\Appointment;
14
use Spinen\Halo\Article;
15
use Spinen\Halo\Asset;
16
use Spinen\Halo\Attachment;
17
use Spinen\Halo\Client;
18
use Spinen\Halo\Concerns\HasClient;
19
use Spinen\Halo\Contract;
20
use Spinen\Halo\Exceptions\InvalidRelationshipException;
21
use Spinen\Halo\Exceptions\ModelNotFoundException;
22
use Spinen\Halo\Exceptions\NoClientException;
23
use Spinen\Halo\Exceptions\TokenException;
24
use Spinen\Halo\Invoice;
25
use Spinen\Halo\Item;
26
use Spinen\Halo\Opportunity;
27
use Spinen\Halo\Project;
28
use Spinen\Halo\Quote;
29
use Spinen\Halo\Report;
30
use Spinen\Halo\Site;
31
use Spinen\Halo\Status;
32
use Spinen\Halo\Supplier;
33
use Spinen\Halo\Team;
34
use Spinen\Halo\Ticket;
35
use Spinen\Halo\TicketType;
36
use Spinen\Halo\User;
37
use Spinen\Halo\Webhook;
38
use Spinen\Halo\WebhookEvent;
39
40
/**
41
 * Class Builder
42
 *
43
 * @property Collection $actions
44
 * @property Collection $agents
45
 * @property Collection $appointments
46
 * @property Collection $articles
47
 * @property Collection $assets
48
 * @property Collection $attachments
49
 * @property Collection $clients
50
 * @property Collection $contracts
51
 * @property Collection $invoices
52
 * @property Collection $items
53
 * @property Collection $opportunities
54
 * @property Collection $projects
55
 * @property Collection $quotes
56
 * @property Collection $reports
57
 * @property Collection $sites
58
 * @property Collection $statuses
59
 * @property Collection $suppliers
60
 * @property Collection $teams
61
 * @property Collection $tickets
62
 * @property Collection $ticket_types
63
 * @property Collection $users
64
 * @property Collection $webhooks
65
 * @property Collection $webhook_events
66
 * @property Agent $agent
67
 * @property User $user
68
 *
69
 * @method self actions()
70
 * @method self agents()
71
 * @method self appointments()
72
 * @method self articles()
73
 * @method self assets()
74
 * @method self attachments()
75
 * @method self clients()
76
 * @method self contracts()
77
 * @method self invoices()
78
 * @method self items()
79
 * @method self opportunities()
80
 * @method self projects()
81
 * @method self quotes()
82
 * @method self reports()
83
 * @method self search($for)
84
 * @method self sites()
85
 * @method self statuses()
86
 * @method self suppliers()
87
 * @method self teams()
88
 * @method self ticket_types()
89
 * @method self tickets()
90
 * @method self users()
91
 * @method self webhook_events()
92
 * @method self webhooks()
93
 */
94
class Builder
95
{
96
    use Conditionable;
97
    use HasClient;
98
99
    /**
100
     * Class to cast the response
101
     */
102
    protected string $class;
103
104
    /**
105
     * Debug Guzzle calls
106
     */
107
    protected bool $debug = false;
108
109
    /**
110
     * Model instance
111
     */
112
    protected Model $model;
113
114
    /**
115
     * Parent model instance
116
     */
117
    protected ?Model $parentModel = null;
118
119
    /**
120
     * Map of potential parents with class name
121
     *
122
     * @var array
123
     */
124
    protected $rootModels = [
125
        'actions' => Action::class,
126
        'agents' => Agent::class,
127
        'appointments' => Appointment::class,
128
        'articles' => Article::class,
129
        'assets' => Asset::class,
130
        'attachments' => Attachment::class,
131
        'clients' => Client::class,
132
        'contracts' => Contract::class,
133
        'invoices' => Invoice::class,
134
        'items' => Item::class,
135
        'opportunities' => Opportunity::class,
136
        'projects' => Project::class,
137
        'quotes' => Quote::class,
138
        'reports' => Report::class,
139
        'sites' => Site::class,
140
        'statuses' => Status::class,
141
        'suppliers' => Supplier::class,
142
        'teams' => Team::class,
143
        'tickets' => Ticket::class,
144
        'ticket_types' => TicketType::class,
145
        'users' => User::class,
146
        'webhooks' => Webhook::class,
147
        'webhook_events' => WebhookEvent::class,
148
    ];
149
150
    /**
151
     * Properties to filter the response
152
     */
153
    protected array $wheres = [];
154
155
    /**
156
     * Magic method to make builders for root models
157
     *
158
     * @throws BadMethodCallException
159
     * @throws ModelNotFoundException
160
     * @throws NoClientException
161
     */
162 24
    public function __call(string $name, array $arguments)
163
    {
164 24
        if (! isset($this->parentModel) && array_key_exists($name, $this->rootModels)) {
165 23
            return $this->newInstanceForModel($this->rootModels[$name]);
166
        }
167
168
        // Alias search or search_anything or searchAnything to where(search|search_anything, for)
169 1
        if (Str::startsWith($name, 'search')) {
170
            return $this->where(...array_merge([Str::of($name)->snake()->toString()], $arguments));
0 ignored issues
show
Bug introduced by
array_merge(array(Illumi...oString()), $arguments) is expanded, but the parameter $property of Spinen\Halo\Support\Builder::where() does not expect variable arguments. ( Ignorable by Annotation )

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

170
            return $this->where(/** @scrutinizer ignore-type */ ...array_merge([Str::of($name)->snake()->toString()], $arguments));
Loading history...
171
        }
172
173 1
        throw new BadMethodCallException(sprintf('Call to undefined method [%s]', $name));
174
    }
175
176
    /**
177
     * Magic method to make builders appears as properties
178
     *
179
     * @throws GuzzleException
180
     * @throws InvalidRelationshipException
181
     * @throws ModelNotFoundException
182
     * @throws NoClientException
183
     * @throws TokenException
184
     */
185 26
    public function __get(string $name): Collection|Model|null
186
    {
187 26
        return match (true) {
188 26
            $name === 'agent' => $this->newInstanceForModel(Agent::class)
189 26
                ->get(extra: 'me')
190 26
                ->first(),
191 26
            $name === 'user' => $this->newInstanceForModel(User::class)
192 26
                ->get(extra: 'me')
193 26
                ->first(),
194 26
            ! $this->parentModel && array_key_exists($name, $this->rootModels) => $this->{$name}()
195 26
                ->get(),
196 26
            default => null,
197 26
        };
198
    }
199
200
    /**
201
     * Create instance of class and save via API
202
     *
203
     * @throws InvalidRelationshipException
204
     */
205 2
    public function create(array $attributes): Model
206
    {
207 2
        return tap(
208 2
            $this->make($attributes),
209 2
            fn (Model $model): bool => $model->save()
210 2
        );
211
    }
212
213
    /**
214
     * Set debug on the client
215
     *
216
     * This is reset to false after the request
217
     */
218
    public function debug(bool $debug = true): self
219
    {
220
        $this->debug = $debug;
221
222
        return $this;
223
    }
224
225
    /**
226
     * Get Collection of class instances that match query
227
     *
228
     * @throws GuzzleException
229
     * @throws InvalidRelationshipException
230
     * @throws NoClientException
231
     * @throws TokenException
232
     */
233 31
    public function get(array|string $properties = ['*'], ?string $extra = null): Collection|Model
234
    {
235 31
        $properties = Arr::wrap($properties);
236
        $count = null;
237
        $page = null;
238 31
        $pageSize = null;
239 31
240 31
        // Call API to get the response
241
        $response = $this->getClient()
242
            ->setDebug($this->debug)
243
            ->request($this->getPath($extra));
244
245 31
        if (
246
            array_key_exists('record_count', $response) &&
247
            array_key_exists('page_no', $response) &&
248 31
            array_key_exists('page_size', $response)
249
        ) {
250 31
            $count = $response['record_count'];
251 31
            $page = $response['page_no'];
252 31
            $pageSize = $response['page_size'];
253 4
        }
254 5
255 5
        // Peel off the key if exist
256 5
        $response = $this->peelWrapperPropertyIfNeeded(Arr::wrap($response));
257 31
258 31
        // Convert to a collection of filtered objects casted to the class
259 31
        return (new Collection((array_values($response) === $response) ? $response : [$response]))
0 ignored issues
show
Bug introduced by
array_values($response) ...onse : array($response) of type array|array<integer,array> is incompatible with the type Illuminate\Contracts\Support\Arrayable expected by parameter $items of Spinen\Halo\Support\Collection::__construct(). ( Ignorable by Annotation )

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

259
        return (new Collection(/** @scrutinizer ignore-type */ (array_values($response) === $response) ? $response : [$response]))
Loading history...
260
            // Cast to class with only the requested, properties
261
            ->map(fn ($items) => $this->getModel()
262
                ->newFromBuilder(
263
                    $properties === ['*']
264
                        ? (array) $items
265
                        : collect($items)
266
                            ->only($properties)
267 60
                            ->toArray()
268
                )
269 60
                ->setClient($this->getClient()->setDebug(false)))
270 1
            ->setPagination(count: $count, page: $page, pageSize: $pageSize);
271
    }
272
273 59
    /**
274 59
     * Get the model instance being queried.
275
     *
276
     * @throws InvalidRelationshipException
277 59
     */
278
    public function getModel(): Model
279
    {
280
        if (! isset($this->class)) {
281
            throw new InvalidRelationshipException();
282
        }
283
284
        if (! isset($this->model)) {
285 49
            $this->model = (new $this->class([], $this->parentModel))->setClient($this->client);
286
        }
287 49
288 49
        return $this->model;
289
    }
290
291
    /**
292
     * Get the path for the resource with the where filters
293
     *
294
     * @throws InvalidRelationshipException
295
     */
296
    public function getPath(?string $extra = null): ?string
297
    {
298
        $w = (array)$this->wheres;
299
        $id = Arr::pull($w, $this->getModel()->getKeyName());
300
301
        return $this->getModel()
302
            ->getPath($extra . (is_null($id) ? null : '/' . $id), $w);
303
    }
304
305
    /**
306
     * Find specific instance of class
307
     *
308
     * @throws GuzzleException
309 2
     * @throws InvalidRelationshipException
310
     * @throws NoClientException
311 2
     * @throws TokenException
312
     */
313 2
    public function find(int|string $id, array|string $properties = ['*']): Model
314
    {
315
        return $this->whereId($id)
316
            ->get($properties)
317
            ->first();
318
    }
319
320
    /**
321 2
     * Order newest to oldest
322
     */
323 2
    public function latest(?string $column = null): self
324
    {
325
        $column ??= $this->getModel()->getCreatedAtColumn();
326
327
        return $column ? $this->orderByDesc($column) : $this;
328
    }
329
330
    /**
331 3
     * Shortcut to where count
332
     *
333
     * @throws InvalidRelationshipException
334 3
     */
335 3
    public function limit(int|string $count): self
336
    {
337
        return $this->where('count', (int) $count);
338
    }
339
340
    /**
341
     * New up a class instance, but not saved
342
     *
343
     * @throws InvalidRelationshipException
344 27
     */
345
    public function make(?array $attributes = []): Model
346 27
    {
347 2
        // TODO: Make sure that the model supports "creating"
348 2
        return $this->getModel()
349 2
            ->newInstance($attributes);
350 2
    }
351 27
352 27
    /**
353 27
     * Create new Builder instance
354
     *
355
     * @throws ModelNotFoundException
356
     * @throws NoClientException
357
     */
358
    public function newInstance(): self
359
    {
360
        return isset($this->class)
361
            ? (new static())
362 26
                ->setClass($this->class)
363
                ->setClient($this->getClient())
364 26
                ->setParent($this->parentModel)
365 26
            : (new static())
366
                ->setClient($this->getClient())
367
                ->setParent($this->parentModel);
368
    }
369
370
    /**
371 2
     * Create new Builder instance for a specific model
372
     *
373 2
     * @throws ModelNotFoundException
374
     * @throws NoClientException
375 2
     */
376
    public function newInstanceForModel(string $model): self
377
    {
378
        return $this->newInstance()
379
            ->setClass($model);
380
    }
381
382
    /**
383
     * Order oldest to newest
384
     */
385 2
    public function oldest(?string $column = null): self
386
    {
387 2
        $column ??= $this->getModel()->getCreatedAtColumn();
388 2
389
        return $column ? $this->orderBy($column) : $this;
390
    }
391
392
    /**
393
     * Shortcut to where order & orderby with expected parameter
394
     *
395
     * The Halo API is not consistent in the parameter used to orderby
396 1
     *
397
     * @throws InvalidRelationshipException
398 1
     */
399
    public function orderBy(string $column, string $direction = 'asc'): self
400
    {
401
        return $this->where($this->getModel()->getOrderByParameter(), $column)
402
            ->where($this->getModel()->getOrderByDirectionParameter(), $direction !== 'asc');
403
    }
404
405
    /**
406 2
     * Shortcut to where order with direction set to desc
407
     *
408 2
     * @throws InvalidRelationshipException
409 2
     */
410
    public function orderByDesc(string $column): self
411
    {
412
        return $this->orderBy($column, 'desc');
413
    }
414
415
    /**
416
     * Shortcut to where page_no
417 2
     *
418
     * @throws InvalidRelationshipException
419 2
     */
420
    public function page(int|string $number, int|string|null $size = null): self
421
    {
422
        return $this->where('page_no', (int) $number)
423
            ->when($size, fn (self $b): self => $b->paginate($size));
424
    }
425
426
    /**
427 5
     * Shortcut to paginate for UK spelling
428
     *
429 5
     * @throws InvalidRelationshipException
430 5
     */
431
    public function pageinate(int|string|null $size = null): self
432
    {
433
        return $this->paginate($size);
434
    }
435
436
    /**
437
     * Shortcut to where pageinate
438 31
     *
439
     * @throws InvalidRelationshipException
440
     */
441 31
    public function paginate(int|string|null $size = null): self
442 31
    {
443 31
        return $this->unless($size, fn (self $b): self => $b->where('pageinate', false))
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->unless($si...ion(...) { /* ... */ }) could return the type Illuminate\Support\Traits\Conditionable which includes types incompatible with the type-hinted return Spinen\Halo\Support\Builder. Consider adding an additional type-check to rule them out.
Loading history...
444 31
            ->when($size, fn (self $b): self => $b->where('pageinate', true)->where('page_size', (int) $size));
445 31
    }
446 1
447 1
    /**
448
     * Peel of the wrapping property if it exist.
449
     *
450
     * @throws InvalidRelationshipException
451 30
     */
452 30
    protected function peelWrapperPropertyIfNeeded(array $properties): array
453 30
    {
454 30
        // Check for single response
455 30
        if (array_key_exists(
456 1
            $this->getModel()
457 1
                ->getResponseKey(),
458
            $properties
459
        )) {
460 29
            return $properties[$this->getModel()
461
                ->getResponseKey()];
462
        }
463
464
        // Check for collection of responses
465
        if (array_key_exists(
466
            $this->getModel()
467
                ->getResponseCollectionKey(),
468 60
            $properties
469
        )) {
470 60
            return $properties[$this->getModel()
471 1
                ->getResponseCollectionKey()];
472
        }
473
474 59
        return $properties;
475
    }
476 59
477
    /**
478
     * Set the class to cast the response
479
     *
480
     * @throws ModelNotFoundException
481
     */
482 33
    public function setClass(string $class): self
483
    {
484 33
        if (! class_exists($class)) {
485
            throw new ModelNotFoundException(sprintf('The model [%s] not found.', $class));
486 33
        }
487
488
        $this->class = $class;
489
490
        return $this;
491
    }
492
493
    /**
494 1
     * Set the parent model
495
     */
496 1
    public function setParent(?Model $parent): self
497
    {
498
        $this->parentModel = $parent;
499
500
        return $this;
501
    }
502
503
    /**
504 20
     * Shortcut to limit
505
     *
506 20
     * @throws InvalidRelationshipException
507
     */
508
    public function take(int|string $count): self
509 20
    {
510
        return $this->limit($count);
511 5
    }
512
513 5
    /**
514
     * Add property to filter the collection
515
     *
516 15
     * @throws InvalidRelationshipException
517
     */
518 15
    public function where(string $property, $value = true): self
519
    {
520
        $this->wheres[$property] = is_a($value, LaravelCollection::class)
521
            ? $value->toArray()
522
            : $value;
523
524
        return $this;
525
    }
526 5
527
    /**
528 5
     * Shortcut to where property id
529
     *
530
     * @throws InvalidRelationshipException
531
     */
532
    public function whereId(int|string|null $id): self
533
    {
534
        return $this->where($this->getModel()->getKeyName(), $id);
535
    }
536 1
537
    /**
538 1
     * Shortcut to where property is false
539
     *
540
     * @throws InvalidRelationshipException
541
     */
542
    public function whereNot(string $property): self
543
    {
544
        return $this->where($property, false);
545
    }
546
}
547