Test Setup Failed
Push — develop ( 96f007...973626 )
by Jimmy
25:28 queued 10:27
created

Builder::delete()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 2
c 1
b 0
f 0
dl 0
loc 5
ccs 0
cts 0
cp 0
rs 10
cc 1
nc 1
nop 1
crap 2
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\AssetType;
17
use Spinen\Halo\Attachment;
18
use Spinen\Halo\Client;
19
use Spinen\Halo\Concerns\HasClient;
20
use Spinen\Halo\Contract;
21
use Spinen\Halo\Exceptions\InvalidRelationshipException;
22
use Spinen\Halo\Exceptions\ModelNotFoundException;
23
use Spinen\Halo\Exceptions\NoClientException;
24
use Spinen\Halo\Exceptions\TokenException;
25
use Spinen\Halo\Invoice;
26
use Spinen\Halo\Item;
27
use Spinen\Halo\Opportunity;
28
use Spinen\Halo\Organisation;
29
use Spinen\Halo\Outcome;
30
use Spinen\Halo\Priority;
31
use Spinen\Halo\Project;
32
use Spinen\Halo\Quote;
33
use Spinen\Halo\Report;
34
use Spinen\Halo\Site;
35
use Spinen\Halo\Sla;
36
use Spinen\Halo\SoftwareLicence;
37
use Spinen\Halo\Status;
38
use Spinen\Halo\Supplier;
39
use Spinen\Halo\Team;
40
use Spinen\Halo\Ticket;
41
use Spinen\Halo\TicketType;
42
use Spinen\Halo\User;
43
use Spinen\Halo\Webhook;
44
use Spinen\Halo\WebhookEvent;
45
use Spinen\Halo\Workday;
46
47
/**
48
 * Class Builder
49
 *
50
 * @property Agent $agent
51
 * @property Client $client
52
 * @property Collection<int, Action> $actions
53
 * @property Collection<int, Agent> $agents
54
 * @property Collection<int, Appointment> $appointments
55
 * @property Collection<int, Article> $articles
56
 * @property Collection<int, AssetType> $asset_types
57
 * @property Collection<int, Asset> $assets
58
 * @property Collection<int, Attachment> $attachments
59
 * @property Collection<int, Client> $clients
60
 * @property Collection<int, Contract> $contracts
61
 * @property Collection<int, Invoice> $invoices
62
 * @property Collection<int, Item> $items
63
 * @property Collection<int, Opportunity> $opportunities
64
 * @property Collection<int, Organisation> $organisations
65
 * @property Collection<int, Priority> $priorities
66
 * @property Collection<int, Project> $projects
67
 * @property Collection<int, Quote> $quotes
68
 * @property Collection<int, Report> $reports
69
 * @property Collection<int, Site> $sites
70
 * @property Collection<int, Sla> $slas
71
 * @property Collection<int, SoftwareLicence> $software_licences
72
 * @property Collection<int, Status> $statuses
73
 * @property Collection<int, Supplier> $suppliers
74
 * @property Collection<int, Team> $teams
75
 * @property Collection<int, TicketType> $ticket_types
76
 * @property Collection<int, Ticket> $tickets
77
 * @property Collection<int, User> $users
78
 * @property Collection<int, WebhookEvent> $webhook_events
79
 * @property Collection<int, Webhook> $webhooks
80
 * @property Collection<int, Workday> $workdays
81
 * @property User $user
82
 *
83
 * @method self actions()
84
 * @method self agents()
85
 * @method self appointments()
86
 * @method self articles()
87
 * @method self asset_types()
88
 * @method self assets()
89
 * @method self attachments()
90
 * @method self clients()
91
 * @method self contracts()
92
 * @method self invoices()
93
 * @method self items()
94
 * @method self opportunities()
95
 * @method self organisations()
96
 * @method self outcomes()
97
 * @method self priorities()
98
 * @method self projects()
99
 * @method self quotes()
100
 * @method self reports()
101
 * @method self search($for)
102
 * @method self sites()
103
 * @method self slas()
104
 * @method self software_licences()
105
 * @method self statuses()
106
 * @method self suppliers()
107
 * @method self teams()
108
 * @method self ticket_types()
109
 * @method self tickets()
110
 * @method self users()
111
 * @method self webhook_events()
112
 * @method self webhooks()
113
 * @method self workdays()
114
 */
