From ad69b5555fce022d71e99a72c3af4ec36f5c7d34 Mon Sep 17 00:00:00 2001 From: Till Schander Date: Fri, 18 Apr 2025 16:51:34 +0200 Subject: [PATCH 1/5] resolve properties of classes with magic getters --- src/Render/RenderContext.php | 1 + tests/Integration/ContextTest.php | 41 +++++++++++++++++++++++++++++++ tests/Stubs/MagicClass.php | 36 +++++++++++++++++++++++++++ tests/Stubs/SimpleClass.php | 15 +++++++++++ 4 files changed, 93 insertions(+) create mode 100644 tests/Stubs/MagicClass.php create mode 100644 tests/Stubs/SimpleClass.php diff --git a/src/Render/RenderContext.php b/src/Render/RenderContext.php index b8ad86b..94428d8 100644 --- a/src/Render/RenderContext.php +++ b/src/Render/RenderContext.php @@ -199,6 +199,7 @@ public function internalContextLookup(mixed $scope, int|string $key): mixed 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_object($scope) && (isset($scope->{$key}) || is_null($scope->{$key} ?? false)) => $scope->{$key}, default => new MissingValue, }; } catch (UndefinedDropMethodException) { diff --git a/tests/Integration/ContextTest.php b/tests/Integration/ContextTest.php index 64de3aa..b210789 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,42 @@ 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(fn() => $context->get('object.protectedProperty'))->toThrow(Error::class); + + 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, +]); + +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..95a203e --- /dev/null +++ b/tests/Stubs/MagicClass.php @@ -0,0 +1,36 @@ + 'foo', + 'nullProperty' => null, + ]; + + public function simpleMethod(): string + { + return 'foo'; + } + + public function __get(string $property) + { + return $this->data[$property]; + } + + public function __set(string $property, $value) + { + $this->data[$property] = $value; + } + + public function __isset($property) + { + return array_key_exists($property, $this->data); + } + + public function __unset($property) + { + unset($this->data[$property]); + } +} diff --git a/tests/Stubs/SimpleClass.php b/tests/Stubs/SimpleClass.php new file mode 100644 index 0000000..b8f2ad1 --- /dev/null +++ b/tests/Stubs/SimpleClass.php @@ -0,0 +1,15 @@ + Date: Fri, 18 Apr 2025 14:52:15 +0000 Subject: [PATCH 2/5] Fix styling --- tests/Integration/ContextTest.php | 6 +++--- tests/Stubs/SimpleClass.php | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/Integration/ContextTest.php b/tests/Integration/ContextTest.php index b210789..5e3ee6a 100644 --- a/tests/Integration/ContextTest.php +++ b/tests/Integration/ContextTest.php @@ -696,11 +696,11 @@ function () use (&$global) { test('internal context lookup with simple object', function (bool $strict) { $context = new RenderContext(options: new RenderContextOptions(strictVariables: $strict)); - $context->set('object', new SimpleClass()); + $context->set('object', new SimpleClass); expect($context->get('object.simpleProperty'))->toBe('foo'); expect($context->get('object.nullProperty'))->toBe(null); - expect(fn() => $context->get('object.protectedProperty'))->toThrow(Error::class); + expect(fn () => $context->get('object.protectedProperty'))->toThrow(Error::class); if ($strict) { expect($context->get('object.nonExistingProperty'))->toBeInstanceOf(UndefinedVariable::class); @@ -716,7 +716,7 @@ function () use (&$global) { test('internal context lookup magic object', function (bool $strict) { $context = new RenderContext(options: new RenderContextOptions(strictVariables: $strict)); - $context->set('object', new MagicClass()); + $context->set('object', new MagicClass); expect($context->get('object.simpleProperty'))->toBe('foo'); expect($context->get('object.nullProperty'))->toBe(null); diff --git a/tests/Stubs/SimpleClass.php b/tests/Stubs/SimpleClass.php index b8f2ad1..324591d 100644 --- a/tests/Stubs/SimpleClass.php +++ b/tests/Stubs/SimpleClass.php @@ -5,7 +5,9 @@ class SimpleClass { public string $simpleProperty = 'foo'; + public ?string $nullProperty = null; + protected string $protectedProperty = 'foo'; public function simpleMethod(): string From be979b1e6b537dd3788d71a441e793674cedf95f Mon Sep 17 00:00:00 2001 From: Fabio Capucci Date: Sat, 19 Apr 2025 14:25:11 +0200 Subject: [PATCH 3/5] change object property check --- src/Render/RenderContext.php | 20 +++++++++++++++++--- tests/Integration/ContextTest.php | 3 ++- tests/Stubs/MagicClass.php | 12 ++++++++---- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/Render/RenderContext.php b/src/Render/RenderContext.php index 94428d8..94e8a29 100644 --- a/src/Render/RenderContext.php +++ b/src/Render/RenderContext.php @@ -196,10 +196,9 @@ 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_object($scope) && (isset($scope->{$key}) || is_null($scope->{$key} ?? false)) => $scope->{$key}, + is_array($scope) && array_key_exists($key, $scope) => $scope[$key], + is_object($scope) && $this->objectHasProperty($scope, (string) $key) => $scope->{$key}, default => new MissingValue, }; } catch (UndefinedDropMethodException) { @@ -209,6 +208,21 @@ public function internalContextLookup(mixed $scope, int|string $key): mixed return $this->normalizeValue($value); } + protected function objectHasProperty(object $object, string $property): bool + { + if (property_exists($object, $property) || method_exists($object, '__get')) { + try { + $value = $object->{$property}; + + return true; + } catch (Throwable) { + return false; + } + } + + return false; + } + 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 5e3ee6a..9d57925 100644 --- a/tests/Integration/ContextTest.php +++ b/tests/Integration/ContextTest.php @@ -700,12 +700,13 @@ function () use (&$global) { expect($context->get('object.simpleProperty'))->toBe('foo'); expect($context->get('object.nullProperty'))->toBe(null); - expect(fn () => $context->get('object.protectedProperty'))->toThrow(Error::class); 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); } diff --git a/tests/Stubs/MagicClass.php b/tests/Stubs/MagicClass.php index 95a203e..e8b614d 100644 --- a/tests/Stubs/MagicClass.php +++ b/tests/Stubs/MagicClass.php @@ -14,22 +14,26 @@ public function simpleMethod(): string return 'foo'; } - public function __get(string $property) + 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, $value) + public function __set(string $property, mixed $value): void { $this->data[$property] = $value; } - public function __isset($property) + public function __isset(string $property): bool { return array_key_exists($property, $this->data); } - public function __unset($property) + public function __unset(string $property): void { unset($this->data[$property]); } From 9578313316c6b414b0d2987db6c7f58e08c6ff7b Mon Sep 17 00:00:00 2001 From: Fabio Capucci Date: Sat, 19 Apr 2025 14:50:48 +0200 Subject: [PATCH 4/5] use get_object_vars & isset --- src/Render/RenderContext.php | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/Render/RenderContext.php b/src/Render/RenderContext.php index 94e8a29..980caa0 100644 --- a/src/Render/RenderContext.php +++ b/src/Render/RenderContext.php @@ -210,17 +210,21 @@ public function internalContextLookup(mixed $scope, int|string $key): mixed protected function objectHasProperty(object $object, string $property): bool { - if (property_exists($object, $property) || method_exists($object, '__get')) { - try { - $value = $object->{$property}; + 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; - } catch (Throwable) { - return false; } - } - return false; + return false; + } catch (Throwable) { + return false; + } } public function normalizeValue(mixed $value): mixed From 06160fd0023d15c879cea2c1cbda584fa9c9bfd3 Mon Sep 17 00:00:00 2001 From: Till Schander Date: Sun, 20 Apr 2025 17:30:27 +0200 Subject: [PATCH 5/5] resolve static class properties in variables --- src/Render/RenderContext.php | 9 +++++++++ tests/Integration/ContextTest.php | 2 ++ tests/Stubs/SimpleClass.php | 4 ++++ 3 files changed, 15 insertions(+) diff --git a/src/Render/RenderContext.php b/src/Render/RenderContext.php index 980caa0..ab8ce46 100644 --- a/src/Render/RenderContext.php +++ b/src/Render/RenderContext.php @@ -199,6 +199,7 @@ public function internalContextLookup(mixed $scope, int|string $key): mixed $scope instanceof Drop => $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) { @@ -227,6 +228,14 @@ protected function objectHasProperty(object $object, string $property): bool } } + 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 9d57925..6cbb088 100644 --- a/tests/Integration/ContextTest.php +++ b/tests/Integration/ContextTest.php @@ -700,6 +700,8 @@ function () use (&$global) { 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); diff --git a/tests/Stubs/SimpleClass.php b/tests/Stubs/SimpleClass.php index 324591d..d498251 100644 --- a/tests/Stubs/SimpleClass.php +++ b/tests/Stubs/SimpleClass.php @@ -10,6 +10,10 @@ class SimpleClass protected string $protectedProperty = 'foo'; + public static string $staticProperty = 'foo'; + + public static ?string $staticNullProperty = null; + public function simpleMethod(): string { return 'foo';