diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index d7ccb782..cd083ec7 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -4,6 +4,10 @@ on: pull_request: push: +defaults: + run: + shell: bash + jobs: phpunit: name: "PHPUnit tests" @@ -19,8 +23,10 @@ jobs: - "7.4" - "8.0" - "8.1" + - "8.2" operating-system: - "ubuntu-latest" + fail-fast: false steps: - name: "Checkout" @@ -44,7 +50,10 @@ jobs: restore-keys: "php-${{ matrix.php-version }}" - name: "Test with lowest dependencies" - run: "composer update --prefer-lowest --no-interaction --no-progress --no-suggest && vendor/bin/simple-phpunit" + if: "matrix.php-version != '8.2'" + run: | + composer update --prefer-lowest --no-interaction --no-progress --no-suggest && vendor/bin/simple-phpunit - name: "Test with highest dependencies" - run: "composer update --no-interaction --no-progress --no-suggest && vendor/bin/simple-phpunit" + run: | + composer update --no-interaction --no-progress --no-suggest $([[ "${{ matrix.php-version }}" = "8.2" ]] && echo ' --ignore-platform-req=php+') && vendor/bin/simple-phpunit diff --git a/src/ProxyManager/Generator/ValueGenerator.php b/src/ProxyManager/Generator/ValueGenerator.php index e7d938af..a0ef743f 100644 --- a/src/ProxyManager/Generator/ValueGenerator.php +++ b/src/ProxyManager/Generator/ValueGenerator.php @@ -10,6 +10,7 @@ use function explode; use function implode; +use function in_array; use function preg_replace; use function preg_split; use function rtrim; @@ -17,6 +18,7 @@ use function var_export; use const PREG_SPLIT_DELIM_CAPTURE; +use const PREG_SPLIT_NO_EMPTY; /** * @internal do not use this in your code: it is only here for internal use @@ -50,16 +52,20 @@ public function generate(): string return parent::generate(); } catch (RuntimeException $e) { if ($this->reflection) { - $value = rtrim(substr(explode('$' . $this->reflection->getName() . ' = ', (string) $this->reflection, 2)[1], 0, -2)); + $value = self::exportDefault($this->reflection); } else { $value = var_export($this->value, true); + + if (\PHP_VERSION_ID < 80200) { + return self::fixVarExport($value); + } } - return self::fixExport($value); + return $value; } } - private static function fixExport(string $value): string + private static function fixVarExport(string $value): string { $parts = preg_split('{(\'(?:[^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\')}', $value, -1, PREG_SPLIT_DELIM_CAPTURE); @@ -73,4 +79,47 @@ private static function fixExport(string $value): string return implode('', $parts); } + + private static function exportDefault(\ReflectionParameter $param): string + { + $default = rtrim(substr(explode('$'.$param->name.' = ', (string) $param, 2)[1] ?? '', 0, -2)); + + if (in_array($default, ['', 'NULL'], true)) { + return 'null'; + } + if (str_ends_with($default, "...'") && preg_match("/^'(?:[^'\\\\]*+(?:\\\\.)*+)*+'$/", $default)) { + return var_export($param->getDefaultValue(), true); + } + + $regexp = "/(\"(?:[^\"\\\\]*+(?:\\\\.)*+)*+\"|'(?:[^'\\\\]*+(?:\\\\.)*+)*+')/"; + $parts = preg_split($regexp, $default, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); + + $regexp = '/([\[\( ]|^)([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\\\\[a-zA-Z0-9_\x7f-\xff]++)*+)(?!: )/'; + $callback = (false !== strpbrk($default, "\\:('") && $class = $param->getDeclaringClass()) + ? function ($m) use ($class) { + switch ($m[2]) { + case 'new': case 'false': case 'true': case 'null': return $m[1].$m[2]; + case 'NULL': return $m[1].'null'; + case 'self': return $m[1].'\\'.$class->name; + case 'namespace\\parent': + case 'parent': $m[1].(($parent = $class->getParentClass()) ? '\\'.$parent->name : 'parent'); + default: return $m[1].'\\'.$m[2]; + } + } + : function ($m) { + switch ($m[2]) { + case 'new': case 'false': case 'true': case 'null': return $m[1].$m[2]; + case 'NULL': return $m[1].'null'; + default: return $m[1].'\\'.$m[2]; + } + }; + + return implode('', array_map(function ($part) use ($regexp, $callback) { + switch ($part[0]) { + case '"': return $part; // for internal classes only + case "'": return false !== strpbrk($part, "\\\0\r\n") ? '"'.substr(str_replace(['$', "\0", "\r", "\n"], ['\$', '\0', '\r', '\n'], $part), 1, -1).'"' : $part; + default: return preg_replace_callback($regexp, $callback, $part); + } + }, $parts)); + } } diff --git a/tests/language-feature-scripts/lazy-loading-ghost-allows-inexisting-magic-property-read.phpt b/tests/language-feature-scripts/lazy-loading-ghost-allows-inexisting-magic-property-read.phpt index e617ff63..af4f8973 100644 --- a/tests/language-feature-scripts/lazy-loading-ghost-allows-inexisting-magic-property-read.phpt +++ b/tests/language-feature-scripts/lazy-loading-ghost-allows-inexisting-magic-property-read.phpt @@ -5,6 +5,7 @@ Verifies that generated lazy loading ghost objects disallow reading non-existing require_once __DIR__ . '/init.php'; +#[AllowDynamicProperties] class Kitchen { private $sweets; @@ -22,4 +23,4 @@ $proxy = $factory->createProxy(Kitchen::class, function () {}); echo $proxy->nonExisting; ?> --EXPECTF-- -nonExisting \ No newline at end of file +nonExisting diff --git a/tests/language-feature-scripts/lazy-loading-ghost-allows-inexisting-property-write.phpt b/tests/language-feature-scripts/lazy-loading-ghost-allows-inexisting-property-write.phpt index 20366534..5ae588e1 100644 --- a/tests/language-feature-scripts/lazy-loading-ghost-allows-inexisting-property-write.phpt +++ b/tests/language-feature-scripts/lazy-loading-ghost-allows-inexisting-property-write.phpt @@ -5,6 +5,7 @@ Verifies that generated lazy loading ghost objects disallow reading non-existing require_once __DIR__ . '/init.php'; +#[AllowDynamicProperties] class Kitchen { private $sweets; @@ -18,4 +19,4 @@ $proxy->nonExisting = 'I do not exist'; echo $proxy->nonExisting; ?> --EXPECTF-- -I do not exist \ No newline at end of file +I do not exist diff --git a/tests/language-feature-scripts/lazy-loading-ghost-denies-inexisting-property-read.phpt b/tests/language-feature-scripts/lazy-loading-ghost-denies-inexisting-property-read.phpt index f7cc2edb..3c9a1cb1 100644 --- a/tests/language-feature-scripts/lazy-loading-ghost-denies-inexisting-property-read.phpt +++ b/tests/language-feature-scripts/lazy-loading-ghost-denies-inexisting-property-read.phpt @@ -5,6 +5,7 @@ Verifies that generated lazy loading ghost objects disallow reading non-existing require_once __DIR__ . '/init.php'; +#[AllowDynamicProperties] class Kitchen { private $sweets; @@ -17,4 +18,4 @@ $proxy = $factory->createProxy(Kitchen::class, function () {}); $proxy->nonExisting; ?> --EXPECTF-- -%SNotice: Undefined property: Kitchen::$nonExisting in %a \ No newline at end of file +%SNotice: Undefined property: Kitchen::$nonExisting in %a diff --git a/tests/language-feature-scripts/lazy-loading-ghost-skip-destructor.phpt b/tests/language-feature-scripts/lazy-loading-ghost-skip-destructor.phpt index dfebd931..023521fe 100644 --- a/tests/language-feature-scripts/lazy-loading-ghost-skip-destructor.phpt +++ b/tests/language-feature-scripts/lazy-loading-ghost-skip-destructor.phpt @@ -5,6 +5,7 @@ Verifies that generated lazy loading ghost objects can skip calling the proxied require_once __DIR__ . '/init.php'; +#[AllowDynamicProperties] class Destructable { public function __destruct() diff --git a/tests/language-feature-scripts/lazy-loading-value-holder-denies-interface-property-read.phpt b/tests/language-feature-scripts/lazy-loading-value-holder-denies-interface-property-read.phpt index 28e84dc4..ddfbeeff 100644 --- a/tests/language-feature-scripts/lazy-loading-value-holder-denies-interface-property-read.phpt +++ b/tests/language-feature-scripts/lazy-loading-value-holder-denies-interface-property-read.phpt @@ -14,7 +14,8 @@ $factory = new \ProxyManager\Factory\LazyLoadingValueHolderFactory($configuratio $proxy = $factory ->createProxy(MyInterface::class, function (& $wrapped, $proxy, $method, array $parameters, & $initializer) : bool { $initializer = null; - $wrapped = new class implements MyInterface { + $wrapped = new #[AllowDynamicProperties] + class implements MyInterface { }; return true; diff --git a/tests/language-feature-scripts/lazy-loading-value-holder-interface-proxy.phpt b/tests/language-feature-scripts/lazy-loading-value-holder-interface-proxy.phpt index da9ab0de..b4421d9b 100644 --- a/tests/language-feature-scripts/lazy-loading-value-holder-interface-proxy.phpt +++ b/tests/language-feature-scripts/lazy-loading-value-holder-interface-proxy.phpt @@ -10,6 +10,7 @@ interface MyInterface public function do(); } +#[AllowDynamicProperties] class MyClass implements MyInterface { public function do() diff --git a/tests/language-feature-scripts/lazy-loading-value-holder-skip-destructor.phpt b/tests/language-feature-scripts/lazy-loading-value-holder-skip-destructor.phpt index 66e5072c..b554d0a5 100644 --- a/tests/language-feature-scripts/lazy-loading-value-holder-skip-destructor.phpt +++ b/tests/language-feature-scripts/lazy-loading-value-holder-skip-destructor.phpt @@ -5,6 +5,7 @@ Verifies that generated lazy loading value holders can skip calling the proxied require_once __DIR__ . '/init.php'; +#[AllowDynamicProperties] class Destructable { public function __destruct()