Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Laravel Scout support #4032

Merged
merged 12 commits into from
Dec 8, 2023
Merged
8 changes: 7 additions & 1 deletion .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,22 @@ jobs:
os: [ubuntu-latest]
include:
- laravel: 10
scout: 10.*
testbench: 8.*
- laravel: 9
scout: 9.*
testbench: 7.*
- laravel: 8
scout: 8.*
testbench: 6.*
- laravel: 7
scout: 7.2.*
testbench: 5.*
- laravel: 6
scout: 7.1.*
testbench: 4.*
- laravel: 5.8
scout: 7.1.*
testbench: 3.8.*
exclude:
- laravel: 10
Expand Down Expand Up @@ -93,7 +99,7 @@ jobs:

- name: Install dependencies
run: |
composer require "laravel/framework:${{ matrix.laravel }}.*" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update
composer require "laravel/framework:${{ matrix.laravel }}.*" "orchestra/testbench:${{ matrix.testbench }}" "laravel/scout:${{ matrix.scout }}" --no-interaction --no-update
composer update --${{ matrix.dependency-version }} --no-interaction

- name: Install legacy factories
Expand Down
3 changes: 2 additions & 1 deletion src/Concerns/FromQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Query\Builder;
use Laravel\Scout\Builder as ScoutBuilder;

interface FromQuery
{
/**
* @return Builder|EloquentBuilder|Relation
* @return Builder|EloquentBuilder|Relation|ScoutBuilder
*/
public function query();
}
117 changes: 117 additions & 0 deletions src/Jobs/AppendPaginatedToSheet.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<?php

namespace Maatwebsite\Excel\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Query\Builder;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Laravel\Scout\Builder as ScoutBuilder;
use Maatwebsite\Excel\Concerns\FromQuery;
use Maatwebsite\Excel\Files\TemporaryFile;
use Maatwebsite\Excel\Jobs\Middleware\LocalizeJob;
use Maatwebsite\Excel\Writer;

class AppendPaginatedToSheet implements ShouldQueue
{
use Queueable, Dispatchable, ProxyFailures, InteractsWithQueue;

/**
* @var TemporaryFile
*/
public $temporaryFile;

/**
* @var string
*/
public $writerType;

/**
* @var int
*/
public $sheetIndex;

/**
* @var FromQuery
*/
public $sheetExport;

/**
* @var int
*/
public $page;

/**
* @var int
*/
public $perPage;

/**
* @param FromQuery $sheetExport
* @param TemporaryFile $temporaryFile
* @param string $writerType
* @param int $sheetIndex
* @param int $page
* @param int $perPage
*/
public function __construct(
FromQuery $sheetExport,
TemporaryFile $temporaryFile,
string $writerType,
int $sheetIndex,
int $page,
int $perPage
) {
$this->sheetExport = $sheetExport;
$this->temporaryFile = $temporaryFile;
$this->writerType = $writerType;
$this->sheetIndex = $sheetIndex;
$this->page = $page;
$this->perPage = $perPage;
}

/**
* Get the middleware the job should be dispatched through.
*
* @return array
*/
public function middleware()
{
return (method_exists($this->sheetExport, 'middleware')) ? $this->sheetExport->middleware() : [];
}

/**
* @param Writer $writer
*
* @throws \PhpOffice\PhpSpreadsheet\Exception
* @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
*/
public function handle(Writer $writer)
{
(new LocalizeJob($this->sheetExport))->handle($this, function () use ($writer) {
$writer = $writer->reopen($this->temporaryFile, $this->writerType);

$sheet = $writer->getSheetByIndex($this->sheetIndex);

$sheet->appendRows($this->chunk($this->sheetExport->query()), $this->sheetExport);

$writer->write($this->sheetExport, $this->temporaryFile, $this->writerType);
});
}

/**
* @param Builder|Relation|EloquentBuilder|ScoutBuilder $query
*/
protected function chunk($query)
{
if ($query instanceof \Laravel\Scout\Builder) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Cellard this breaks for everyone not using Laravel scout but do use laravel Excel :(

Copy link
Member

@patrickbrouwers patrickbrouwers Jan 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't break anything for me. I'm not using Laravel Scout, everything still works like expected. instanceof checks can run on non-existing classes.

return $query->paginate($this->perPage, 'page', $this->page)->items();
}

// Fallback
return $query->forPage($this->page, $this->perPage)->get();
}
}
45 changes: 45 additions & 0 deletions src/QueuedWriter.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use Maatwebsite\Excel\Files\TemporaryFile;
use Maatwebsite\Excel\Files\TemporaryFileFactory;
use Maatwebsite\Excel\Jobs\AppendDataToSheet;
use Maatwebsite\Excel\Jobs\AppendPaginatedToSheet;
use Maatwebsite\Excel\Jobs\AppendQueryToSheet;
use Maatwebsite\Excel\Jobs\AppendViewToSheet;
use Maatwebsite\Excel\Jobs\CloseSheet;
Expand Down Expand Up @@ -150,6 +151,10 @@ private function exportQuery(
): Collection {
$query = $export->query();

if ($query instanceof \Laravel\Scout\Builder) {
return $this->exportScout($export, $temporaryFile, $writerType, $sheetIndex);
}

$count = $export instanceof WithCustomQuerySize ? $export->querySize() : $query->count();
$spins = ceil($count / $this->getChunkSize($export));

Expand All @@ -169,6 +174,46 @@ private function exportQuery(
return $jobs;
}

/**
* @param FromQuery $export
* @param TemporaryFile $temporaryFile
* @param string $writerType
* @param int $sheetIndex
* @return Collection
*/
private function exportScout(
FromQuery $export,
TemporaryFile $temporaryFile,
string $writerType,
int $sheetIndex
): Collection {
$jobs = new Collection();

$chunk = $export->query()->paginate($this->getChunkSize($export));
// Append first page
$jobs->push(new AppendDataToSheet(
$export,
$temporaryFile,
$writerType,
$sheetIndex,
$chunk->items()
));

// Append rest of pages
for ($page = 2; $page <= $chunk->lastPage(); $page++) {
$jobs->push(new AppendPaginatedToSheet(
$export,
$temporaryFile,
$writerType,
$sheetIndex,
$page,
$this->getChunkSize($export)
));
}

return $jobs;
}

/**
* @param FromView $export
* @param TemporaryFile $temporaryFile
Expand Down
25 changes: 25 additions & 0 deletions src/Sheet.php
Original file line number Diff line number Diff line change
Expand Up @@ -464,11 +464,36 @@ public function fromView(FromView $sheetExport, $sheetIndex = null)
*/
public function fromQuery(FromQuery $sheetExport, Worksheet $worksheet)
{
if ($sheetExport->query() instanceof \Laravel\Scout\Builder) {
$this->fromScout($sheetExport, $worksheet);
Cellard marked this conversation as resolved.
Show resolved Hide resolved

return;
}

$sheetExport->query()->chunk($this->getChunkSize($sheetExport), function ($chunk) use ($sheetExport) {
$this->appendRows($chunk, $sheetExport);
});
}

/**
* @param FromQuery $sheetExport
* @param Worksheet $worksheet
*/
public function fromScout(FromQuery $sheetExport, Worksheet $worksheet)
{
$scout = $sheetExport->query();
$chunkSize = $this->getChunkSize($sheetExport);

$chunk = $scout->paginate($chunkSize);
// Append first page
$this->appendRows($chunk->items(), $sheetExport);

// Append rest of pages
for ($page = 2; $page <= $chunk->lastPage(); $page++) {
$this->appendRows($scout->paginate($chunkSize, 'page', $page)->items(), $sheetExport);
}
}

/**
* @param FromCollection $sheetExport
*/
Expand Down
27 changes: 27 additions & 0 deletions tests/Concerns/FromQueryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Maatwebsite\Excel\Tests\Data\Stubs\FromUsersQueryExport;
use Maatwebsite\Excel\Tests\Data\Stubs\FromUsersQueryExportWithEagerLoad;
use Maatwebsite\Excel\Tests\Data\Stubs\FromUsersQueryExportWithPrepareRows;
use Maatwebsite\Excel\Tests\Data\Stubs\FromUsersScoutExport;
use Maatwebsite\Excel\Tests\TestCase;

class FromQueryTest extends TestCase
Expand Down Expand Up @@ -248,6 +249,32 @@ public function can_export_from_query_with_prepare_rows()
$this->assertEquals($allUsers, $contents);
}

