Skip to content

Commit 2d624af

Browse files
Support Laravel Scout Builder (#1582)
* Add Scout Builder support * Add typesense.yml * Add typesense.yml * Add typesense.yml * Disable typesense * Add Builder Macro paginateSafe * Add Builder Macro paginateSafe * phpstan fixes
1 parent 9125e36 commit 2d624af

File tree

7 files changed

+180
-16
lines changed

7 files changed

+180
-16
lines changed

.github/workflows/postgres.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: PostgreSQL
1+
name: PostGreSQL
22

33
on:
44
push:

.github/workflows/typesense.yml

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
name: Scout Typesense
2+
3+
on: workflow_dispatch
4+
5+
jobs:
6+
build:
7+
runs-on: ubuntu-latest
8+
9+
services:
10+
mysql:
11+
image: mysql:5.7
12+
env:
13+
MYSQL_ROOT_PASSWORD: password
14+
MYSQL_DATABASE: powergridtest
15+
ports:
16+
- 3307:3306
17+
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
18+
19+
strategy:
20+
fail-fast: false
21+
matrix:
22+
php: [8.3]
23+
laravel: [11.*]
24+
dependency-version: [ prefer-stable ]
25+
26+
name: PHP:${{ matrix.php }} / L:${{ matrix.laravel }}
27+
28+
if: github.ref != 'refs/heads/todo-tests'
29+
30+
steps:
31+
- name: Checkout
32+
uses: actions/checkout@v4
33+
34+
- name: Setup PHP, with composer and extensions
35+
uses: shivammathur/setup-php@v2
36+
with:
37+
php-version: ${{ matrix.php }}
38+
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv
39+
tools: composer:v2
40+
coverage: none
41+
42+
- name: Get composer cache directory
43+
id: composer-cache
44+
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
45+
46+
- name: Cache composer dependencies
47+
uses: actions/cache@v4
48+
with:
49+
path: $(composer config cache-files-dir)
50+
key: dependencies-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }}
51+
52+
- name: Install Typesense
53+
run: |
54+
curl -O https://dl.typesense.org/releases/26.0/typesense-server-26.0-arm64.deb
55+
curl -O https://dl.typesense.org/releases/26.0/typesense-server-26.0-amd64.deb
56+
sudo apt install ./typesense-server-26.0-amd64.deb
57+
58+
- name: Install Composer dependencies
59+
run: |
60+
composer require "laravel/framework:${{ matrix.laravel }}" --no-interaction --no-update
61+
composer require laravel/scout
62+
composer require typesense/typesense-php
63+
composer install
64+
65+
- name: Tests
66+
run: composer test:typesense

composer.json

