From 2d624af21dcf90c1a600876a9cc9da8d46187fc6 Mon Sep 17 00:00:00 2001 From: Luan Freitas <33601626+luanfreitasdev@users.noreply.github.com> Date: Sat, 1 Jun 2024 15:10:48 -0300 Subject: [PATCH] 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 --- .github/workflows/postgres.yml | 2 +- .github/workflows/typesense.yml | 66 ++++++++++++++++++++++ composer.json | 6 +- phpunit.typesense.xml | 21 +++++++ src/ProcessDataSource.php | 55 ++++++++++++++---- src/Providers/PowerGridServiceProvider.php | 44 ++++++++++++++- src/Traits/WithExport.php | 2 +- 7 files changed, 180 insertions(+), 16 deletions(-) create mode 100644 .github/workflows/typesense.yml create mode 100644 phpunit.typesense.xml diff --git a/.github/workflows/postgres.yml b/.github/workflows/postgres.yml index d1bc33883..b0f674470 100644 --- a/.github/workflows/postgres.yml +++ b/.github/workflows/postgres.yml @@ -1,4 +1,4 @@ -name: PostgreSQL +name: PostGreSQL on: push: diff --git a/.github/workflows/typesense.yml b/.github/workflows/typesense.yml new file mode 100644 index 000000000..24677d4cd --- /dev/null +++ b/.github/workflows/typesense.yml @@ -0,0 +1,66 @@ +name: Scout Typesense + +on: workflow_dispatch + +jobs: + build: + runs-on: ubuntu-latest + + services: + mysql: + image: mysql:5.7 + env: + MYSQL_ROOT_PASSWORD: password + MYSQL_DATABASE: powergridtest + ports: + - 3307:3306 + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + + strategy: + fail-fast: false + matrix: + php: [8.3] + laravel: [11.*] + dependency-version: [ prefer-stable ] + + name: PHP:${{ matrix.php }} / L:${{ matrix.laravel }} + + if: github.ref != 'refs/heads/todo-tests' + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP, with composer and extensions + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv + tools: composer:v2 + coverage: none + + - name: Get composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache composer dependencies + uses: actions/cache@v4 + with: + path: $(composer config cache-files-dir) + key: dependencies-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} + + - name: Install Typesense + run: | + curl -O https://dl.typesense.org/releases/26.0/typesense-server-26.0-arm64.deb + curl -O https://dl.typesense.org/releases/26.0/typesense-server-26.0-amd64.deb + sudo apt install ./typesense-server-26.0-amd64.deb + + - name: Install Composer dependencies + run: | + composer require "laravel/framework:${{ matrix.laravel }}" --no-interaction --no-update + composer require laravel/scout + composer require typesense/typesense-php + composer install + + - name: Tests + run: composer test:typesense diff --git a/composer.json b/composer.json index 5cf944980..28eb8757e 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,8 @@ "larastan/larastan": "^2.9.0", "pestphp/pest": "^2.34.0", "orchestra/testbench": "8.19|^9.0", - "laradumps/laradumps": "^3.1" + "laradumps/laradumps": "^3.1", + "laravel/scout": "^10.9" }, "suggest": { "openspout/openspout": "Required to export XLS and CSV" @@ -76,6 +77,9 @@ "test:sqlsrv": [ "./vendor/bin/pest --configuration phpunit.sqlsrv.xml" ], + "test:typesense": [ + "curl http://localhost:8108/health" + ], "test:types": "./vendor/bin/phpstan analyse --ansi --memory-limit=-1", "test:dbs": [ "@test:sqlite", diff --git a/phpunit.typesense.xml b/phpunit.typesense.xml new file mode 100644 index 000000000..c9b275a89 --- /dev/null +++ b/phpunit.typesense.xml @@ -0,0 +1,21 @@ + + + + + ./tests/Feature + + + + + + + + + + + + + ./src + + + diff --git a/src/ProcessDataSource.php b/src/ProcessDataSource.php index eb5165dc1..92e5cde89 100644 --- a/src/ProcessDataSource.php +++ b/src/ProcessDataSource.php @@ -7,7 +7,8 @@ use Illuminate\Database\Eloquent\{Builder as EloquentBuilder, Model}; use Illuminate\Database\Query\Builder as QueryBuilder; use Illuminate\Pagination\Paginator; -use Illuminate\Support\{Collection as BaseCollection, Facades\DB, Str}; +use Illuminate\Support\{Collection as BaseCollection, Facades\DB, Str, Stringable}; +use Laravel\Scout\Builder as ScoutBuilder; use PowerComponents\LivewirePowerGrid\Components\Actions\ActionsController; use PowerComponents\LivewirePowerGrid\Components\Rules\{RulesController}; use PowerComponents\LivewirePowerGrid\DataSource\{Builder, Collection}; @@ -18,8 +19,6 @@ class ProcessDataSource { use Concerns\SoftDeletes; - public bool $isCollection = false; - private array $queryLog = []; public function __construct( @@ -49,16 +48,46 @@ public function get(bool $isExport = false): Paginator|LengthAwarePaginator|\Ill return $this->processCollection($datasource, $isExport); } + if ($datasource instanceof ScoutBuilder) { + return $this->processScoutCollection($datasource); + } + $this->setCurrentTable($datasource); - /** @phpstan-ignore-next-line */ - return $this->processModel($datasource); + return $this->processModel($datasource); // @phpstan-ignore-line } - /** - * @return EloquentBuilder|BaseCollection|Collection|QueryBuilder|MorphToMany|null - */ - public function prepareDataSource(): EloquentBuilder|BaseCollection|Collection|QueryBuilder|MorphToMany|null + public function processScoutCollection(ScoutBuilder $datasource): Paginator|LengthAwarePaginator + { + $datasource->query = Str::of($datasource->query) + ->when($this->component->search != '', fn (Stringable $self) => $self + ->prepend($this->component->search . ',')) + ->toString(); + + collect($this->component->filters)->each(fn (array $filters) => collect($filters) + ->each(fn (string $value, string $field) => $datasource + ->where($field, $value))); + + if ($this->component->multiSort) { + foreach ($this->component->sortArray as $sortField => $direction) { + $datasource->orderBy($sortField, $direction); + } + } else { + $datasource->orderBy($this->component->sortField, $this->component->sortDirection); + } + + $results = self::applyPerPage($datasource); + + if (method_exists($results, 'total')) { + $this->component->total = $results->total(); + } + + return $results->setCollection( // @phpstan-ignore-line + $this->transform($results->getCollection(), $this->component) // @phpstan-ignore-line + ); + } + + public function prepareDataSource(): EloquentBuilder|BaseCollection|Collection|QueryBuilder|MorphToMany|ScoutBuilder|null { $datasource = $this->component->datasource ?? null; @@ -70,8 +99,6 @@ public function prepareDataSource(): EloquentBuilder|BaseCollection|Collection|Q $datasource = collect($datasource); } - $this->isCollection = $datasource instanceof BaseCollection; - return $datasource; } @@ -226,7 +253,7 @@ private function applyWithSortStringNumber( return $results; } - private function applyPerPage(EloquentBuilder|QueryBuilder|MorphToMany $results): LengthAwarePaginator|Paginator + private function applyPerPage(EloquentBuilder|QueryBuilder|MorphToMany|ScoutBuilder $results): LengthAwarePaginator|Paginator { $pageName = strval(data_get($this->component->setUp, 'footer.pageName', 'page')); $perPage = intval(data_get($this->component->setUp, 'footer.perPage')); @@ -237,6 +264,10 @@ private function applyPerPage(EloquentBuilder|QueryBuilder|MorphToMany $results) default => 'paginate', }; + if ($results instanceof ScoutBuilder) { + return $results->paginateSafe($perPage, pageName: $pageName); // @phpstan-ignore-line + } + if ($perPage > 0) { return $results->$paginate($perPage, pageName: $pageName); } diff --git a/src/Providers/PowerGridServiceProvider.php b/src/Providers/PowerGridServiceProvider.php index eb064074c..8ce0a910b 100644 --- a/src/Providers/PowerGridServiceProvider.php +++ b/src/Providers/PowerGridServiceProvider.php @@ -2,9 +2,13 @@ namespace PowerComponents\LivewirePowerGrid\Providers; +use Illuminate\Container\Container; use Illuminate\Database\Events\MigrationsEnded; +use Illuminate\Pagination\{LengthAwarePaginator, Paginator}; use Illuminate\Support\Facades\{Blade, Event}; use Illuminate\Support\ServiceProvider; +use Laravel\Scout\Builder; +use Laravel\Scout\Contracts\PaginatesEloquentModels; use Livewire\Features\SupportLegacyModels\{EloquentCollectionSynth, EloquentModelSynth}; use Livewire\Livewire; use PowerComponents\LivewirePowerGrid\Commands\CheckDependenciesCommand; @@ -64,7 +68,7 @@ public function register(): void Livewire::component('powergrid-performance-card', PerformanceCard::class); } - Macros::boot(); + $this->macros(); } private function publishViews(): void @@ -89,4 +93,42 @@ private function publishConfigs(): void $this->publishes([__DIR__ . '/../../resources/lang' => lang_path('vendor/' . $this->packageName)], $this->packageName . '-lang'); } + + private function macros(): void + { + Macros::boot(); + + if (class_exists(\Laravel\Scout\Builder::class)) { + Builder::macro('paginateSafe', function ($perPage = null, $pageName = 'page', $page = null) { + $engine = $this->engine(); // @phpstan-ignore-line + + if ($engine instanceof PaginatesEloquentModels) { + return $engine->paginate($this, $perPage, $page)->appends('query', $this->query); + } + + $page = $page ?: Paginator::resolveCurrentPage($pageName); + + $perPage = $perPage ?: $this->model->getPerPage(); + + $results = $this->model->newCollection( + $engine->map( + $this, + $rawResults = $engine->paginate($this, $perPage, $page), + $this->model + )->all() + ); + + return Container::getInstance()->makeWith(LengthAwarePaginator::class, [ + 'items' => $results, + 'total' => $engine->getTotalCount($rawResults), + 'perPage' => $perPage, + 'currentPage' => $page, + 'options' => [ + 'path' => Paginator::resolveCurrentPath(), + 'pageName' => $pageName, + ], + ])->appends('query', $this->query); + }); + } + } } diff --git a/src/Traits/WithExport.php b/src/Traits/WithExport.php index 83a033f98..94f6f0071 100644 --- a/src/Traits/WithExport.php +++ b/src/Traits/WithExport.php @@ -168,7 +168,7 @@ public function prepareToExport(bool $selected = false): Eloquent\Collection|Sup $inClause = $processDataSource->component->checkboxValues; } - if ($processDataSource->isCollection) { + if ($processDataSource->component->datasource() instanceof Collection) { if ($inClause) { $results = $processDataSource->get(isExport: true)->whereIn($this->primaryKey, $inClause);