Test Failed
Push — develop ( 105ddb...c73f86 )
by Jimmy
49s queued 14s
created

Builder::limit()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 1
c 2
b 0
f 0
dl 0
loc 3
rs 10
cc 1
nc 1
nop 1
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
    public function __call(string $name, array $arguments)
163
    {
164
        if (! isset($this->parentModel) && array_key_exists($name, $this->rootModels)) {
165
            return $this->newInstanceForModel($this->rootModels[$name]);
166
        }
167
168
        // Alias search or search_anything or searchAnything to where(search|search_anything, for)
169
        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
        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
    public function __get(string $name): Collection|Model|null
186
    {
187
        return match (true) {
188
            $name === 'agent' => $this->newInstanceForModel(Agent::class)
189
                ->get(extra: 'me')
190
                ->first(),
191
            $name === 'user' => $this->newInstanceForModel(User::class)
192
                ->get(extra: 'me')
193
                ->first(),
194
            ! $this->parentModel && array_key_exists($name, $this->rootModels) => $this->{$name}()
195
                ->get(),
196
            default => null,
197
        };
198
    }
199
200
    /**
201
     * Create instance of class and save via API
202
     *
203
     * @throws InvalidRelationshipException
204
     */
205
    public function create(array $attributes): Model
206
    {
207
        return tap(
208
            $this->make($attributes),
209
            fn (Model $model): bool => $model->save()
210
        );
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
    public function get(array|string $properties = ['*'], ?string $extra = null): Collection|Model
234
    {
235
        $properties = Arr::wrap($properties);
236
237
        // Call API to get the response
238
        $response = $this->getClient()
239
            ->setDebug($this->debug)
240
            ->request($this->getPath($extra));
241
242
        // TODO: Should we capture record_count?
243
244
        // Peel off the key if exist
245
        $response = $this->peelWrapperPropertyIfNeeded(Arr::wrap($response));
246
247
        // Convert to a collection of filtered objects casted to the class
248
        return (new Collection((array_values($response) === $response) ? $response : [$response]))->map(
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

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