Skip to content

Commit 7fd6c23

Browse files
Support mapping of entry data to database columns (#273)
* Add upgrade note to readme * Missing update script * Correct file path to migration * Lets not exclude parent * Split forms and form submissions, provide Eloquent Submission Repository and Query Builder (#177) * Add upgrade note to readme * Begin split configs * Use form handle to relate, rather then a database id * Defer to core methods for retrieving submissions * Update fresh migrations and add migration for existing installs * Eloquent FormRepository * Fix migration * Dont force a model type or it can't be overwritten * Missing update script * Correct file path to migration * Lets not exclude parent * StyleCI * Actually run update script * Prep for Statamic 5 * Update dev dependencies * Try again * Meh * Remove deprecated test functions * static * Migrate PHP unit config * Fix migration * Fix tests to use handle instead of id * Bug fixes * Fix form listing bugs * Split migration publish tags (#266) * Split migration tags * fix everything * Merge branch '5.x-support' into split-migration-tags * Ignore `.phpunit.cache` --------- Co-authored-by: Ryan Mitchell <[email protected]> * Statamic 5: Simplify `TestCase` (#267) * Simplify `TestCase` * Remove `partialMock` method from `TestCase` I'm not sure which "earlier versions of Laravel" this is referring to but the test suite seems to pass without it so I presume it's no longer needed. * doesn't look like we're using this method either * The `ConsoleKernel` isn't needed * Make drivers opt-in, rather than opt-out (#268) * Eloquent driver should be opt-in, not opt-out * Swap all drivers to `eloquent` in our `TestCase` * Drop status on entries (#228) * Fix asset import bug * Drop status on entries * Revert "Fix asset import bug" This reverts commit 35ebe65. * Fix test * StyleCI * Revert * Proper update script * Make status nullable in down migration * Statamic 5: Docs Refresh (#254) * Initial docs refresh * Fix tpyo * Missing space --------- Co-authored-by: Ryan Mitchell <[email protected]> * Support mapped data columns * Add tests and fix test errors by freezing time * Update `AddonTestCase` import * Fix failing tests in `EntryQueryBuilderTest` (#275) * Fix failing tests in `EntryQueryBuilderTest` * dont need to freeze time for this test * 🍺 * 🍺 * Remove accidental testcase addition * Avoid make()-ing * Refactor out hooks * Add test coverage after bringin data into COLUMNS * Not array_keys * Add assertion that it doesnt save the field to the data column * 🍺 * Remove `@test` in favour of #[Test] * Remove ::hook calls * 🍺 --------- Co-authored-by: Duncan McClean <[email protected]>
1 parent 0b15b88 commit 7fd6c23

File tree

7 files changed

+150
-4
lines changed

7 files changed

+150
-4
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ The configuration file, found in `config/statamic/eloquent-driver.php` is automa
5252

5353
For each of the repositories, it allows you to determine if they should be driven by flat-files (`file`) or Eloquent (`eloquent`). Some repositories also have additional options, like the ability to override the model used.
5454

55+
### Mapping Entry data
56+
57+
If you want to map fields from your blueprints to columns with the same handle in your blueprint, set `entries.map_data_to_columns` to true. When adding new columns in a migration we recommend resaving all Entries so that column data is filled: `Entry::all()->each->save()`.
58+
5559
## Upgrading
5660

5761
After updating to a new version of the Eloquent Driver, please ensure you run `php artisan migrate` to update your database to the latest schema.

config/eloquent-driver.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
'driver' => 'file',
4040
'model' => \Statamic\Eloquent\Entries\EntryModel::class,
4141
'entry' => \Statamic\Eloquent\Entries\Entry::class,
42+
'map_data_to_columns' => false,
4243
],
4344

4445
'fieldsets' => [

src/Entries/Entry.php

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Illuminate\Support\Carbon;
77
use Statamic\Contracts\Entries\Entry as EntryContract;
88
use Statamic\Entries\Entry as FileEntry;
9+
use Statamic\Facades\Blink;
910
use Statamic\Facades\Entry as EntryFacade;
1011

1112
class Entry extends FileEntry
@@ -16,6 +17,10 @@ public static function fromModel(Model $model)
1617
{
1718
$data = isset($model->data['__localized_fields']) ? collect($model->data)->only($model->data['__localized_fields']) : $model->data;
1819

20+
foreach ((new self)->getDataColumnMappings($model) as $key) {
21+
$data[$key] = $model->$key;
22+
}
23+
1924
$entry = (new static())
2025
->origin($model->origin_id)
2126
->locale($model->site)
@@ -93,6 +98,8 @@ public static function makeModelFromContract(EntryContract $source)
9398
$data->put('parent', (string) $parent->id);
9499
}
95100

101+
$dataMappings = (new self)->getDataColumnMappings(new $class);
102+
96103
$attributes = [
97104
'origin_id' => $origin?->id(),
98105
'site' => $source->locale(),
@@ -101,12 +108,16 @@ public static function makeModelFromContract(EntryContract $source)
101108
'date' => $date,
102109
'collection' => $source->collectionHandle(),
103110
'blueprint' => $source->blueprint ?? $source->blueprint()->handle(),
104-
'data' => $data->except(EntryQueryBuilder::COLUMNS),
111+
'data' => $data->except(array_merge(EntryQueryBuilder::COLUMNS, $dataMappings)),
105112
'published' => $source->published(),
106113
'updated_at' => $source->lastModified(),
107114
'order' => $source->order(),
108115
];
109116

117+
foreach ($dataMappings as $key) {
118+
$attributes[$key] = $data->get($key);
119+
}
120+
110121
if ($id = $source->id()) {
111122
$attributes['id'] = $id;
112123
}
@@ -173,4 +184,17 @@ public function makeLocalization($site)
173184
return parent::makeLocalization($site)
174185
->data($this->data());
175186
}
187+
188+
public function getDataColumnMappings(Model $model)
189+
{
190+
if (! config('statamic.eloquent-driver.entries.map_data_to_columns', false)) {
191+
return [];
192+
}
193+
194+
return Blink::once("eloquent-schema-{$model->getTable()}", function () use ($model) {
195+
$schema = $model->getConnection()->getSchemaBuilder()->getColumnListing($model->getTable());
196+
197+
return collect($schema)->reject(fn ($value) => in_array($value, EntryQueryBuilder::COLUMNS))->all();
198+
});
199+
}
176200
}

