diff --git a/CHANGELOG.md b/CHANGELOG.md index c13e9af2bf2..15064080957 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ ### What's fixed - CP forms only submit visible fields, in order to fix sometimes/required_if/etc validation rules. [#5101](https://github.com/statamic/cms/issues/5101) by @jesseleite +### What's changed +- Entries fieldtypes augment to query builders instead of collections. [#5238](https://github.com/statamic/cms/issues/5238) by @jasonvarga ## 3.2.32 (2022-01-26) diff --git a/src/Fieldtypes/Entries.php b/src/Fieldtypes/Entries.php index eb9607bfb0f..a04c834eff1 100644 --- a/src/Fieldtypes/Entries.php +++ b/src/Fieldtypes/Entries.php @@ -214,23 +214,44 @@ protected function collect($value) return new \Statamic\Entries\EntryCollection($value); } - protected function augmentValue($value) + public function augment($values) { - if (! is_object($value)) { - $value = Entry::find($value); + $site = Site::current()->handle(); + if (($parent = $this->field()->parent()) && $parent instanceof Localization) { + $site = $parent->locale(); } - if ($value != null && $parent = $this->field()->parent()) { - $site = $parent instanceof Localization ? $parent->locale() : Site::current()->handle(); - $value = $value->in($site); - } + $ids = Entry::query() + ->whereIn('id', Arr::wrap($values)) + ->get() + ->map(function ($entry) use ($site) { + return optional($entry->in($site))->id(); + }) + ->filter() + ->all(); + + $query = Entry::query() + ->whereIn('id', $ids) + ->where('status', 'published'); - return ($value && $value->status() === 'published') ? $value : null; + return $this->config('max_items') === 1 ? $query->first() : $query; } - protected function shallowAugmentValue($value) + public function shallowAugment($values) { - return $value->toShallowAugmentedCollection(); + $items = $this->augment($values); + + if ($this->config('max_items') === 1) { + $items = collect([$items]); + } else { + $items = $items->get(); + } + + $items = $items->filter()->map(function ($item) { + return $item->toShallowAugmentedCollection(); + }); + + return $this->config('max_items') === 1 ? $items->first() : $items; } public function getSelectionFilters() diff --git a/src/GraphQL/ResolvesValues.php b/src/GraphQL/ResolvesValues.php index df4f3ad848c..592dc596072 100644 --- a/src/GraphQL/ResolvesValues.php +++ b/src/GraphQL/ResolvesValues.php @@ -2,6 +2,7 @@ namespace Statamic\GraphQL; +use Statamic\Contracts\Query\Builder; use Statamic\Fields\Value; trait ResolvesValues @@ -14,6 +15,10 @@ public function resolveGqlValue($field) $value = $value->value(); } + if ($value instanceof Builder) { + $value = $value->get(); + } + return $value; } diff --git a/src/View/Antlers/Parser.php b/src/View/Antlers/Parser.php index 7f18864e9b3..d14b6ad3418 100644 --- a/src/View/Antlers/Parser.php +++ b/src/View/Antlers/Parser.php @@ -569,8 +569,10 @@ public function parseCallbackTags($text, $data) // a callback. If it's a query builder instance, we want to use the Query tag's index // method to handle the logic. We'll pass the builder into the builder parameter. if (isset($data[$name])) { - if ($data[$name] instanceof Builder) { - $parameters['builder'] = $data[$name]; + $value = $data[$name]; + $value = $value instanceof Value ? $value->value() : $value; + if ($value instanceof Builder) { + $parameters['builder'] = $value; $name = 'query'; } } @@ -1235,6 +1237,10 @@ protected function getVariableExistenceAndValue($key, $context) $context = $context->value(); } + if ($context instanceof Builder) { + $context = $context->get(); + } + if ($context instanceof Augmentable) { $context = $context->toAugmentedArray(); } diff --git a/tests/Fieldtypes/EntriesTest.php b/tests/Fieldtypes/EntriesTest.php index bc3184e74fe..d8a93eb225b 100644 --- a/tests/Fieldtypes/EntriesTest.php +++ b/tests/Fieldtypes/EntriesTest.php @@ -6,8 +6,10 @@ use Illuminate\Support\Carbon; use Illuminate\Support\Collection; use Statamic\Contracts\Entries\Entry; +use Statamic\Contracts\Query\Builder; use Statamic\Data\AugmentedCollection; use Statamic\Facades; +use Statamic\Facades\Site; use Statamic\Fields\Field; use Statamic\Fieldtypes\Entries; use Tests\PreventSavingStacheItemsToDisk; @@ -23,23 +25,39 @@ public function setUp(): void Carbon::setTestNow(Carbon::parse('2021-01-02')); - $collection = tap(Facades\Collection::make('blog')->routes('blog/{slug}'))->dated(true)->pastDateBehavior('private')->futureDateBehavior('private')->save(); + Site::setConfig(['sites' => [ + 'en' => ['url' => 'http://localhost/', 'locale' => 'en'], + 'fr' => ['url' => 'http://localhost/fr/', 'locale' => 'fr'], + ]]); + + $collection = tap(Facades\Collection::make('blog')->routes('blog/{slug}'))->sites(['en', 'fr'])->dated(true)->pastDateBehavior('private')->futureDateBehavior('private')->save(); EntryFactory::id('123')->collection($collection)->slug('one')->data(['title' => 'One'])->date('2021-01-02')->create(); EntryFactory::id('456')->collection($collection)->slug('two')->data(['title' => 'Two'])->date('2021-01-02')->create(); + EntryFactory::id('789')->collection($collection)->slug('three')->data(['title' => 'Three'])->date('2021-01-02')->create(); + EntryFactory::id('910')->collection($collection)->slug('four')->data(['title' => 'Four'])->date('2021-01-02')->create(); EntryFactory::id('draft')->collection($collection)->slug('draft')->data(['title' => 'Draft'])->published(false)->create(); EntryFactory::id('scheduled')->collection($collection)->slug('scheduled')->data(['title' => 'Scheduled'])->date('2021-01-03')->create(); EntryFactory::id('expired')->collection($collection)->slug('expired')->data(['title' => 'Expired'])->date('2021-01-01')->create(); } /** @test */ - public function it_augments_to_a_collection_of_entries() + public function it_augments_to_a_query_builder() { $augmented = $this->fieldtype()->augment(['123', 'invalid', 456, 'draft', 'scheduled', 'expired']); - $this->assertInstanceOf(Collection::class, $augmented); - $this->assertEveryItemIsInstanceOf(Entry::class, $augmented); - $this->assertEquals(['one', 'two'], $augmented->map->slug()->all()); + $this->assertInstanceOf(Builder::class, $augmented); + $this->assertEveryItemIsInstanceOf(Entry::class, $augmented->get()); + $this->assertEquals(['one', 'two'], $augmented->get()->map->slug()->all()); + } + + /** @test */ + public function it_augments_to_a_query_builder_when_theres_no_value() + { + $augmented = $this->fieldtype()->augment(null); + + $this->assertInstanceOf(Builder::class, $augmented); + $this->assertCount(0, $augmented->get()); } /** @test */ @@ -52,9 +70,88 @@ public function it_augments_to_a_single_asset_when_max_items_is_one() } /** @test */ - public function it_shallow_augments_to_a_collection_of_enties() + public function it_localizes_the_augmented_items_to_the_parent_entrys_locale() { - $augmented = $this->fieldtype()->shallowAugment(['123', '456']); + $parent = EntryFactory::id('parent')->collection('blog')->slug('theparent')->locale('fr')->create(); + + EntryFactory::id('123-fr')->origin('123')->locale('fr')->collection('blog')->slug('one-fr')->data(['title' => 'Le One'])->date('2021-01-02')->create(); + EntryFactory::id('789-fr')->origin('789')->locale('fr')->collection('blog')->slug('three-fr')->data(['title' => 'Le Three'])->date('2021-01-02')->published(false)->create(); + EntryFactory::id('910-fr')->origin('910')->locale('fr')->collection('blog')->slug('four-fr')->data(['title' => 'Le Four'])->date('2021-01-02')->create(); + + $augmented = $this->fieldtype([], $parent)->augment(['123', 'invalid', 456, 789, 910, 'draft', 'scheduled', 'expired']); + + $this->assertInstanceOf(Builder::class, $augmented); + $this->assertEveryItemIsInstanceOf(Entry::class, $augmented->get()); + $this->assertEquals(['one-fr', 'four-fr'], $augmented->get()->map->slug()->all()); // 456 isn't localized, and 789-fr is a draft. + } + + /** @test */ + public function it_localizes_the_augmented_item_to_the_parent_entrys_locale_when_max_items_is_one() + { + $parent = EntryFactory::id('parent')->collection('blog')->slug('theparent')->locale('fr')->create(); + + EntryFactory::id('123-fr')->origin('123')->locale('fr')->collection('blog')->slug('one-fr')->data(['title' => 'Le One'])->date('2021-01-02')->create(); + EntryFactory::id('789-fr')->origin('789')->locale('fr')->collection('blog')->slug('three-fr')->data(['title' => 'Le Three'])->date('2021-01-02')->published(false)->create(); + + $fieldtype = $this->fieldtype(['max_items' => 1], $parent); + + $augmented = $fieldtype->augment(['123']); + $this->assertInstanceOf(Entry::class, $augmented); + $this->assertEquals('one-fr', $augmented->slug()); + + $augmented = $fieldtype->augment(['456']); + $this->assertNull($augmented); // 456 isnt localized + + $augmented = $fieldtype->augment(['789']); + $this->assertNull($augmented); // 789-fr is a draft + } + + /** @test */ + public function it_localizes_the_augmented_items_to_the_current_sites_locale_when_parent_is_not_localizable() + { + Site::setCurrent('fr'); + + $parent = new class + { + // Class does not implement "Localizable" + }; + + EntryFactory::id('123-fr')->origin('123')->locale('fr')->collection('blog')->slug('one-fr')->data(['title' => 'Le One'])->date('2021-01-02')->create(); + EntryFactory::id('789-fr')->origin('789')->locale('fr')->collection('blog')->slug('three-fr')->data(['title' => 'Le Three'])->date('2021-01-02')->create(); + + $augmented = $this->fieldtype([], $parent)->augment(['123', 'invalid', 456, 789, 'draft', 'scheduled', 'expired']); + + $this->assertInstanceOf(Builder::class, $augmented); + $this->assertEveryItemIsInstanceOf(Entry::class, $augmented->get()); + $this->assertEquals(['one-fr', 'three-fr'], $augmented->get()->map->slug()->all()); // only 123 and 789 have localized versions + } + + /** @test */ + public function it_localizes_the_augmented_item_to_the_current_sites_locale_when_parent_is_not_localizable_when_max_items_is_one() + { + Site::setCurrent('fr'); + + $parent = new class + { + // Class does not implement "Localizable" + }; + + EntryFactory::id('123-fr')->origin('123')->locale('fr')->collection('blog')->slug('one-fr')->data(['title' => 'Le One'])->date('2021-01-02')->create(); + + $fieldtype = $this->fieldtype(['max_items' => 1], $parent); + + $augmented = $fieldtype->augment(['123'], $parent); + $this->assertInstanceOf(Entry::class, $augmented); + $this->assertEquals('one-fr', $augmented->slug()); + + $augmented = $fieldtype->augment(['456'], $parent); + $this->assertNull($augmented); // 456 isnt localized + } + + /** @test */ + public function it_shallow_augments_to_a_collection_of_entries() + { + $augmented = $this->fieldtype()->shallowAugment(['123', 'invalid', 456, 'draft', 'scheduled', 'expired']); $this->assertInstanceOf(Collection::class, $augmented); $this->assertEveryItemIsInstanceOf(AugmentedCollection::class, $augmented); @@ -91,10 +188,137 @@ public function it_shallow_augments_to_a_single_entry_when_max_items_is_one() ], $augmented->toArray()); } - public function fieldtype($config = []) + /** @test */ + public function it_localizes_the_shallow_augmented_items_to_the_parent_entrys_locale() { - return (new Entries)->setField(new Field('test', array_merge([ + $parent = EntryFactory::id('parent')->collection('blog')->slug('theparent')->locale('fr')->create(); + + EntryFactory::id('123-fr')->origin('123')->locale('fr')->collection('blog')->slug('one-fr')->data(['title' => 'Le One'])->date('2021-01-02')->create(); + EntryFactory::id('789-fr')->origin('789')->locale('fr')->collection('blog')->slug('three-fr')->data(['title' => 'Le Three'])->date('2021-01-02')->published(false)->create(); + EntryFactory::id('910-fr')->origin('910')->locale('fr')->collection('blog')->slug('four-fr')->data(['title' => 'Le Four'])->date('2021-01-02')->create(); + + $augmented = $this->fieldtype([], $parent)->shallowAugment(['123', 'invalid', 456, 789, 910, 'draft', 'scheduled', 'expired']); + + $this->assertInstanceOf(Collection::class, $augmented); + $this->assertEveryItemIsInstanceOf(AugmentedCollection::class, $augmented); + $this->assertEquals([ + [ + 'id' => '123-fr', + 'title' => 'Le One', + 'url' => '/fr/blog/one-fr', + 'permalink' => 'http://localhost/fr/blog/one-fr', + 'api_url' => 'http://localhost/api/collections/blog/entries/123-fr', + ], + [ + 'id' => '910-fr', + 'title' => 'Le Four', + 'url' => '/fr/blog/four-fr', + 'permalink' => 'http://localhost/fr/blog/four-fr', + 'api_url' => 'http://localhost/api/collections/blog/entries/910-fr', + ], + ], $augmented->toArray()); // 456 isn't localized, and 789-fr is a draft. + } + + /** @test */ + public function it_localizes_the_shallow_augmented_item_to_the_parent_entrys_locale_when_max_items_is_one() + { + $parent = EntryFactory::id('parent')->collection('blog')->slug('theparent')->locale('fr')->create(); + + EntryFactory::id('123-fr')->origin('123')->locale('fr')->collection('blog')->slug('one-fr')->data(['title' => 'Le One'])->date('2021-01-02')->create(); + EntryFactory::id('789-fr')->origin('789')->locale('fr')->collection('blog')->slug('three-fr')->data(['title' => 'Le Three'])->date('2021-01-02')->published(false)->create(); + + $fieldtype = $this->fieldtype(['max_items' => 1], $parent); + + $augmented = $fieldtype->shallowAugment(['123']); + $this->assertInstanceOf(AugmentedCollection::class, $augmented); + $this->assertEquals([ + 'id' => '123-fr', + 'title' => 'Le One', + 'url' => '/fr/blog/one-fr', + 'permalink' => 'http://localhost/fr/blog/one-fr', + 'api_url' => 'http://localhost/api/collections/blog/entries/123-fr', + ], $augmented->toArray()); + + $augmented = $fieldtype->shallowAugment(['456']); + $this->assertNull($augmented); // 456 isnt localized + + $augmented = $fieldtype->shallowAugment(['789']); + $this->assertNull($augmented); // 789-fr is a draft + } + + /** @test */ + public function it_localizes_the_shallow_augmented_items_to_the_current_sites_locale_when_parent_is_not_localizable() + { + Site::setCurrent('fr'); + + $parent = new class + { + // Class does not implement "Localizable" + }; + + EntryFactory::id('123-fr')->origin('123')->locale('fr')->collection('blog')->slug('one-fr')->data(['title' => 'Le One'])->date('2021-01-02')->create(); + EntryFactory::id('789-fr')->origin('789')->locale('fr')->collection('blog')->slug('three-fr')->data(['title' => 'Le Three'])->date('2021-01-02')->create(); + + $augmented = $this->fieldtype([], $parent)->shallowAugment(['123', 'invalid', 456, 789, 'draft', 'scheduled', 'expired']); + + $this->assertInstanceOf(Collection::class, $augmented); + $this->assertEveryItemIsInstanceOf(AugmentedCollection::class, $augmented); + $this->assertEquals([ + [ + 'id' => '123-fr', + 'title' => 'Le One', + 'url' => '/fr/blog/one-fr', + 'permalink' => 'http://localhost/fr/blog/one-fr', + 'api_url' => 'http://localhost/api/collections/blog/entries/123-fr', + ], + [ + 'id' => '789-fr', + 'title' => 'Le Three', + 'url' => '/fr/blog/three-fr', + 'permalink' => 'http://localhost/fr/blog/three-fr', + 'api_url' => 'http://localhost/api/collections/blog/entries/789-fr', + ], + ], $augmented->toArray()); // only 123 and 789 have localized versions + } + + /** @test */ + public function it_localizes_the_shallow_augmented_item_to_the_current_sites_locale_when_parent_is_not_localizable_when_max_items_is_one() + { + Site::setCurrent('fr'); + + $parent = new class + { + // Class does not implement "Localizable" + }; + + EntryFactory::id('123-fr')->origin('123')->locale('fr')->collection('blog')->slug('one-fr')->data(['title' => 'Le One'])->date('2021-01-02')->create(); + + $fieldtype = $this->fieldtype(['max_items' => 1], $parent); + + $augmented = $fieldtype->shallowAugment(['123']); + $this->assertInstanceOf(AugmentedCollection::class, $augmented); + $this->assertEquals([ + 'id' => '123-fr', + 'title' => 'Le One', + 'url' => '/fr/blog/one-fr', + 'permalink' => 'http://localhost/fr/blog/one-fr', + 'api_url' => 'http://localhost/api/collections/blog/entries/123-fr', + ], $augmented->toArray()); + + $augmented = $fieldtype->shallowAugment(['456']); + $this->assertNull($augmented); // 456 isnt localized + } + + public function fieldtype($config = [], $parent = null) + { + $field = new Field('test', array_merge([ 'type' => 'entries', - ], $config))); + ], $config)); + + if ($parent) { + $field->setParent($parent); + } + + return (new Entries)->setField($field); } } diff --git a/tests/View/Antlers/ParserTest.php b/tests/View/Antlers/ParserTest.php index bf3be2a6228..c33fa629c68 100644 --- a/tests/View/Antlers/ParserTest.php +++ b/tests/View/Antlers/ParserTest.php @@ -8,7 +8,9 @@ use Illuminate\Support\Facades\Log; use Illuminate\Support\MessageBag; use Illuminate\Support\ViewErrorBag; +use Mockery; use Statamic\Contracts\Data\Augmentable; +use Statamic\Contracts\Query\Builder; use Statamic\Data\HasAugmentedData; use Statamic\Facades\Antlers; use Statamic\Facades\Entry; @@ -2226,6 +2228,62 @@ public function it_parses_single_and_tag_pairs_with_modifiers() $this->assertEquals('3', $this->parse('{{ items limit="2" }}<{{ value }}>{{ /items }}{{ items | count }}', $data)); $this->assertEquals('3', $this->parse('{{ items | count }}{{ items limit="2" }}<{{ value }}>{{ /items }}', $data)); } + + /** @test */ + public function it_passes_along_query_builder_values_to_the_query_tag() + { + $builder = Mockery::mock(Builder::class); + $builder->shouldReceive('get')->once()->andReturn(collect([ + ['title' => 'Foo'], + ['title' => 'Bar'], + ])); + + $this->assertEquals('', $this->parse('{{ my_query }}<{{ title }}>{{ /my_query }}', [ + 'my_query' => $builder, + ])); + } + + /** @test */ + public function it_passes_along_query_builder_augmented_values_to_the_query_tag() + { + $builder = Mockery::mock(Builder::class); + $builder->shouldReceive('get')->once()->andReturn(collect([ + ['title' => 'Foo'], + ['title' => 'Bar'], + ])); + + $this->assertEquals('', $this->parse('{{ my_query }}<{{ title }}>{{ /my_query }}', [ + 'my_query' => new Value($builder), + ])); + } + + /** @test */ + public function it_can_reach_into_query_builders() + { + $builder = Mockery::mock(Builder::class); + $builder->shouldReceive('get')->times(2)->andReturn(collect([ + ['title' => 'Foo'], + ['title' => 'Bar'], + ])); + + $this->assertEquals('', $this->parse('<{{ my_query:1:title }}><{{ my_query:0:title }}>', [ + 'my_query' => $builder, + ])); + } + + /** @test */ + public function it_can_reach_into_query_builders_through_values() + { + $builder = Mockery::mock(Builder::class); + $builder->shouldReceive('get')->times(2)->andReturn(collect([ + ['title' => 'Foo'], + ['title' => 'Bar'], + ])); + + $this->assertEquals('', $this->parse('<{{ my_query:1:title }}><{{ my_query:0:title }}>', [ + 'my_query' => new Value($builder), + ])); + } } class NonArrayableObject