115
class Builder
116
{
117
    use Conditionable;
118
    use HasClient;
119
120
    /**
121
     * Class to cast the response
122
     */
123
    protected string $class;
124
125
    /**
126
     * Debug Guzzle calls
127
     */
128
    protected bool $debug = false;
129
130
    /**
131
     * Model instance
132
     */
133
    protected Model $model;
134
135
    /**
136
     * Parent model instance
137
     */
138
    protected ?Model $parentModel = null;
139
140
    /**
141
     * Map of potential parents with class name
142
     *
143
     * @var array
144
     */
145
    protected $rootModels = [
146
        'actions' => Action::class,
147
        'agents' => Agent::class,
148
        'appointments' => Appointment::class,
149
        'articles' => Article::class,
150
        'asset_types' => AssetType::class,
151
        'assets' => Asset::class,
152
        'attachments' => Attachment::class,
153
        'clients' => Client::class,
154
        'contracts' => Contract::class,
155
        'invoices' => Invoice::class,
156
        'items' => Item::class,
157
        'opportunities' => Opportunity::class,
158
        'organisations' => Organisation::class,
159
        'outcomes' => Outcome::class,
160
        'priorities' => Priority::class,
161
        'projects' => Project::class,
162 24
        'quotes' => Quote::class,
163
        'reports' => Report::class,
164 24
        'sites' => Site::class,
165 23
        'slas' => Sla::class,
166
        'software_licences' => SoftwareLicence::class,
167
        'statuses' => Status::class,
168
        'suppliers' => Supplier::class,
169 1
        'teams' => Team::class,
170
        'ticket_types' => TicketType::class,
171
        'tickets' => Ticket::class,
172
        'users' => User::class,
173 1
        'webhook_events' => WebhookEvent::class,
174
        'webhooks' => Webhook::class,
175
        'workdays' => Workday::class,
176
    ];
177
178
    /**
179
     * Properties to filter the response
180
     */
181
    protected array $wheres = [];
182
183
    /**
184
     * Magic method to make builders for root models
185 26
     *
186
     * @throws BadMethodCallException
187 26
     * @throws ModelNotFoundException
188 26
     * @throws NoClientException
189 26
     */
190 26
    public function __call(string $name, array $arguments)
191 26
    {
192 26
        if (! isset($this->parentModel) && array_key_exists($name, $this->rootModels)) {
193 26
            return $this->newInstanceForModel($this->rootModels[$name]);
194 26
        }
195 26
196 26
        // Alias search or search_anything or searchAnything to where(search|search_anything, for)
197 26
        if (Str::startsWith($name, 'search')) {
198
            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

198
            return $this->where(/** @scrutinizer ignore-type */ ...array_merge([Str::of($name)->snake()->toString()], $arguments));
Loading history...
199
        }
200
201
        throw new BadMethodCallException(sprintf('Call to undefined method [%s]', $name));
202
    }
203
204
    /**
205 2
     * Magic method to make builders appears as properties
206
     *
207 2
     * @throws GuzzleException
208 2
     * @throws InvalidRelationshipException
209 2
     * @throws ModelNotFoundException
210 2
     * @throws NoClientException
211
     * @throws TokenException
212
     */
213
    public function __get(string $name): Collection|Model|null
214
    {
215
        return match (true) {
216
            $name === 'agent' => $this->newInstanceForModel(Agent::class)
217
                ->get(extra: 'me')
218
                ->first(),
219
            $name === 'client' => $this->newInstanceForModel(Client::class)
220
                ->get(extra: 'me')
221
                ->first(),
222
            $name === 'user' => $this->newInstanceForModel(User::class)
223
                ->get(extra: 'me')
224
                ->first(),
225
            ! $this->parentModel && array_key_exists($name, $this->rootModels) => $this->{$name}()
226
                ->get(),
227
            default => null,
228
        };
229
    }
230
231
    /**
232
     * Create instance of class and save via API
233 31
     *
234
     * @throws InvalidRelationshipException
235 31
     */
236
    public function create(array $attributes): Model
237
    {
238 31
        return tap(
239 31
            $this->make($attributes),
240 31
            fn (Model $model): bool => $model->save()
241
        );
242
    }
243
244
    /**
245 31
     * Set debug on the client
246
     *
247
     * This is reset to false after the request
248 31
     */
249
    public function debug(bool $debug = true): self
250 31
    {
251 31
        $this->debug = $debug;
252 31
253 4
        return $this;
254 5
    }
255 5
256 5
    /**
257 31
     * Delete specific instance of class
258 31
     *
259 31
     * @throws NoClientException
260
     * @throws TokenException
261
     */
262
    public function delete(int|string $id): bool
263
    {
264
        //  TODO: Consider allowing $id to be null & deleting all
265
        return $this->make([$this->getModel()->getKeyName() => $id])
266
                    ->delete();
267 60
    }
268
269 60
    /**
270 1
     * Get Collection of class instances that match query
271
     *
272
     * @throws GuzzleException
273 59
     * @throws InvalidRelationshipException
274 59
     * @throws NoClientException
275
     * @throws TokenException
276
     */
277 59
    public function get(array|string $properties = ['*'], ?string $extra = null): Collection|Model
278
    {
279
        $properties = Arr::wrap($properties);
280
        $count = null;
281
        $page = null;
282
        $pageSize = null;
283
284
        // Call API to get the response
285 49
        $response = $this->getClient()
286
            ->setDebug($this->debug)
287 49
            ->request($this->getPath($extra));
288 49
289
        if (
290
            array_key_exists('record_count', $response) &&
291
            array_key_exists('page_no', $response) &&
292
            array_key_exists('page_size', $response)
293
        ) {
294
            $count = $response['record_count'];
295
            $page = $response['page_no'];
296
            $pageSize = $response['page_size'];
297
        }
298
299
        // Peel off the key if exist
300
        $response = $this->peelWrapperPropertyIfNeeded(Arr::wrap($response));
301
302
        // Convert to a collection of filtered objects casted to the class
303
        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

303
        return (new Collection(/** @scrutinizer ignore-type */ (array_values($response) === $response) ? $response : [$response]))
Loading history...
304
            // Cast to class with only the requested, properties
305
            ->map(fn ($items) => $this->getModel()
306
                ->newFromBuilder(
307
                    $properties === ['*']
308
                        ? (array) $items
309 2
                        : collect($items)
310
                            ->only($properties)
311 2
                            ->toArray()
312
                )
313 2
                ->setClient($this->getClient()->setDebug(false)))
314
            ->setPagination(count: $count, page: $page, pageSize: $pageSize);
315
    }
316
317
    /**
318
     * Get the model instance being queried.
319
     *
320
     * @throws InvalidRelationshipException
321 2
     */
322
    public function getModel(): Model
323 2
    {
324
        if (! isset($this->class)) {
325
            throw new InvalidRelationshipException();
326
        }
327
328
        if (! isset($this->model)) {
329
            $this->model = (new $this->class([], $this->parentModel))->setClient($this->client);
330
        }
331 3
332
        return $this->model;
333
    }
334 3
335 3
    /**
336
     * Get the path for the resource with the where filters
337
     *
338
     * @throws InvalidRelationshipException
339
     */
340
    public function getPath(?string $extra = null): ?string
341
    {
342
        $w = (array) $this->wheres;
343
        $id = Arr::pull($w, $this->getModel()->getKeyName());
344 27
345
        return $this->getModel()
346 27
            ->getPath($extra.(is_null($id) ? null : '/'.$id), $w);
347 2
    }
348 2
349 2
    /**
350 2
     * Find specific instance of class
351 27
     *
352 27
     * @throws GuzzleException
353 27
     * @throws InvalidRelationshipException
354
     * @throws NoClientException
355
     * @throws TokenException
356
     */
357
    public function find(int|string $id, array|string $properties = ['*']): Model
358
    {
359
        return $this->whereId($id)
360
            ->get($properties)
361
            ->first();
362 26
    }
363
364 26
    /**
365 26
     * Order newest to oldest
366
     */
367
    public function latest(?string $column = null): self
368
    {
369
        $column ??= $this->getModel()->getCreatedAtColumn();
370
371 2
        return $column ? $this->orderByDesc($column) : $this;
372
    }
373 2
374
    /**
375 2
     * Shortcut to where count
376
     *
377
     * @throws InvalidRelationshipException
378
     */
379
    public function limit(int|string $count): self
380
    {
381
        return $this->where('count', (int) $count);
382
    }
383
384
    /**
385 2
     * New up a class instance, but not saved
386
     *
387 2
     * @throws InvalidRelationshipException
388 2
     */
389
    public function make(?array $attributes = []): Model
390
    {
391
        // TODO: Make sure that the model supports "creating"
392
        return $this->getModel()
393
            ->newInstance($attributes);
394
    }
395
396 1
    /**
397
     * Create new Builder instance
398 1
     *
399
     * @throws ModelNotFoundException
400
     * @throws NoClientException
401
     */
402
    public function newInstance(): self
403
    {
404
        return isset($this->class)
405
            ? (new static())
406 2
                ->setClass($this->class)
407
                ->setClient($this->getClient())
408 2
                ->setParent($this->parentModel)
409 2
            : (new static())
410
                ->setClient($this->getClient())
411
                ->setParent($this->parentModel);
412
    }
413
414
    /**
415
     * Create new Builder instance for a specific model
416
     *
417 2
     * @throws ModelNotFoundException
418
     * @throws NoClientException
419 2
     */
420
    public function newInstanceForModel(string $model): self
421
    {
422
        return $this->newInstance()
423
            ->setClass($model);
424
    }
425
426
    /**
427 5
     * Order oldest to newest
428
     */
429 5
    public function oldest(?string $column = null): self
430 5
    {
431
        $column ??= $this->getModel()->getCreatedAtColumn();
432
433
        return $column ? $this->orderBy($column) : $this;
434
    }
435
436
    /**
437
     * Shortcut to where order & orderby with expected parameter
438 31
     *
439
     * The Halo API is not consistent in the parameter used to orderby
440
     *
441 31
     * @throws InvalidRelationshipException
442 31
     */
443 31
    public function orderBy(string $column, string $direction = 'asc'): self
444 31
    {
445 31
        return $this->where($this->getModel()->getOrderByParameter(), $column)
446 1
            ->where($this->getModel()->getOrderByDirectionParameter(), $direction !== 'asc');
447 1
    }
448
449
    /**
450
     * Shortcut to where order with direction set to desc
451 30
     *
452 30
     * @throws InvalidRelationshipException
453 30
     */
454 30
    public function orderByDesc(string $column): self
455 30
    {
456 1
        return $this->orderBy($column, 'desc');
457 1
    }
458
459
    /**
460 29
     * Shortcut to where page_no
461
     *
462
     * @throws InvalidRelationshipException
463
     */
464
    public function page(int|string $number, int|string|null $size = null): self
465
    {
466
        return $this->where('page_no', (int) $number)
467
            ->when($size, fn (self $b): self => $b->paginate($size));
468 60
    }
469
470 60
    /**
471 1
     * Shortcut to paginate for UK spelling
472
     *
473
     * @throws InvalidRelationshipException
474 59
     */
475
    public function pageinate(int|string|null $size = null): self
476 59
    {
477
        return $this->paginate($size);
478
    }
479
480
    /**
481
     * Shortcut to where pageinate
482 33
     *
483
     * @throws InvalidRelationshipException
484 33
     */
485
    public function paginate(int|string|null $size = null): self
486 33
    {
487
        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...
488
            ->when($size, fn (self $b): self => $b->where('pageinate', true)->where('page_size', (int) $size));
489
    }
490
491
    /**
492
     * Peel of the wrapping property if it exist.
493
     *
494 1
     * @throws InvalidRelationshipException
495
     */
496 1
    protected function peelWrapperPropertyIfNeeded(array $properties): array
497
    {
498
        // TODO: This is causing an issue where some of the models have a
499
        // key matching the name that is not a collection (i.e. outcome)
500
501
        // Check for single response
502
        if (array_key_exists(
503
            $this->getModel()
504 20
                ->getResponseKey(),
505
            $properties
506 20
        )) {
507
            return $properties[$this->getModel()
508
                ->getResponseKey()];
509 20
        }
510
511 5
        // Check for collection of responses
512
        if (array_key_exists(
513 5
            $this->getModel()
514
                ->getResponseCollectionKey(),
515
            $properties
516 15
        )) {
517
            return $properties[$this->getModel()
518 15
                ->getResponseCollectionKey()];
519
        }
520
521
        return $properties;
522
    }
523
524
    /**
525
     * Set the class to cast the response
526 5
     *
527
     * @throws ModelNotFoundException
528 5
     */
529
    public function setClass(string $class): self
530
    {
531
        if (! class_exists($class)) {
532
            throw new ModelNotFoundException(sprintf('The model [%s] not found.', $class));
533
        }
534
535
        $this->class = $class;
536 1
537
        return $this;
538 1
    }
539
540
    /**
541
     * Set the parent model
542
     */
543
    public function setParent(?Model $parent): self
544
    {
545
        $this->parentModel = $parent;
546
547
        return $this;
548
    }
549
550
    /**
551
     * Shortcut to limit
552
     *
553
     * @throws InvalidRelationshipException
554
     */
555
    public function take(int|string $count): self
556
    {
557
        return $this->limit($count);
558
    }
559
560
    /**
561
     * Add property to filter the collection
562
     *
563
     * @throws InvalidRelationshipException
564
     */
565
    public function where(iterable|string $property, $value = true): self
566
    {
567
        is_iterable($property)
568
            // Given multiple properties to set, so recursively call self
569
            ? Collection::wrap($property)->each(
0 ignored issues
show
Bug introduced by
It seems like $property can also be of type string; however, parameter $value of Illuminate\Support\Collection::wrap() does only seem to accept iterable, maybe add an additional type check? ( Ignorable by Annotation )

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

569
            ? Collection::wrap(/** @scrutinizer ignore-type */ $property)->each(
Loading history...
570
                fn ($v, string $p): self => is_numeric($p)
571
                    // No value given, so use default value
572
                    ? $this->where($v, $value)
573
                    // Pass property & value
574
                    : $this->where($p, $v),
575
            )
576
            // Given single property
577
            : $this->wheres[$property] = is_a($value, LaravelCollection::class)
578
                ? $value->toArray()
579
                : $value;
580
581
        return $this;
582
    }
583
584
    /**
585
     * Shortcut to where property id
586
     *
587
     * @throws InvalidRelationshipException
588
     */
589
    public function whereId(int|string|null $id): self
590
    {
591
        return $this->where($this->getModel()->getKeyName(), $id);
592
    }
593
594
    /**
595
     * Shortcut to where property is false
596
     *
597
     * @throws InvalidRelationshipException
598
     */
599
    public function whereNot(string $property): self
600
    {
601
        return $this->where($property, false);
602
    }
603
}
604