+5-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@
2929
"larastan/larastan": "^2.9.0",
3030
"pestphp/pest": "^2.34.0",
3131
"orchestra/testbench": "8.19|^9.0",
32-
"laradumps/laradumps": "^3.1"
32+
"laradumps/laradumps": "^3.1",
33+
"laravel/scout": "^10.9"
3334
},
3435
"suggest": {
3536
"openspout/openspout": "Required to export XLS and CSV"
@@ -76,6 +77,9 @@
7677
"test:sqlsrv": [
7778
"./vendor/bin/pest --configuration phpunit.sqlsrv.xml"
7879
],
80+
"test:typesense": [
81+
"curl http://localhost:8108/health"
82+
],
7983
"test:types": "./vendor/bin/phpstan analyse --ansi --memory-limit=-1",
8084
"test:dbs": [
8185
"@test:sqlite",

phpunit.typesense.xml

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.1/phpunit.xsd" bootstrap="vendor/autoload.php" colors="true" cacheDirectory=".phpunit.cache">
3+
<testsuites>
4+
<testsuite name="Test Suite">
5+
<directory suffix="Test.php">./tests/Feature</directory>
6+
</testsuite>
7+
</testsuites>
8+
<coverage/>
9+
<php>
10+
<server name="SCOUT_DRIVER" value="typesense"/>
11+
<server name="TYPESENSE_HOST" value="localhost"/>
12+
<server name="TYPESENSE_PORT" value="8108"/>
13+
<server name="TYPESENSE_PROTOCOL" value="http"/>
14+
<server name="TYPESENSE_API_KEY" value="xyz"/>
15+
</php>
16+
<source>
17+
<include>
18+
<directory suffix=".php">./src</directory>
19+
</include>
20+
</source>
21+
</phpunit>

src/ProcessDataSource.php

+43-12
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
use Illuminate\Database\Eloquent\{Builder as EloquentBuilder, Model};
88
use Illuminate\Database\Query\Builder as QueryBuilder;
99
use Illuminate\Pagination\Paginator;
10-
use Illuminate\Support\{Collection as BaseCollection, Facades\DB, Str};
10+
use Illuminate\Support\{Collection as BaseCollection, Facades\DB, Str, Stringable};
11+
use Laravel\Scout\Builder as ScoutBuilder;
1112
use PowerComponents\LivewirePowerGrid\Components\Actions\ActionsController;
1213
use PowerComponents\LivewirePowerGrid\Components\Rules\{RulesController};
1314
use PowerComponents\LivewirePowerGrid\DataSource\{Builder, Collection};
@@ -18,8 +19,6 @@ class ProcessDataSource
1819
{
1920
use Concerns\SoftDeletes;
2021

21-
public bool $isCollection = false;
22-
2322
private array $queryLog = [];
2423

2524
public function __construct(
@@ -49,16 +48,46 @@ public function get(bool $isExport = false): Paginator|LengthAwarePaginator|\Ill
4948
return $this->processCollection($datasource, $isExport);
5049
}
5150

51+
if ($datasource instanceof ScoutBuilder) {
52+
return $this->processScoutCollection($datasource);
53+
}
54+
5255
$this->setCurrentTable($datasource);
5356

54-
/** @phpstan-ignore-next-line */
55-
return $this->processModel($datasource);
57+
return $this->processModel($datasource); // @phpstan-ignore-line
5658
}
5759

58-
/**
59-
* @return EloquentBuilder|BaseCollection|Collection|QueryBuilder|MorphToMany|null
60-
*/
61-
public function prepareDataSource(): EloquentBuilder|BaseCollection|Collection|QueryBuilder|MorphToMany|null
60+
public function processScoutCollection(ScoutBuilder $datasource): Paginator|LengthAwarePaginator
61+
{
62+
$datasource->query = Str::of($datasource->query)
63+
->when($this->component->search != '', fn (Stringable $self) => $self
64+
->prepend($this->component->search . ','))
65+
->toString();
66+
67+
collect($this->component->filters)->each(fn (array $filters) => collect($filters)
68+
->each(fn (string $value, string $field) => $datasource
69+
->where($field, $value)));
70+
71+
if ($this->component->multiSort) {
72+
foreach ($this->component->sortArray as $sortField => $direction) {
73+
$datasource->orderBy($sortField, $direction);
74+
}
75+
} else {
76+
$datasource->orderBy($this->component->sortField, $this->component->sortDirection);
77+
}
78+
79+
$results = self::applyPerPage($datasource);
80+
81+
if (method_exists($results, 'total')) {
82+
$this->component->total = $results->total();
83+
}
84+
85+
return $results->setCollection( // @phpstan-ignore-line
86+
$this->transform($results->getCollection(), $this->component) // @phpstan-ignore-line
87+
);
88+
}
89+
90+
public function prepareDataSource(): EloquentBuilder|BaseCollection|Collection|QueryBuilder|MorphToMany|ScoutBuilder|null
6291
{
6392
$datasource = $this->component->datasource ?? null;
6493

@@ -70,8 +99,6 @@ public function prepareDataSource(): EloquentBuilder|BaseCollection|Collection|Q
7099
$datasource = collect($datasource);
71100
}
72101

73-
$this->isCollection = $datasource instanceof BaseCollection;
74-
75102
return $datasource;
76103
}
77104

@@ -226,7 +253,7 @@ private function applyWithSortStringNumber(
226253
return $results;
227254
}
228255

229-
private function applyPerPage(EloquentBuilder|QueryBuilder|MorphToMany $results): LengthAwarePaginator|Paginator
256+
private function applyPerPage(EloquentBuilder|QueryBuilder|MorphToMany|ScoutBuilder $results): LengthAwarePaginator|Paginator
230257
{
231258
$pageName = strval(data_get($this->component->setUp, 'footer.pageName', 'page'));
232259
$perPage = intval(data_get($this->component->setUp, 'footer.perPage'));
@@ -237,6 +264,10 @@ private function applyPerPage(EloquentBuilder|QueryBuilder|MorphToMany $results)
237264
default => 'paginate',
238265
};
239266

267+
if ($results instanceof ScoutBuilder) {
268+
return $results->paginateSafe($perPage, pageName: $pageName); // @phpstan-ignore-line
269+
}
270+
240271
if ($perPage > 0) {
241272
return $results->$paginate($perPage, pageName: $pageName);
242273
}

src/Providers/PowerGridServiceProvider.php

+43-1
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@
22

33
namespace PowerComponents\LivewirePowerGrid\Providers;
44

5+
use Illuminate\Container\Container;
56
use Illuminate\Database\Events\MigrationsEnded;
7+
use Illuminate\Pagination\{LengthAwarePaginator, Paginator};
68
use Illuminate\Support\Facades\{Blade, Event};
79
use Illuminate\Support\ServiceProvider;
10+
use Laravel\Scout\Builder;
11+
use Laravel\Scout\Contracts\PaginatesEloquentModels;
812
use Livewire\Features\SupportLegacyModels\{EloquentCollectionSynth, EloquentModelSynth};
913
use Livewire\Livewire;
1014
use PowerComponents\LivewirePowerGrid\Commands\CheckDependenciesCommand;
@@ -64,7 +68,7 @@ public function register(): void
6468
Livewire::component('powergrid-performance-card', PerformanceCard::class);
6569
}
6670

