• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Okipa / laravel-table / #1458

16 Nov 2023 11:01AM UTC coverage: 87.209%. Remained the same
#1458

push

php-coveralls

Okipa
* Bumped dependencies
* Fixed PHPStan analysis

21 of 21 new or added lines in 9 files covered. (100.0%)

6 existing lines in 2 files now uncovered.

975 of 1118 relevant lines covered (87.21%)

24.48 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.07
/src/Table.php
1
<?php
2

3
namespace Okipa\LaravelTable;
4

5
use Closure;
6
use Illuminate\Database\Eloquent\Builder;
7
use Illuminate\Database\Eloquent\Model;
8
use Illuminate\Pagination\LengthAwarePaginator;
9
use Illuminate\Support\Collection;
10
use Illuminate\Support\Str;
11
use JsonException;
12
use Okipa\LaravelTable\Abstracts\AbstractFilter;
13
use Okipa\LaravelTable\Abstracts\AbstractHeadAction;
14
use Okipa\LaravelTable\Abstracts\AbstractRowAction;
15
use Okipa\LaravelTable\Exceptions\NoColumnsDeclared;
16

17
/** @SuppressWarnings(PHPMD.ExcessiveClassComplexity) */
18
class Table
19
{
20
    protected Model $model;
21

22
    protected array $eventsToEmitOnLoad = [];
23

24
    protected null|Column $orderColumn = null;
25

26
    protected bool $numberOfRowsPerPageChoiceEnabled;
27

28
    protected array $numberOfRowsPerPageOptions;
29

30
    protected array $filters = [];
31

32
    protected null|AbstractHeadAction $headAction = null;
33

34
    protected null|Closure $rowActionsClosure = null;
35

36
    protected null|Closure $bulkActionsClosure = null;
37

38
    protected null|Closure $queryClosure = null;
39

40
    protected null|Closure $rowClassesClosure = null;
41

42
    protected Collection $columns;
43

44
    protected Collection $results;
45

46
    protected LengthAwarePaginator $rows;
47

48
    public function __construct()
49
    {
50
        $this->numberOfRowsPerPageChoiceEnabled = config('laravel-table.enable_number_of_rows_per_page_choice');
136✔
51
        $this->numberOfRowsPerPageOptions = config('laravel-table.number_of_rows_per_page_default_options');
136✔
52
        $this->columns = collect();
136✔
53
        $this->results = collect();
136✔
54
    }
55

56
    public function model(string $modelClass): self
57
    {
58
        $this->model = app($modelClass);
136✔
59

60
        return $this;
136✔
61
    }
62

63
    public function getModel(): Model
64
    {
UNCOV
65
        return $this->model;
×
66
    }
67

68
    public function emitEventsOnLoad(array $eventsToEmitOnLoad): self
69
    {
70
        $this->eventsToEmitOnLoad = $eventsToEmitOnLoad;
2✔
71

72
        return $this;
2✔
73
    }
74

75
    /** @throws \Okipa\LaravelTable\Exceptions\InvalidColumnSortDirection */
76
    public function reorderable(string $attribute, string $title = null, string $sortDirByDefault = 'asc'): self
77
    {
78
        $orderColumn = Column::make($attribute)->sortable()->sortByDefault($sortDirByDefault);
16✔
79
        if ($title) {
16✔
UNCOV
80
            $orderColumn->title($title);
×
81
        }
82
        $this->orderColumn = $orderColumn;
16✔
83

84
        return $this;
16✔
85
    }
86

87
    public static function make(): self
88
    {
89
        return new self();
136✔
90
    }
91

92
    /** @throws \Okipa\LaravelTable\Exceptions\NoColumnsDeclared */
93
    public function prependReorderColumn(): void
94
    {
95
        $orderColumn = $this->getOrderColumn();
136✔
96
        if ($orderColumn) {
136✔
97
            $this->getColumns()->prepend($orderColumn);
16✔
98
        }
99
    }
100

101
    public function getOrderColumn(): null|Column
102
    {
103
        return $this->orderColumn;
136✔
104
    }
105

106
    /** @throws \Okipa\LaravelTable\Exceptions\NoColumnsDeclared */
107
    public function getColumns(): Collection
108
    {
109
        if ($this->columns->isEmpty()) {
136✔
110
            throw new NoColumnsDeclared($this->model);
2✔
111
        }
112

113
        return $this->columns;
134✔
114
    }
115

116
    public function getReorderConfig(null|string $sortDir): array
117
    {
118
        if (! $this->getOrderColumn()) {
134✔
119
            return [];
118✔
120
        }
121
        $query = $this->model->query();
16✔
122
        // Query
123
        if ($this->queryClosure) {
16✔
124
            $query->where(fn ($subQueryQuery) => ($this->queryClosure)($query));
2✔
125
        }
126
        // Sort
127
        $query->orderBy($this->getOrderColumn()->getAttribute(), $sortDir);
16✔
128

129
        return [
16✔
130
            'modelClass' => $this->model::class,
16✔
131
            'reorderAttribute' => $this->getOrderColumn()->getAttribute(),
16✔
132
            'sortDir' => $sortDir,
16✔
133
            'beforeReorderAllModelKeysWithPosition' => $query
16✔
134
                ->get()
16✔
135
                ->map(fn (Model $model) => [
16✔
136
                    'modelKey' => (string) $model->getKey(),
16✔
137
                    'position' => $model->getAttribute($this->getOrderColumn()->getAttribute()),
16✔
138
                ])
16✔
139
                ->toArray(),
16✔
140
        ];
16✔
141
    }
142

143
    public function query(Closure $queryClosure): self
144
    {
145
        $this->queryClosure = $queryClosure;
8✔
146

147
        return $this;
8✔
148
    }
149

150
    /** @throws \Okipa\LaravelTable\Exceptions\NoColumnsDeclared */
151
    public function prepareQuery(
152
        array $filterClosures,
153
        null|string $searchBy,
154
        string|Closure|null $sortBy,
155
        null|string $sortDir
156
    ): Builder {
157
        $query = $this->model->query();
134✔
158
        // Query
159
        if ($this->queryClosure) {
134✔
160
            $query->where(fn (Builder $subQueryQuery) => ($this->queryClosure)($query));
8✔
161
        }
162
        // Filters
163
        if ($filterClosures) {
134✔
164
            $query->where(function (Builder $subFiltersQuery) use ($filterClosures) {
6✔
165
                foreach ($filterClosures as $filterClosure) {
6✔
166
                    $filterClosure($subFiltersQuery);
6✔
167
                }
168
            });
6✔
169
        }
170
        // Search
171
        if ($searchBy) {
134✔
172
            $query->where(function (Builder $subSearchQuery) use ($searchBy) {
14✔
173
                $this->getSearchableColumns()
14✔
174
                    ->each(function (Column $searchableColumn) use ($subSearchQuery, $searchBy) {
14✔
175
                        $searchableClosure = $searchableColumn->getSearchableClosure();
14✔
176
                        $searchableClosure
14✔
177
                            ? $subSearchQuery->orWhere(fn (Builder $orWhereQuery) => ($searchableClosure)(
2✔
178
                                $orWhereQuery,
2✔
179
                                $searchBy
2✔
180
                            ))
2✔
181
                            : $subSearchQuery->orWhereRaw(
14✔
182
                                $this->getSearchSqlStatement($searchableColumn->getAttribute()),
14✔
183
                                ['%' . Str::of($searchBy)->trim()->lower() . '%']
14✔
184
                            );
14✔
185
                    });
14✔
186
            });
14✔
187
        }
188
        // Sort
189
        if ($sortBy && $sortDir) {
134✔
190
            $sortBy instanceof Closure
28✔
191
                ? $sortBy($query, $sortDir)
2✔
192
                : $query->orderBy($sortBy, $sortDir);
26✔
193
        }
194

195
        return $query;
134✔
196
    }
197

198
    /** @throws \Okipa\LaravelTable\Exceptions\NoColumnsDeclared */
199
    protected function getSearchableColumns(): Collection
200
    {
201
        return $this->getColumns()->filter(fn (Column $column) => $column->isSearchable());
136✔
202
    }
203

204
    protected function getSearchSqlStatement(string $attribute): string
205
    {
206
        $connection = config('database.default');
14✔
207
        $driver = config('database.connections.' . $connection . '.driver');
14✔
208

209
        return $this->getSqlLowerFunction($driver, $attribute) . ' '
14✔
210
            . $this->getSqlCaseInsensitiveSearchingLikeOperator($driver) . ' ?';
14✔
211
    }
212

213
    protected function getSqlLowerFunction(string $driver, string $attribute): string
214
    {
215
        return $driver === 'pgsql' ? 'LOWER(CAST(' . $attribute . ' AS TEXT))' : 'LOWER(' . $attribute . ')';
14✔
216
    }
217

218
    protected function getSqlCaseInsensitiveSearchingLikeOperator(string $driver): string
219
    {
220
        return $driver === 'pgsql' ? 'ILIKE' : 'LIKE';
14✔
221
    }
222

223
    public function getRows(): LengthAwarePaginator
224
    {
225
        return $this->rows;
134✔
226
    }
227

228
    public function triggerEventsEmissionOnLoad(Livewire\Table $table): void
229
    {
230
        foreach ($this->eventsToEmitOnLoad as $event => $params) {
136✔
231
            $eventName = is_string($event) ? $event : $params;
2✔
232
            $eventParams = is_array($params) ? $params : [];
2✔
233
            $table->emit($eventName, $eventParams);
2✔
234
        }
235
    }
236

237
    public function enableNumberOfRowsPerPageChoice(bool $numberOfRowsPerPageChoiceEnabled): self
238
    {
239
        $this->numberOfRowsPerPageChoiceEnabled = $numberOfRowsPerPageChoiceEnabled;
4✔
240

241
        return $this;
4✔
242
    }
243

244
    public function isNumberOfRowsPerPageChoiceEnabled(): bool
245
    {
246
        return $this->numberOfRowsPerPageChoiceEnabled;
134✔
247
    }
248

249
    public function numberOfRowsPerPageOptions(array $numberOfRowsPerPageOptions): self
250
    {
251
        $this->numberOfRowsPerPageOptions = $numberOfRowsPerPageOptions;
16✔
252

253
        return $this;
16✔
254
    }
255

256
    public function getNumberOfRowsPerPageOptions(): array
257
    {
258
        return $this->numberOfRowsPerPageOptions;
134✔
259
    }
260

261
    public function filters(array $filters): self
262
    {
263
        $this->filters = $filters;
8✔
264

265
        return $this;
8✔
266
    }
267

268
    public function headAction(AbstractHeadAction $headAction): self
269
    {
270
        $this->headAction = $headAction;
4✔
271

272
        return $this;
4✔
273
    }
274

275
    public function rowActions(Closure $rowActionsClosure): self
276
    {
277
        $this->rowActionsClosure = $rowActionsClosure;
6✔
278

279
        return $this;
6✔
280
    }
281

282
    public function bulkActions(Closure $bulkActionsClosure): self
283
    {
284
        $this->bulkActionsClosure = $bulkActionsClosure;
10✔
285

286
        return $this;
10✔
287
    }
288

289
    public function rowClass(Closure $rowClassesClosure): self
290
    {
291
        $this->rowClassesClosure = $rowClassesClosure;
2✔
292

293
        return $this;
2✔
294
    }
295

296
    public function columns(array $columns): void
297
    {
298
        $this->columns = collect($columns);
136✔
299
    }
300

301
    public function results(array $results): void
302
    {
303
        $this->results = collect($results);
136✔
304
    }
305

306
    /** @throws \Okipa\LaravelTable\Exceptions\NoColumnsDeclared */
307
    public function getColumnSortedByDefault(): null|Column
308
    {
309
        $sortableColumns = $this->getColumns()
134✔
310
            ->filter(fn (Column $column) => $column->isSortable($this->getOrderColumn()));
134✔
311
        if ($sortableColumns->isEmpty()) {
134✔
312
            return null;
106✔
313
        }
314
        $columnSortedByDefault = $sortableColumns->filter(fn (Column $column) => $column->isSortedByDefault())->first();
28✔
315
        if (! $columnSortedByDefault) {
28✔
316
            return $sortableColumns->first();
6✔
317
        }
318

319
        return $columnSortedByDefault;
22✔
320
    }
321

322
    /** @throws \Okipa\LaravelTable\Exceptions\NoColumnsDeclared */
323
    public function getColumn(string $attribute): Column
324
    {
325
        return $this->getColumns()->filter(fn (Column $column) => $column->getAttribute() === $attribute)->first();
12✔
326
    }
327

328
    public function paginateRows(Builder $query, int $numberOfRowsPerPage): void
329
    {
330
        $this->rows = $query->paginate($numberOfRowsPerPage);
134✔
331
        $this->rows->transform(function (Model $model) {
134✔
332
            $model->laravel_table_unique_identifier = Str::uuid()->getInteger()->toString();
98✔
333

334
            return $model;
98✔
335
        });
134✔
336
    }
337

338
    public function computeResults(Collection $displayedRows): void
339
    {
340
        $this->results = $this->results->map(fn (Result $result) => $result->compute(
134✔
341
            $this->model->query()->toBase(),
134✔
342
            $displayedRows,
134✔
343
        ));
134✔
344
    }
345

346
    public function generateFiltersArray(): array
347
    {
348
        return collect($this->filters)->map(function (AbstractFilter $filter) {
134✔
349
            $filter->setup($this->model->getKeyName());
8✔
350

351
            return json_decode(json_encode(
8✔
352
                $filter,
8✔
353
                JSON_THROW_ON_ERROR
8✔
354
            ), true, 512, JSON_THROW_ON_ERROR);
8✔
355
        })->toArray();
134✔
356
    }
357

358
    public function getFilterClosures(array $filtersArray, array $selectedFilters): array
359
    {
360
        $filterClosures = [];
134✔
361
        foreach ($selectedFilters as $identifier => $value) {
134✔
362
            if ($value === '' || $value === []) {
6✔
363
                continue;
3✔
364
            }
365
            $filterArray = AbstractFilter::retrieve($filtersArray, $identifier);
6✔
366
            $filterInstance = AbstractFilter::make($filterArray);
6✔
367
            $filterClosures[$identifier] = static fn (Builder $query) => $filterInstance->filter($query, $value);
6✔
368
        }
369

370
        return $filterClosures;
134✔
371
    }
372

373
    public function getHeadActionArray(): array
374
    {
375
        if (! $this->headAction) {
134✔
376
            return [];
130✔
377
        }
378
        $this->headAction->setup();
4✔
379
        if (! $this->headAction->isAllowed()) {
4✔
380
            return [];
2✔
381
        }
382

383
        return (array) $this->headAction;
2✔
384
    }
385

386
    /** @throws JsonException */
387
    public function getRowClass(): array
388
    {
389
        $tableRowClass = [];
134✔
390
        if (! $this->rowClassesClosure) {
134✔
391
            return $tableRowClass;
132✔
392
        }
393
        foreach ($this->rows->getCollection() as $model) {
2✔
394
            $tableRowClass[$model->laravel_table_unique_identifier] = ($this->rowClassesClosure)($model);
2✔
395
        }
396

397
        return json_decode(json_encode(
2✔
398
            $tableRowClass,
2✔
399
            JSON_THROW_ON_ERROR
2✔
400
        ), true, 512, JSON_THROW_ON_ERROR);
2✔
401
    }
402

403
    /** @throws JsonException */
404
    public function generateBulkActionsArray(array $selectedModelKeys): array
405
    {
406
        $tableBulkActionsArray = [];
134✔
407
        $tableRawBulkActionsArray = [];
134✔
408
        if (! $this->bulkActionsClosure) {
134✔
409
            return $tableBulkActionsArray;
124✔
410
        }
411
        $bulkActionsModelKeys = [];
10✔
412
        foreach ($this->rows as $index => $model) {
10✔
413
            $modelBulkActions = collect(($this->bulkActionsClosure)($model));
10✔
414
            foreach ($modelBulkActions as $modelBulkAction) {
10✔
415
                $modelBulkAction->setup($model);
10✔
416
                if (! $index) {
10✔
417
                    $tableRawBulkActionsArray[] = json_decode(json_encode(
10✔
418
                        $modelBulkAction,
10✔
419
                        JSON_THROW_ON_ERROR
10✔
420
                    ), true, 512, JSON_THROW_ON_ERROR);
10✔
421
                }
422
                if (! in_array((string) $model->getKey(), $selectedModelKeys, true)) {
10✔
423
                    continue;
6✔
424
                }
425
                $modelBulkAction->isAllowed()
8✔
426
                    ? $bulkActionsModelKeys[$modelBulkAction->identifier]['allowed'][] = $model->getKey()
8✔
427
                    : $bulkActionsModelKeys[$modelBulkAction->identifier]['disallowed'][] = $model->getKey();
2✔
428
            }
429
        }
430
        foreach ($tableRawBulkActionsArray as $tableBulkActionArray) {
10✔
431
            $identifier = $tableBulkActionArray['identifier'];
10✔
432
            $tableBulkActionArray['allowedModelKeys'] = $bulkActionsModelKeys[$identifier]['allowed'] ?? [];
10✔
433
            $tableBulkActionArray['disallowedModelKeys'] = $bulkActionsModelKeys[$identifier]['disallowed'] ?? [];
10✔
434
            $tableBulkActionsArray[] = $tableBulkActionArray;
10✔
435
        }
436

437
        return $tableBulkActionsArray;
10✔
438
    }
439

440
    public function generateRowActionsArray(): array
441
    {
442
        $tableRowActionsArray = [];
134✔
443
        if (! $this->rowActionsClosure) {
134✔
444
            return $tableRowActionsArray;
128✔
445
        }
446
        foreach ($this->rows->getCollection() as $model) {
6✔
447
            $rowActions = collect(($this->rowActionsClosure)($model))
6✔
448
                ->filter(fn (AbstractRowAction $rowAction) => $rowAction->isAllowed());
6✔
449
            $rowActionsArray = $rowActions->map(static function (AbstractRowAction $rowAction) use ($model) {
6✔
450
                $rowAction->setup($model);
6✔
451

452
                return json_decode(json_encode(
6✔
453
                    $rowAction,
6✔
454
                    JSON_THROW_ON_ERROR
6✔
455
                ), true, 512, JSON_THROW_ON_ERROR);
6✔
456
            })->toArray();
6✔
457
            $tableRowActionsArray = [...$tableRowActionsArray, ...$rowActionsArray];
6✔
458
        }
459

460
        return $tableRowActionsArray;
6✔
461
    }
462

463
    /**
464
     * @throws \Okipa\LaravelTable\Exceptions\NoColumnsDeclared
465
     * @throws JsonException
466
     */
467
    public function generateColumnActionsArray(): array
468
    {
469
        $tableColumnActionsArray = [];
134✔
470
        foreach ($this->rows->getCollection() as $model) {
134✔
471
            $columnActions = $this->getColumns()
98✔
472
                ->mapWithKeys(fn (Column $column) => [
98✔
473
                    $column->getAttribute() => $column->getAction()
98✔
474
                        ? $column->getAction()($model)
8✔
475
                        : null,
476
                ])
98✔
477
                ->filter();
98✔
478
            foreach ($columnActions as $attribute => $columnAction) {
98✔
479
                $columnAction->setup($model, $attribute);
8✔
480
                $tableColumnActionsArray[] = json_decode(json_encode(
8✔
481
                    $columnAction,
8✔
482
                    JSON_THROW_ON_ERROR
8✔
483
                ), true, 512, JSON_THROW_ON_ERROR);
8✔
484
            }
485
        }
486

487
        return $tableColumnActionsArray;
134✔
488
    }
489

490
    /** @throws \Okipa\LaravelTable\Exceptions\NoColumnsDeclared */
491
    public function getSearchableLabels(): string
492
    {
493
        return $this->getSearchableColumns()
136✔
494
            ->map(fn (Column $searchableColumn) => ['title' => $searchableColumn->getTitle()])
136✔
495
            ->implode('title', ', ');
136✔
496
    }
497

498
    public function getResults(): Collection
499
    {
500
        return $this->results;
134✔
501
    }
502

503
    public function getNavigationStatus(): string
504
    {
505
        return __('Showing results <b>:start</b> to <b>:stop</b> on <b>:total</b>', [
134✔
506
            'start' => $this->rows->isNotEmpty()
134✔
507
                ? ($this->rows->perPage() * ($this->rows->currentPage() - 1)) + 1
98✔
508
                : 0,
134✔
509
            'stop' => $this->rows->count() + (($this->rows->currentPage() - 1) * $this->rows->perPage()),
134✔
510
            'total' => $this->rows->total(),
134✔
511
        ]);
134✔
512
    }
513
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc