diff --git a/src/Fieldtypes/Link.php b/src/Fieldtypes/Link.php index 816c4c2e2a..61a9371e56 100644 --- a/src/Fieldtypes/Link.php +++ b/src/Fieldtypes/Link.php @@ -7,9 +7,11 @@ use Statamic\Contracts\Entries\Entry; use Statamic\Facades; use Statamic\Facades\Blink; +use Statamic\Facades\GraphQL; use Statamic\Facades\Site; use Statamic\Fields\Field; use Statamic\Fields\Fieldtype; +use Statamic\Fieldtypes\Link\ArrayableLink; use Statamic\Support\Str; class Link extends Fieldtype @@ -42,13 +44,11 @@ protected function configFieldItems(): array public function augment($value) { - if (! $value) { - return null; - } - - $redirect = ResolveRedirect::resolve($value, $this->field->parent(), true); - - return $redirect === 404 ? null : $redirect; + return new ArrayableLink( + $value + ? ResolveRedirect::item($value, $this->field->parent(), true) + : null + ); } public function preload() @@ -174,4 +174,20 @@ private function showAssetOption() { return $this->config('container') !== null; } + + public function toGqlType() + { + return [ + 'type' => GraphQL::string(), + 'resolve' => function ($item, $args, $context, $info) { + if (! $augmented = $item->resolveGqlValue($info->fieldName)) { + return null; + } + + $item = $augmented->value(); + + return is_object($item) ? $item->url() : $item; + }, + ]; + } } diff --git a/src/Fieldtypes/Link/ArrayableLink.php b/src/Fieldtypes/Link/ArrayableLink.php new file mode 100644 index 0000000000..7ac7d04ec0 --- /dev/null +++ b/src/Fieldtypes/Link/ArrayableLink.php @@ -0,0 +1,31 @@ +url(); + } + + public function toArray() + { + return is_object($this->value) + ? $this->value->toAugmentedArray() + : ['url' => $this->url()]; + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + return $this->url(); // Use a string for backwards compatibility in the REST API, etc. + } + + private function url() + { + return is_object($this->value) ? $this->value?->url() : $this->value; + } +} diff --git a/src/Routing/ResolveRedirect.php b/src/Routing/ResolveRedirect.php index cc3b6b6566..a3176085cc 100644 --- a/src/Routing/ResolveRedirect.php +++ b/src/Routing/ResolveRedirect.php @@ -22,18 +22,33 @@ public function resolve($redirect, $parent = null, $localize = false) return null; } + if (! $item = $this->item($redirect, $parent, $localize)) { + return 404; + } + + return is_object($item) ? $item->url() : $item; + } + + public function item($redirect, $parent = null, $localize = false) + { + if (is_null($redirect)) { + return null; + } + if ($redirect === '@child') { - $redirect = $this->firstChildUrl($parent); + return $this->firstChild($parent); } if (Str::startsWith($redirect, 'entry::')) { $id = Str::after($redirect, 'entry::'); - $redirect = optional($this->findEntry($id, $parent, $localize))->url() ?? 404; + + return $this->findEntry($id, $parent, $localize); } if (Str::startsWith($redirect, 'asset::')) { $id = Str::after($redirect, 'asset::'); - $redirect = optional(Facades\Asset::find($id))->url() ?? 404; + + return Facades\Asset::find($id); } return is_numeric($redirect) ? (int) $redirect : $redirect; @@ -56,7 +71,7 @@ private function findEntry($id, $parent, $localize) return $entry->in($site) ?? $entry; } - private function firstChildUrl($parent) + private function firstChild($parent) { if (! $parent || ! $parent instanceof Entry) { throw new \Exception("Cannot resolve a page's child redirect without providing a page."); @@ -71,9 +86,9 @@ private function firstChildUrl($parent) : $parent->pages()->all(); if ($children->isEmpty()) { - return 404; + return null; } - return $children->first()->url(); + return $children->first(); } } diff --git a/tests/Feature/GraphQL/Fieldtypes/LinkFieldtypeTest.php b/tests/Feature/GraphQL/Fieldtypes/LinkFieldtypeTest.php index 44df5ef77e..cc424e31ee 100644 --- a/tests/Feature/GraphQL/Fieldtypes/LinkFieldtypeTest.php +++ b/tests/Feature/GraphQL/Fieldtypes/LinkFieldtypeTest.php @@ -3,6 +3,8 @@ namespace Tests\Feature\GraphQL\Fieldtypes; use Facades\Statamic\Routing\ResolveRedirect; +use Mockery; +use Statamic\Contracts\Entries\Entry; /** @group graphql */ class LinkFieldtypeTest extends FieldtypeTestCase @@ -17,7 +19,7 @@ public function it_gets_null_when_undefined() ], ]); - ResolveRedirect::shouldReceive('resolve')->never(); + ResolveRedirect::shouldReceive('item')->never(); $this->assertGqlEntryHas('link', ['link' => null]); } @@ -32,7 +34,7 @@ public function it_gets_a_hardcoded_url() ], ]); - ResolveRedirect::shouldReceive('resolve')->once()->with('/hardcoded', $entry, true)->andReturn('/hardcoded'); + ResolveRedirect::shouldReceive('item')->once()->with('/hardcoded', $entry, true)->andReturn('/hardcoded'); $this->assertGqlEntryHas('link', ['link' => '/hardcoded']); } @@ -47,7 +49,10 @@ public function it_gets_an_entry() ], ]); - ResolveRedirect::shouldReceive('resolve')->once()->with('entry::123', $entry, true)->andReturn('/the-entry-url'); + $another = Mockery::mock(Entry::class); + $another->shouldReceive('url')->once()->andReturn('/the-entry-url'); + + ResolveRedirect::shouldReceive('item')->once()->with('entry::123', $entry, true)->andReturn($another); $this->assertGqlEntryHas('link', ['link' => '/the-entry-url']); } @@ -62,7 +67,10 @@ public function it_gets_a_child_url() ], ]); - ResolveRedirect::shouldReceive('resolve')->once()->with('@child', $entry, true)->andReturn('/the-first-child'); + $another = Mockery::mock(Entry::class); + $another->shouldReceive('url')->once()->andReturn('/the-first-child'); + + ResolveRedirect::shouldReceive('item')->once()->with('@child', $entry, true)->andReturn($another); $this->assertGqlEntryHas('link', ['link' => '/the-first-child']); } @@ -77,7 +85,7 @@ public function it_gets_a_404() ], ]); - ResolveRedirect::shouldReceive('resolve')->once()->with('entry::unknown', $entry, true)->andReturn(404); + ResolveRedirect::shouldReceive('item')->once()->with('entry::unknown', $entry, true)->andReturnNull(); $this->assertGqlEntryHas('link', ['link' => null]); } diff --git a/tests/Fieldtypes/LinkTest.php b/tests/Fieldtypes/LinkTest.php index a5dc099c3e..f9b1aad5ea 100644 --- a/tests/Fieldtypes/LinkTest.php +++ b/tests/Fieldtypes/LinkTest.php @@ -3,7 +3,9 @@ namespace Tests\Fieldtypes; use Facades\Statamic\Routing\ResolveRedirect; +use Mockery; use Statamic\Entries\Entry; +use Statamic\Fields\ArrayableString; use Statamic\Fields\Field; use Statamic\Fieldtypes\Link; use Tests\TestCase; @@ -11,35 +13,62 @@ class LinkTest extends TestCase { /** @test */ - public function it_augments_to_url() + public function it_augments_string_to_string() { - ResolveRedirect::shouldReceive('resolve') - ->with('entry::test', $parent = new Entry, true) + ResolveRedirect::shouldReceive('item') + ->with('/foo', $parent = new Entry, true) ->once() - ->andReturn('/the-redirect'); + ->andReturn('/foo'); $field = new Field('test', ['type' => 'link']); $field->setParent($parent); $fieldtype = (new Link)->setField($field); - $this->assertEquals('/the-redirect', $fieldtype->augment('entry::test')); + $augmented = $fieldtype->augment('/foo'); + $this->assertInstanceOf(ArrayableString::class, $augmented); + $this->assertEquals('/foo', $augmented->value()); + $this->assertEquals(['url' => '/foo'], $augmented->toArray()); } /** @test */ - public function it_augments_invalid_entry_to_null() + public function it_augments_reference_to_object() { - // invalid entries come back from the ResolveRedirect class as a 404 integer + $entry = Mockery::mock(); + $entry->shouldReceive('url')->once()->andReturn('/the-entry-url'); + $entry->shouldReceive('toAugmentedArray')->once()->andReturn('augmented entry array'); - ResolveRedirect::shouldReceive('resolve') + ResolveRedirect::shouldReceive('item') ->with('entry::test', $parent = new Entry, true) ->once() - ->andReturn(404); + ->andReturn($entry); + + $field = new Field('test', ['type' => 'link']); + $field->setParent($parent); + $fieldtype = (new Link)->setField($field); + + $augmented = $fieldtype->augment('entry::test'); + $this->assertInstanceOf(ArrayableString::class, $augmented); + $this->assertEquals($entry, $augmented->value()); + $this->assertEquals('/the-entry-url', (string) $augmented); + $this->assertEquals('augmented entry array', $augmented->toArray()); + } + + /** @test */ + public function it_augments_invalid_object_to_null() + { + ResolveRedirect::shouldReceive('item') + ->with('entry::invalid', $parent = new Entry, true) + ->once() + ->andReturnNull(); $field = new Field('test', ['type' => 'link']); $field->setParent($parent); $fieldtype = (new Link)->setField($field); - $this->assertNull($fieldtype->augment('entry::test')); + $augmented = $fieldtype->augment('entry::invalid'); + $this->assertInstanceOf(ArrayableString::class, $augmented); + $this->assertNull($augmented->value()); + $this->assertEquals(['url' => null], $augmented->toArray()); } /** @test */ @@ -53,6 +82,9 @@ public function it_augments_null_to_null() $field->setParent(new Entry); $fieldtype = (new Link)->setField($field); - $this->assertNull($fieldtype->augment(null)); + $augmented = $fieldtype->augment(null); + $this->assertInstanceOf(ArrayableString::class, $augmented); + $this->assertNull($augmented->value()); + $this->assertEquals(['url' => null], $augmented->toArray()); } } diff --git a/tests/Routing/ResolveRedirectTest.php b/tests/Routing/ResolveRedirectTest.php index 596412d10b..76f41bb8e3 100644 --- a/tests/Routing/ResolveRedirectTest.php +++ b/tests/Routing/ResolveRedirectTest.php @@ -66,6 +66,7 @@ public function it_resolves_first_child() $parent->shouldReceive('pages')->andReturn($children); $this->assertEquals('/parent/first-child', $resolver('@child', $parent)); + $this->assertEquals($child, $resolver->item('@child', $parent)); } /** @test */ @@ -87,6 +88,7 @@ public function it_resolves_first_child_through_an_entry() $parent->shouldReceive('page')->andReturn($parentPage); $this->assertEquals('/parent/first-child', $resolver('@child', $parent)); + $this->assertEquals($child, $resolver->item('@child', $parent)); } /** @test */ @@ -116,6 +118,7 @@ public function it_resolves_a_first_child_redirect_when_its_a_root_page() $root->shouldReceive('structure')->andReturn($structure); $this->assertEquals('/parent/first-child', $resolver('@child', $root)); + $this->assertEquals($child, $resolver->item('@child', $root)); } /** @test */ @@ -131,6 +134,7 @@ public function a_parent_without_a_child_resolves_to_a_404() $parent->shouldReceive('pages')->andReturn($pages); $this->assertSame(404, $resolver('@child', $parent)); + $this->assertSame(null, $resolver->item('@child', $parent)); } /** @test */ @@ -139,9 +143,10 @@ public function it_resolves_references_to_entries() $resolver = new ResolveRedirect; $entry = Mockery::mock(Entry::class)->shouldReceive('url')->once()->andReturn('/the-entry')->getMock(); - Facades\Entry::shouldReceive('find')->with('123')->once()->andReturn($entry); + Facades\Entry::shouldReceive('find')->with('123')->twice()->andReturn($entry); $this->assertEquals('/the-entry', $resolver('entry::123')); + $this->assertEquals($entry, $resolver->item('entry::123')); } /** @test */ @@ -151,10 +156,11 @@ public function it_resolves_references_to_entries_localized() $parentEntry = Mockery::mock(Entry::class); $frenchEntry = Mockery::mock(Entry::class)->shouldReceive('url')->once()->andReturn('/le-entry')->getMock(); - $defaultEntry = Mockery::mock(Entry::class)->shouldReceive('in')->once()->andReturn($frenchEntry)->getMock(); - Facades\Entry::shouldReceive('find')->with('123')->once()->andReturn($defaultEntry); + $defaultEntry = Mockery::mock(Entry::class)->shouldReceive('in')->twice()->andReturn($frenchEntry)->getMock(); + Facades\Entry::shouldReceive('find')->with('123')->twice()->andReturn($defaultEntry); $this->assertEquals('/le-entry', $resolver('entry::123', $parentEntry, true)); + $this->assertEquals($frenchEntry, $resolver->item('entry::123', $parentEntry, true)); } /** @test */ @@ -164,11 +170,12 @@ public function it_resolves_references_to_entries_localized_with_fallback() $parentEntry = Mockery::mock(Entry::class); $entry = Mockery::mock(Entry::class); - $entry->shouldReceive('in')->once()->andReturn(null); + $entry->shouldReceive('in')->twice()->andReturn(null); $entry->shouldReceive('url')->once()->andReturn('/the-entry'); - Facades\Entry::shouldReceive('find')->with('123')->once()->andReturn($entry); + Facades\Entry::shouldReceive('find')->with('123')->twice()->andReturn($entry); $this->assertEquals('/the-entry', $resolver('entry::123', $parentEntry, true)); + $this->assertEquals($entry, $resolver->item('entry::123', $parentEntry, true)); } /** @test */ @@ -177,9 +184,10 @@ public function it_resolves_references_to_assets() $resolver = new ResolveRedirect; $asset = Mockery::mock(Asset::class)->shouldReceive('url')->once()->andReturn('/assets/foo/bar/baz.jpg')->getMock(); - Facades\Asset::shouldReceive('find')->with('foo::bar/baz.jpg')->once()->andReturn($asset); + Facades\Asset::shouldReceive('find')->with('foo::bar/baz.jpg')->twice()->andReturn($asset); $this->assertEquals('/assets/foo/bar/baz.jpg', $resolver('asset::foo::bar/baz.jpg')); + $this->assertEquals($asset, $resolver->item('asset::foo::bar/baz.jpg')); } /** @test */ @@ -187,9 +195,10 @@ public function unknown_entry_ids_resolve_to_404() { $resolver = new ResolveRedirect; - Facades\Entry::shouldReceive('find')->with('123')->once()->andReturnNull(); + Facades\Entry::shouldReceive('find')->with('123')->twice()->andReturnNull(); $this->assertSame(404, $resolver('entry::123')); + $this->assertSame(null, $resolver->item('entry::123')); } /** @test */