67-
Macros::boot();
71+
$this->macros();
6872
}
6973

7074
private function publishViews(): void
@@ -89,4 +93,42 @@ private function publishConfigs(): void
8993

9094
$this->publishes([__DIR__ . '/../../resources/lang' => lang_path('vendor/' . $this->packageName)], $this->packageName . '-lang');
9195
}
96+
97+
private function macros(): void
98+
{
99+
Macros::boot();
100+
101+
if (class_exists(\Laravel\Scout\Builder::class)) {
102+
Builder::macro('paginateSafe', function ($perPage = null, $pageName = 'page', $page = null) {
103+
$engine = $this->engine(); // @phpstan-ignore-line
104+
105+
if ($engine instanceof PaginatesEloquentModels) {
106+
return $engine->paginate($this, $perPage, $page)->appends('query', $this->query);
107+
}
108+
109+
$page = $page ?: Paginator::resolveCurrentPage($pageName);
110+
111+
$perPage = $perPage ?: $this->model->getPerPage();
112+
113+
$results = $this->model->newCollection(
114+
$engine->map(
115+
$this,
116+
$rawResults = $engine->paginate($this, $perPage, $page),
117+
$this->model
118+
)->all()
119+
);
120+
121+
return Container::getInstance()->makeWith(LengthAwarePaginator::class, [
122+
'items' => $results,
123+
'total' => $engine->getTotalCount($rawResults),
124+
'perPage' => $perPage,
125+
'currentPage' => $page,
126+
'options' => [
127+
'path' => Paginator::resolveCurrentPath(),
128+
'pageName' => $pageName,
129+
],
130+
])->appends('query', $this->query);
131+
});
132+
}
133+
}
92134
}

src/Traits/WithExport.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ public function prepareToExport(bool $selected = false): Eloquent\Collection|Sup
168168
$inClause = $processDataSource->component->checkboxValues;
169169
}
170170

171-
if ($processDataSource->isCollection) {
171+
if ($processDataSource->component->datasource() instanceof Collection) {
172172
if ($inClause) {
173173
$results = $processDataSource->get(isExport: true)->whereIn($this->primaryKey, $inClause);
174174

0 commit comments

Comments
 (0)