diff --git a/src/Render/RenderContext.php b/src/Render/RenderContext.php index b8ad86b..ab8ce46 100644 --- a/src/Render/RenderContext.php +++ b/src/Render/RenderContext.php @@ -196,9 +196,10 @@ public function internalContextLookup(mixed $scope, int|string $key): mixed { try { $value = match (true) { - is_array($scope) && array_key_exists($key, $scope) => $scope[$key], $scope instanceof Drop => $scope->{$key}, - is_object($scope) && property_exists($scope, (string) $key) => $scope->{$key}, + is_array($scope) && array_key_exists($key, $scope) => $scope[$key], + is_object($scope) && $this->objectHasProperty($scope, (string) $key) => $scope->{$key}, + is_object($scope) && $this->objectHasStaticProperty($scope, (string) $key) => $scope::$$key, default => new MissingValue, }; } catch (UndefinedDropMethodException) { @@ -208,6 +209,33 @@ public function internalContextLookup(mixed $scope, int|string $key): mixed return $this->normalizeValue($value); } + protected function objectHasProperty(object $object, string $property): bool + { + try { + // Check if the property is a public property + if (array_key_exists($property, get_object_vars($object))) { + return true; + } + + // Check if the property is accessible via __get() and __isset() + if (isset($object->{$property})) { + return true; + } + + return false; + } catch (Throwable) { + return false; + } + } + + protected function objectHasStaticProperty(object $object, string $property): bool + { + $className = get_class($object); + $staticProperties = get_class_vars($className); + + return array_key_exists($property, $staticProperties); + } + public function normalizeValue(mixed $value): mixed { if (is_object($value) && isset($this->sharedState->computedObjectsCache[$value])) { diff --git a/tests/Integration/ContextTest.php b/tests/Integration/ContextTest.php index 64de3aa..6cbb088 100644 --- a/tests/Integration/ContextTest.php +++ b/tests/Integration/ContextTest.php @@ -10,6 +10,8 @@ use Keepsuit\Liquid\Tests\Stubs\ContextSensitiveDrop; use Keepsuit\Liquid\Tests\Stubs\CounterDrop; use Keepsuit\Liquid\Tests\Stubs\HundredCents; +use Keepsuit\Liquid\Tests\Stubs\MagicClass; +use Keepsuit\Liquid\Tests\Stubs\SimpleClass; test('variables', function (bool $strict) { $context = new RenderContext(options: new RenderContextOptions(strictVariables: $strict)); @@ -691,3 +693,45 @@ function () use (&$global) { 'default' => false, 'strict' => true, ]); + +test('internal context lookup with simple object', function (bool $strict) { + $context = new RenderContext(options: new RenderContextOptions(strictVariables: $strict)); + $context->set('object', new SimpleClass); + + expect($context->get('object.simpleProperty'))->toBe('foo'); + expect($context->get('object.nullProperty'))->toBe(null); + expect($context->get('object.staticProperty'))->toBe('foo'); + expect($context->get('object.staticNullProperty'))->toBe(null); + + if ($strict) { + expect($context->get('object.protectedProperty'))->toBeInstanceOf(UndefinedVariable::class); + expect($context->get('object.nonExistingProperty'))->toBeInstanceOf(UndefinedVariable::class); + expect($context->get('object.simpleMethod'))->toBeInstanceOf(UndefinedVariable::class); + } else { + expect($context->get('object.protectedProperty'))->toBe(null); + expect($context->get('object.nonExistingProperty'))->toBe(null); + expect($context->get('object.simpleMethod'))->toBe(null); + } +})->with([ + 'default' => false, + 'strict' => true, +]); + +test('internal context lookup magic object', function (bool $strict) { + $context = new RenderContext(options: new RenderContextOptions(strictVariables: $strict)); + $context->set('object', new MagicClass); + + expect($context->get('object.simpleProperty'))->toBe('foo'); + expect($context->get('object.nullProperty'))->toBe(null); + + if ($strict) { + expect($context->get('object.nonExistingProperty'))->toBeInstanceOf(UndefinedVariable::class); + expect($context->get('object.simpleMethod'))->toBeInstanceOf(UndefinedVariable::class); + } else { + expect($context->get('object.nonExistingProperty'))->toBe(null); + expect($context->get('object.simpleMethod'))->toBe(null); + } +})->with([ + 'default' => false, + 'strict' => true, +]); diff --git a/tests/Stubs/MagicClass.php b/tests/Stubs/MagicClass.php new file mode 100644 index 0000000..e8b614d --- /dev/null +++ b/tests/Stubs/MagicClass.php @@ -0,0 +1,40 @@ + 'foo', + 'nullProperty' => null, + ]; + + public function simpleMethod(): string + { + return 'foo'; + } + + public function __get(string $property): mixed + { + if (! array_key_exists($property, $this->data)) { + throw new \Error('Property does not exist: '.$property); + } + + return $this->data[$property]; + } + + public function __set(string $property, mixed $value): void + { + $this->data[$property] = $value; + } + + public function __isset(string $property): bool + { + return array_key_exists($property, $this->data); + } + + public function __unset(string $property): void + { + unset($this->data[$property]); + } +} diff --git a/tests/Stubs/SimpleClass.php b/tests/Stubs/SimpleClass.php new file mode 100644 index 0000000..d498251 --- /dev/null +++ b/tests/Stubs/SimpleClass.php @@ -0,0 +1,21 @@ +