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

Okipa / laravel-table / #1363

pending completion
#1363

push

php-coveralls

web-flow
V5: fix searching with Postgres (#130)

* Fixed searching SQL request for PostgreSQL by casting every values into text (fixes #129)

4 of 4 new or added lines in 1 file covered. (100.0%)

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 Okipa\LaravelTable\Abstracts\AbstractFilter;
12
use Okipa\LaravelTable\Abstracts\AbstractHeadAction;
13
use Okipa\LaravelTable\Abstracts\AbstractRowAction;
14
use Okipa\LaravelTable\Exceptions\NoColumnsDeclared;
15

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

21
    protected array $eventsToEmitOnLoad = [];
22

23
    protected Column|null $orderColumn = null;
24

25
    protected bool $numberOfRowsPerPageChoiceEnabled;
26

27
    protected array $numberOfRowsPerPageOptions;
28

29
    protected array $filters = [];
30

31
    protected AbstractHeadAction|null $headAction = null;
32

33
    protected Closure|null $rowActionsClosure = null;
34

35
    protected Closure|null $bulkActionsClosure = null;
36

37
    protected Closure|null $queryClosure = null;
38

39
    protected Closure|null $rowClassesClosure = null;
40

41
    protected Collection $columns;
42

43
    protected Collection $results;
44

45
    protected LengthAwarePaginator $rows;
46

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

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

59
        return $this;
136✔
60
    }
61

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

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

71
        return $this;
2✔
72
    }
73

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

83
        return $this;
16✔
84
    }
85

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

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

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

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

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

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

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

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

146
        return $this;
8✔
147
    }
148

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

194
        return $query;
134✔
195
    }
196

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

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

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

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

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

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

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

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

240
        return $this;
4✔
241
    }
242

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

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

252
        return $this;
16✔
253
    }
254

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

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

264
        return $this;
8✔
265
    }
266

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

271
        return $this;
4✔
272
    }
273

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

278
        return $this;
6✔
279
    }
280

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

285
        return $this;
10✔
286
    }
287

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

292
        return $this;
2✔
293
    }
294

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

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

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

318
        return $columnSortedByDefault;
22✔
319
    }
320

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

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

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

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

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

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

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

369
        return $filterClosures;
134✔
370
    }
371

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

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

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

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

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

436
        return $tableBulkActionsArray;
10✔
437
    }
438

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

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

459
        return $tableRowActionsArray;
6✔
460
    }
461

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

486
        return $tableColumnActionsArray;
134✔
487
    }
488

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

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

502
    public function getNavigationStatus(): string
503
    {
504
        return __('Showing results <b>:start</b> to <b>:stop</b> on <b>:total</b>', [
134✔
505
            'start' => $this->rows->isNotEmpty()
134✔
506
                ? ($this->rows->perPage() * ($this->rows->currentPage() - 1)) + 1
98✔
507
                : 0,
134✔
508
            'stop' => $this->rows->count() + (($this->rows->currentPage() - 1) * $this->rows->perPage()),
134✔
509
            'total' => $this->rows->total(),
134✔
510
        ]);
134✔
511
    }
512
}
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