/**
* @test
*/
public function can_export_from_scout()
{
if (!class_exists('\Laravel\Scout\Engines\DatabaseEngine')) {
$this->markTestSkipped('Laravel Scout is too old');

return;
}

$export = new FromUsersScoutExport;

$response = $export->store('from-scout-store.xlsx');

$this->assertTrue($response);

$contents = $this->readAsArray(__DIR__ . '/../Data/Disks/Local/from-scout-store.xlsx', 'Xlsx');

$allUsers = $export->query()->get()->map(function (User $user) {
return array_values($user->toArray());
})->toArray();

$this->assertEquals($allUsers, $contents);
}

protected function format_nested_arrays_expected_data($groups)
{
$expected = [];
Expand Down
25 changes: 25 additions & 0 deletions tests/Data/Stubs/Database/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,17 @@

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Laravel\Scout\Engines\DatabaseEngine;
use Laravel\Scout\Engines\Engine;
use Laravel\Scout\Engines\NullEngine;
use Laravel\Scout\Searchable;
use Maatwebsite\Excel\Tests\Concerns\FromQueryTest;
use Maatwebsite\Excel\Tests\QueuedQueryExportTest;

class User extends Model
{
use Searchable;

/**
* @var array
*/
Expand All @@ -31,4 +39,21 @@ public function groups(): BelongsToMany
{
return $this->belongsToMany(Group::class);
}

/**
* Laravel Scout under <=8 provides only
* — NullEngine, that is searches nothing and not applicable for tests and
* — AlgoliaEngine, that is 3-d party dependent and not applicable for tests too.
*
* The only test-ready engine is DatabaseEngine that comes with Scout >8
*
* Then running tests we will examine engine and skip test until DatabaseEngine is provided.
*
* @see QueuedQueryExportTest::can_queue_scout_export()
* @see FromQueryTest::can_export_from_scout()
*/
public function searchableUsing(): Engine
{
return class_exists('\Laravel\Scout\Engines\DatabaseEngine') ? new DatabaseEngine() : new NullEngine();
Cellard marked this conversation as resolved.
Show resolved Hide resolved
}
}
33 changes: 33 additions & 0 deletions tests/Data/Stubs/FromUsersScoutExport.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace Maatwebsite\Excel\Tests\Data\Stubs;

use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Query\Builder;
use Laravel\Scout\Builder as ScoutBuilder;
use Maatwebsite\Excel\Concerns\Exportable;
use Maatwebsite\Excel\Concerns\FromQuery;
use Maatwebsite\Excel\Concerns\WithCustomChunkSize;
use Maatwebsite\Excel\Tests\Data\Stubs\Database\User;

class FromUsersScoutExport implements FromQuery, WithCustomChunkSize
{
use Exportable;

/**
* @return Builder|EloquentBuilder|Relation|ScoutBuilder
*/
public function query()
{
return new ScoutBuilder(new User, '');
}

/**
* @return int
*/
public function chunkSize(): int
{
return 10;
}
}
Loading