diff --git a/src/Data/AbstractAugmented.php b/src/Data/AbstractAugmented.php index 2d79ff3eba1..63b59544754 100644 --- a/src/Data/AbstractAugmented.php +++ b/src/Data/AbstractAugmented.php @@ -2,6 +2,7 @@ namespace Statamic\Data; +use ReflectionMethod; use Statamic\Contracts\Data\Augmented; use Statamic\Fields\Value; use Statamic\Statamic; @@ -97,11 +98,25 @@ private function methodExistsOnThisClass(string $method): bool private function methodExistsOnData(string $handle, string $method): bool { - return method_exists($this->data, $method) + // Non-public methods route through __call(), which re-augments the same handle forever + return $this->publicMethodExistsOnData($method) && collect($this->keys())->contains(Str::snake($handle)) && ! in_array($handle, ['hook', 'value', 'entry']); } + private function publicMethodExistsOnData(string $method): bool + { + static $cache = []; + + if (! method_exists($this->data, $method)) { + return false; + } + + $key = get_class($this->data).'::'.$method; + + return $cache[$key] ??= (new ReflectionMethod($this->data, $method))->isPublic(); + } + protected function getFromData($handle) { $value = method_exists($this->data, 'value') ? $this->data->value($handle) : $this->data->get($handle); diff --git a/tests/Data/AugmentedTest.php b/tests/Data/AugmentedTest.php index cce9ebae385..c424c71c825 100644 --- a/tests/Data/AugmentedTest.php +++ b/tests/Data/AugmentedTest.php @@ -10,6 +10,7 @@ use Statamic\Facades\Blueprint; use Statamic\Fields\Fieldtype; use Statamic\Fields\Value; +use Statamic\Support\Str; use Tests\TestCase; class AugmentedTest extends TestCase @@ -332,6 +333,36 @@ public function no_infinite_loop_when_getting_keys_that_match_methods() $this->assertEqualsValue('selected', $augmented->get('select')); $this->assertEqualsValue('excepted', $augmented->get('except')); } + + #[Test] + public function it_ignores_non_public_methods_on_the_thing() + { + $thing = new ThingWithNonPublicMethods([ + 'protected_method' => 'protected data value', + 'private_method' => 'private data value', + 'public_method' => 'public data value', + ]); + + $augmented = new BaseAugmentedThing($thing); + + $this->assertEqualsValue('protected data value', $augmented->get('protected_method')); + $this->assertEqualsValue('private data value', $augmented->get('private_method')); + $this->assertEqualsValue('from the public method', $augmented->get('public_method')); + } + + #[Test] + public function it_uses_protected_methods_on_the_augmented_thing() + { + $augmented = new class($this->thing) extends BaseAugmentedThing + { + protected function foo() + { + return 'from the protected method'; + } + }; + + $this->assertEqualsValue('from the protected method', $augmented->get('foo')); + } } class Thing @@ -362,6 +393,34 @@ public function cantCallMe() } } +class ThingWithNonPublicMethods extends Thing +{ + public function __call($method, $args) + { + return $this->augmentedValue(Str::snake($method)); + } + + public function augmentedValue($key) + { + return (new BaseAugmentedThing($this))->get($key); + } + + public function publicMethod() + { + return 'from the public method'; + } + + protected function protectedMethod() + { + return 'from the protected method'; + } + + private function privateMethod() + { + return 'from the private method'; + } +} + class BlueprintThing extends Thing { public function blueprint() diff --git a/tests/Data/Entries/AugmentedEntryTest.php b/tests/Data/Entries/AugmentedEntryTest.php index 1fcf1fe9637..5fffd4405d7 100644 --- a/tests/Data/Entries/AugmentedEntryTest.php +++ b/tests/Data/Entries/AugmentedEntryTest.php @@ -251,4 +251,22 @@ public function it_doesnt_evaluated_computed_callbacks_when_getting_keys() $augmented->get('computed'); $this->assertEquals(2, $computedCallbackCount); } + + #[Test] + public function it_gets_a_field_whose_name_matches_a_non_public_method_on_the_entry() + { + $blueprint = Blueprint::makeFromFields([ + 'cp_url' => ['type' => 'text'], + ])->setHandle('test'); + Blueprint::shouldReceive('in')->with('collections/test')->andReturn(collect(['test' => $blueprint])); + + tap(Collection::make('test'))->save(); + + $entry = EntryFactory::collection('test') + ->slug('entry-slug') + ->data(['cp_url' => 'the stored value']) + ->create(); + + $this->assertEquals('the stored value', $entry->augmentedValue('cp_url')->value()); + } }