src/Entries/EntryQueryBuilder.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44

55
use Illuminate\Support\Str;
66
use Statamic\Contracts\Entries\QueryBuilder;
7+
use Statamic\Eloquent\Entries\Entry as EloquentEntry;
78
use Statamic\Entries\EntryCollection;
9+
use Statamic\Facades\Blink;
810
use Statamic\Facades\Collection;
911
use Statamic\Facades\Entry;
1012
use Statamic\Query\EloquentQueryBuilder;
@@ -21,7 +23,7 @@ class EntryQueryBuilder extends EloquentQueryBuilder implements QueryBuilder
2123
private const STATUSES = ['published', 'draft', 'scheduled', 'expired'];
2224

2325
const COLUMNS = [
24-
'id', 'site', 'origin_id', 'published', 'slug', 'uri',
26+
'id', 'site', 'origin_id', 'published', 'slug', 'uri', 'data',
2527
'date', 'collection', 'created_at', 'updated_at', 'order', 'blueprint',
2628
];
2729

@@ -117,7 +119,9 @@ protected function column($column)
117119
$column = 'origin_id';
118120
}
119121

120-
if (! in_array($column, self::COLUMNS)) {
122+
$columns = Blink::once('eloquent-entry-data-column-mappings', fn () => array_merge(self::COLUMNS, (new EloquentEntry)->getDataColumnMappings($this->builder->getModel())));
123+
124+
if (! in_array($column, $columns)) {
121125
if (! Str::startsWith($column, 'data->')) {
122126
$column = 'data->'.$column;
123127
}

tests/Assets/AssetTest.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Illuminate\Foundation\Testing\RefreshDatabase;
66
use Illuminate\Support\Facades\Storage;
7+
use PHPUnit\Framework\Attributes\Test;
78
use Statamic\Facades;
89
use Tests\TestCase;
910

@@ -40,7 +41,7 @@ public function setUp(): void
4041
Facades\Asset::make()->container('test')->path('f.jpg')->save();
4142
}
4243

43-
/** @test */
44+
#[Test]
4445
public function saving_an_asset_clears_the_eloquent_blink_cache()
4546
{
4647
$asset = Facades\Asset::find('test::f.jpg');

tests/Data/Entries/EntryQueryBuilderTest.php

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ private function createWhereDateTestEntries()
190190
EntryFactory::id('5')->slug('post-5')->collection('posts')->data(['title' => 'Post 5', 'test_date' => null])->create();
191191
}
192192

193+
#[Test]
193194
public function entries_are_found_using_where_null()
194195
{
195196
EntryFactory::id('1')->slug('post-1')->collection('posts')->data(['title' => 'Post 1', 'text' => 'Text 1'])->create();
@@ -796,6 +797,46 @@ public function entries_can_be_ordered_by_an_date_json_field()
796797
$this->assertEquals(['Post 2', 'Post 1', 'Post 3'], $entries->map->title->all());
797798
}
798799

800+
#[Test]
801+
public function entries_can_be_ordered_by_a_mapped_data_column()
802+
{
803+
config()->set('statamic.eloquent-driver.entries.map_data_to_columns', true);
804+
805+
\Illuminate\Support\Facades\Schema::table('entries', function ($table) {
806+
$table->string('foo', 30);
807+
});
808+
809+
Collection::make('posts')->save();
810+
EntryFactory::id('1')->slug('post-1')->collection('posts')->data(['title' => 'Post 1', 'foo' => 2])->create();
811+
EntryFactory::id('2')->slug('post-2')->collection('posts')->data(['title' => 'Post 2', 'foo' => 3])->create();
812+
EntryFactory::id('3')->slug('post-3')->collection('posts')->data(['title' => 'Post 3', 'foo' => 1])->create();
813+
814+
$entries = Entry::query()->where('collection', 'posts')->orderBy('foo', 'desc')->get();
815+
816+
$this->assertCount(3, $entries);
817+
$this->assertEquals(['Post 2', 'Post 1', 'Post 3'], $entries->map->title->all());
818+
}
819+
820+
#[Test]
821+
public function entries_can_be_queried_by_a_mapped_data_column()
822+
{
823+
config()->set('statamic.eloquent-driver.entries.map_data_to_columns', true);
824+
825+
\Illuminate\Support\Facades\Schema::table('entries', function ($table) {
826+
$table->string('foo', 30);
827+
});
828+
829+
Collection::make('posts')->save();
830+
EntryFactory::id('1')->slug('post-1')->collection('posts')->data(['title' => 'Post 1', 'foo' => 2])->create();
831+
EntryFactory::id('2')->slug('post-2')->collection('posts')->data(['title' => 'Post 2', 'foo' => 3])->create();
832+
EntryFactory::id('3')->slug('post-3')->collection('posts')->data(['title' => 'Post 3', 'foo' => 1])->create();
833+
834+
$entries = Entry::query()->where('collection', 'posts')->where('foo', 3)->get();
835+
836+
$this->assertCount(1, $entries);
837+
$this->assertEquals(['Post 2'], $entries->map->title->all());
838+
}
839+
799840
#[Test]
800841
public function filtering_using_where_status_column_writes_deprecation_log()
801842
{
@@ -884,4 +925,15 @@ public static function filterByStatusProvider()
884925
]],
885926
];
886927
}
928+
929+
#[Test]
930+
public function entries_are_found_using_where_data()
931+
{
932+
$this->createDummyCollectionAndEntries();
933+
934+
$entries = Entry::query()->where('data->title', 'Post 1')->orWhere('data->title', 'Post 3')->get();
935+
936+
$this->assertCount(2, $entries);
937+
$this->assertEquals(['Post 1', 'Post 3'], $entries->map->title->all());
938+
}
887939
}

