Skip to content

Commit

Permalink
Added support to eager load repeater relations
Browse files Browse the repository at this point in the history
  • Loading branch information
Thomas Bruckmaier committed Aug 22, 2024
1 parent 7166850 commit 8421555
Show file tree
Hide file tree
Showing 4 changed files with 195 additions and 3 deletions.
23 changes: 23 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,29 @@ $fcLayouts->get(1)->accordion_items->first()->title; // "First accordion element
$fcLayouts->get(1)->accordion_items->first()->content; // "First accordion content..."
```

#### Eager loading relationships

If you have a repeater which includes an image field, it may be wise to preload the image's `Attachment` relation (otherwise an own query is fired for each attachment):

```php
use Tbruckmaier\Corcelacf\Models\Repeater;

$post = Post::find(1);

// fires 2 queries per iteration
foreach ($post->acf->main_repeater as $layout) {
$layout->foo_image->attachment;
$layout->bar_image->attachment;
}

// preloads all attachment relations, fires 2 queries in total
foreach ($post->acf->main_repeater()->load('foo_image.attachment', 'bar_image.attachment') as $layout) {
$layout->foo_image->attachment;
$layout->bar_image->attachment;
}

```

### Group field

A group field returns a `GroupLayout`, which contains all grouped fields. `GroupLayout` acts like a `FlexibleContentLayout` or a `RepeaterLayout`: by accessing its fields as attributes, the parsed value is returned. When accessing them as methods, the class itself is returned.
Expand Down
53 changes: 50 additions & 3 deletions src/Models/Repeater.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,22 @@

namespace Tbruckmaier\Corcelacf\Models;

use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\RelationNotFoundException;
use InvalidArgumentException;
use Tbruckmaier\Corcelacf\Support\RepeaterLayout;

class Repeater extends BaseField
{
protected $with = ['children'];

protected $_with = [];

public function getValueAttribute()
{
$count = $this->internal_value;

$ret = collect();
$layouts = collect();

for ($i = 0; $i < $count; $i++) {
$row = collect();
Expand All @@ -26,9 +31,51 @@ public function getValueAttribute()
$row->put($layout->post_excerpt, $field);
}

$ret->push(new RepeaterLayout($row));
$layouts->push(new RepeaterLayout($row));
}

return $ret;
// eager load all relations. Makes sense for fields like "Image" which
// has a hasOne relation to "Attachment". If "foo_image.attachment" is
// requested, load the relation "attachment" on the foo_image field of
// each layout
foreach ($this->_with as $fullRelationName) {

if (!str_contains($fullRelationName, '.')) {
throw new InvalidArgumentException(sprintf(
"Eager-loaded ACF repeater relations must contain '.'. Available parents:\n%s",
$layouts->first()->getData()->map(fn ($layout, $key) => sprintf('- %s: %s', $key, get_class($layout)))->join("\n"),
));
}

list ($layoutKey, $relationName) = explode('.', $fullRelationName, 2);

// get a plain collection of the relationship's parent field (e.g. a
// list of "Image::class")
$fields = $layouts->map(function (RepeaterLayout $layout) use ($layoutKey) : BaseField {
$field = $layout->$layoutKey();

// "foo_image" does not exist in thsi repeater
if (!$field) {
throw RelationNotFoundException::make($this->getModel(), $layoutKey);
}

return $field;
});

// turn it into a Eloquent Collection and use its load() function
Collection::make($fields)->load($relationName);
}

return $layouts;
}

/**
* Eager load relations. Makes sense for repeaters with images, to preload
* their attachment relationship
*/
public function load($relations)
{
$this->_with = array_merge($this->_with, is_string($relations) ? func_get_args() : $relations);
return $this;
}
}
120 changes: 120 additions & 0 deletions tests/FieldRepeaterPreload.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<?php

use Tbruckmaier\Corcelacf\Models\Repeater;
use Tbruckmaier\Corcelacf\Models\Text;
use Tbruckmaier\Corcelacf\Models\Boolean;
use Tbruckmaier\Corcelacf\Tests\TestCase;
use Illuminate\Support\Collection;
use Tbruckmaier\Corcelacf\Support\RepeaterLayout;
use Corcel\Model\Attachment;
use Illuminate\Database\Eloquent\RelationNotFoundException;
use Illuminate\Support\Arr;
use Tbruckmaier\Corcelacf\Models\Image;

class FieldRepeaterPreload extends TestCase
{
protected $acfField;
protected array $imgs;

protected function setUp() : void
{
parent::setUp();
$this->acfField = factory(Repeater::class)->create();

$this->imgs = array_map(fn () => factory(Attachment::class)->create(), range(0, 3));

$data = [
'fake_repeater' => '2',
'fake_repeater_0_text' => 'First text',
'fake_repeater_0_image' => $this->imgs[0]->getKey(),
'fake_repeater_0_another_image' => $this->imgs[1]->getKey(),
'fake_repeater_1_text' => 'Second text',
'fake_repeater_1_image' => $this->imgs[2]->getKey(),
'fake_repeater_1_another_image' => $this->imgs[3]->getKey(),
];

$this->setAcfData($this->acfField, $data)->setLocalKey('fake_repeater');

factory(Text::class)->create(['post_parent' => $this->acfField->getKey(), 'post_excerpt' => 'text']);
factory(Image::class)->create(['post_parent' => $this->acfField->getKey(), 'post_excerpt' => 'image']);
factory(Image::class)->create(['post_parent' => $this->acfField->getKey(), 'post_excerpt' => 'another_image']);
}

public function testLoad()
{
$this->acfField->load('image.attachment');

$array = $this->acfField->value->toArray();

$this->assertNotNull(Arr::get($array, '0.image.attachment'));
$this->assertEquals($this->imgs[0]->getKey(), Arr::get($array, '0.image.attachment.ID'));
$this->assertNull(Arr::get($array, '0.another_image.attachment'));

$this->assertNotNull(Arr::get($array, '1.image.attachment'));
$this->assertEquals($this->imgs[2]->getKey(), Arr::get($array, '1.image.attachment.ID'));
$this->assertNull(Arr::get($array, '1.another_image.attachment'));
}

public function testLoadMultiple()
{
$this->acfField->load('image.attachment', 'another_image.attachment');

$array = $this->acfField->value->toArray();

$this->assertNotNull(Arr::get($array, '0.image.attachment'));
$this->assertEquals($this->imgs[0]->getKey(), Arr::get($array, '0.image.attachment.ID'));
$this->assertNotNull(Arr::get($array, '0.another_image.attachment'));
$this->assertEquals($this->imgs[1]->getKey(), Arr::get($array, '0.another_image.attachment.ID'));

$this->assertNotNull(Arr::get($array, '1.image.attachment'));
$this->assertEquals($this->imgs[2]->getKey(), Arr::get($array, '1.image.attachment.ID'));
$this->assertNotNull(Arr::get($array, '1.another_image.attachment'));
$this->assertEquals($this->imgs[3]->getKey(), Arr::get($array, '1.another_image.attachment.ID'));
}

public function testLoadMultipleArray()
{
$this->acfField->load(['image.attachment', 'another_image.attachment']);

$array = $this->acfField->value->toArray();

$this->assertNotNull(Arr::get($array, '0.image.attachment'));
$this->assertEquals($this->imgs[0]->getKey(), Arr::get($array, '0.image.attachment.ID'));
$this->assertNotNull(Arr::get($array, '0.another_image.attachment'));
$this->assertEquals($this->imgs[1]->getKey(), Arr::get($array, '0.another_image.attachment.ID'));

$this->assertNotNull(Arr::get($array, '1.image.attachment'));
$this->assertEquals($this->imgs[2]->getKey(), Arr::get($array, '1.image.attachment.ID'));
$this->assertNotNull(Arr::get($array, '1.another_image.attachment'));
$this->assertEquals($this->imgs[3]->getKey(), Arr::get($array, '1.another_image.attachment.ID'));
}

public function testNoAutoload()
{
// per default, attachments should not be loaded
$array = $this->acfField->value->toArray();

$this->assertNull(Arr::get($array, '0.image.attachment'));
$this->assertNull(Arr::get($array, '0.another_image.attachment'));
$this->assertNull(Arr::get($array, '1.image.attachment'));
$this->assertNull(Arr::get($array, '1.another_image.attachment'));
}

public function testLoadInvalid1()
{
$this->expectException(InvalidArgumentException::class);
$this->acfField->load('invalid')->value;
}

public function testLoadInvalid2()
{
$this->expectException(RelationNotFoundException::class);
$this->acfField->load('imag2e.attachment')->value;
}

public function testLoadInvalid3()
{
$this->expectException(RelationNotFoundException::class);
$this->acfField->load('image.atta2chment')->value;
}
}
2 changes: 2 additions & 0 deletions tests/FieldRepeaterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
use Tbruckmaier\Corcelacf\Tests\TestCase;
use Illuminate\Support\Collection;
use Tbruckmaier\Corcelacf\Support\RepeaterLayout;
use Corcel\Model\Attachment;
use Tbruckmaier\Corcelacf\Models\Image;

class FieldRepeaterTest extends TestCase
{
Expand Down

0 comments on commit 8421555

Please sign in to comment.