tests/Entries/EntryTest.php

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,4 +241,64 @@ public function it_propagates_origin_date_to_descendent_models()
241241

242242
$this->assertEquals($entry->descendants()->get('fr')->model()->date, '2024-01-01 00:00:00');
243243
}
244+
245+
#[Test]
246+
public function it_stores_and_retrieves_mapped_data_values()
247+
{
248+
config()->set('statamic.eloquent-driver.entries.map_data_to_columns', true);
249+
250+
$collection = Collection::make('blog')->title('blog')->routes([
251+
'en' => '/blog/{slug}',
252+
])->save();
253+
254+
\Illuminate\Support\Facades\Schema::table('entries', function ($table) {
255+
$table->string('foo', 30);
256+
});
257+
258+
$entry = (new Entry())
259+
->collection('blog')
260+
->slug('the-slug')
261+
->data([
262+
'foo' => 'bar',
263+
]);
264+
265+
$entry->save();
266+
267+
$this->assertEquals('bar', $entry->model()->toArray()['foo']);
268+
$this->assertArrayNotHasKey('foo', $entry->model()->data);
269+
270+
$fresh = Entry::fromModel($entry->model()->fresh());
271+
272+
$this->assertSame($entry->foo, $fresh->foo);
273+
}
274+
275+
#[Test]
276+
public function it_doesnt_store_mapped_data_when_config_is_disabled()
277+
{
278+
config()->set('statamic.eloquent-driver.entries.map_data_to_columns', false);
279+
280+
$collection = Collection::make('blog')->title('blog')->routes([
281+
'en' => '/blog/{slug}',
282+
])->save();
283+
284+
\Illuminate\Support\Facades\Schema::table('entries', function ($table) {
285+
$table->string('foo', 30)->nullable();
286+
});
287+
288+
$entry = (new Entry())
289+
->collection('blog')
290+
->slug('the-slug')
291+
->data([
292+
'foo' => 'bar',
293+
]);
294+
295+
$entry->save();
296+
297+
$this->assertNull($entry->model()->toArray()['foo']);
298+
$this->assertArrayHasKey('foo', $entry->model()->data);
299+
300+
$fresh = Entry::fromModel($entry->model()->fresh());
301+
302+
$this->assertSame($entry->foo, $fresh->foo);
303+
}
244304
}

0 commit comments

Comments
 (0)