diff --git a/composer.json b/composer.json index d92c5fde9..c4bfbdb6a 100644 --- a/composer.json +++ b/composer.json @@ -37,7 +37,7 @@ "icewind/streams": "^0.7.8", "justinrainbow/json-schema": "^6.10", "kornrunner/blurhash": "^1.2", - "laravel/serializable-closure": "^2.0.4", + "laravel/serializable-closure": "^2.0.15", "mexitek/phpcolors": "^1.0", "microsoft/azure-storage-blob": "^1.5.4", "mlocati/ip-lib": "^1.22", diff --git a/composer.lock b/composer.lock index c382652df..4f4f33e78 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "eba98ef52bbf1ab1f9b60b4e76d92abc", + "content-hash": "60ac218433a3dcf5a4a46a452bba3674", "packages": [ { "name": "aws/aws-crt-php", @@ -1778,27 +1778,27 @@ }, { "name": "laravel/serializable-closure", - "version": "v2.0.4", + "version": "v2.0.15", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "b352cf0534aa1ae6b4d825d1e762e35d43f8a841" + "reference": "dccd8bcb851bb03fcc005df650b708b57cc52661" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/b352cf0534aa1ae6b4d825d1e762e35d43f8a841", - "reference": "b352cf0534aa1ae6b4d825d1e762e35d43f8a841", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/dccd8bcb851bb03fcc005df650b708b57cc52661", + "reference": "dccd8bcb851bb03fcc005df650b708b57cc52661", "shasum": "" }, "require": { "php": "^8.1" }, "require-dev": { - "illuminate/support": "^10.0|^11.0|^12.0", + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", "nesbot/carbon": "^2.67|^3.0", - "pestphp/pest": "^2.36|^3.0", + "pestphp/pest": "^2.36|^3.0|^4.0", "phpstan/phpstan": "^2.0", - "symfony/var-dumper": "^6.2.0|^7.0.0" + "symfony/var-dumper": "^6.2.0|^7.0.0|^8.0.0" }, "type": "library", "extra": { @@ -1835,7 +1835,7 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2025-03-19T13:51:03+00:00" + "time": "2026-07-21T16:49:22+00:00" }, { "name": "lcobucci/clock", diff --git a/composer/autoload_classmap.php b/composer/autoload_classmap.php index fbf7ba482..e5b61b1f4 100644 --- a/composer/autoload_classmap.php +++ b/composer/autoload_classmap.php @@ -1358,7 +1358,6 @@ 'Laravel\\SerializableClosure\\Contracts\\Signer' => $vendorDir . '/laravel/serializable-closure/src/Contracts/Signer.php', 'Laravel\\SerializableClosure\\Exceptions\\InvalidSignatureException' => $vendorDir . '/laravel/serializable-closure/src/Exceptions/InvalidSignatureException.php', 'Laravel\\SerializableClosure\\Exceptions\\MissingSecretKeyException' => $vendorDir . '/laravel/serializable-closure/src/Exceptions/MissingSecretKeyException.php', - 'Laravel\\SerializableClosure\\Exceptions\\PhpVersionNotSupportedException' => $vendorDir . '/laravel/serializable-closure/src/Exceptions/PhpVersionNotSupportedException.php', 'Laravel\\SerializableClosure\\SerializableClosure' => $vendorDir . '/laravel/serializable-closure/src/SerializableClosure.php', 'Laravel\\SerializableClosure\\Serializers\\Native' => $vendorDir . '/laravel/serializable-closure/src/Serializers/Native.php', 'Laravel\\SerializableClosure\\Serializers\\Signed' => $vendorDir . '/laravel/serializable-closure/src/Serializers/Signed.php', diff --git a/composer/autoload_static.php b/composer/autoload_static.php index 2f2c871c6..91f87b82b 100644 --- a/composer/autoload_static.php +++ b/composer/autoload_static.php @@ -1913,7 +1913,6 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'Laravel\\SerializableClosure\\Contracts\\Signer' => __DIR__ . '/..' . '/laravel/serializable-closure/src/Contracts/Signer.php', 'Laravel\\SerializableClosure\\Exceptions\\InvalidSignatureException' => __DIR__ . '/..' . '/laravel/serializable-closure/src/Exceptions/InvalidSignatureException.php', 'Laravel\\SerializableClosure\\Exceptions\\MissingSecretKeyException' => __DIR__ . '/..' . '/laravel/serializable-closure/src/Exceptions/MissingSecretKeyException.php', - 'Laravel\\SerializableClosure\\Exceptions\\PhpVersionNotSupportedException' => __DIR__ . '/..' . '/laravel/serializable-closure/src/Exceptions/PhpVersionNotSupportedException.php', 'Laravel\\SerializableClosure\\SerializableClosure' => __DIR__ . '/..' . '/laravel/serializable-closure/src/SerializableClosure.php', 'Laravel\\SerializableClosure\\Serializers\\Native' => __DIR__ . '/..' . '/laravel/serializable-closure/src/Serializers/Native.php', 'Laravel\\SerializableClosure\\Serializers\\Signed' => __DIR__ . '/..' . '/laravel/serializable-closure/src/Serializers/Signed.php', diff --git a/composer/installed.json b/composer/installed.json index 4ffd9f084..362c1a10e 100644 --- a/composer/installed.json +++ b/composer/installed.json @@ -1847,30 +1847,30 @@ }, { "name": "laravel/serializable-closure", - "version": "v2.0.4", - "version_normalized": "2.0.4.0", + "version": "v2.0.15", + "version_normalized": "2.0.15.0", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "b352cf0534aa1ae6b4d825d1e762e35d43f8a841" + "reference": "dccd8bcb851bb03fcc005df650b708b57cc52661" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/b352cf0534aa1ae6b4d825d1e762e35d43f8a841", - "reference": "b352cf0534aa1ae6b4d825d1e762e35d43f8a841", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/dccd8bcb851bb03fcc005df650b708b57cc52661", + "reference": "dccd8bcb851bb03fcc005df650b708b57cc52661", "shasum": "" }, "require": { "php": "^8.1" }, "require-dev": { - "illuminate/support": "^10.0|^11.0|^12.0", + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", "nesbot/carbon": "^2.67|^3.0", - "pestphp/pest": "^2.36|^3.0", + "pestphp/pest": "^2.36|^3.0|^4.0", "phpstan/phpstan": "^2.0", - "symfony/var-dumper": "^6.2.0|^7.0.0" + "symfony/var-dumper": "^6.2.0|^7.0.0|^8.0.0" }, - "time": "2025-03-19T13:51:03+00:00", + "time": "2026-07-21T16:49:22+00:00", "type": "library", "extra": { "branch-alias": { diff --git a/composer/installed.php b/composer/installed.php index d7cce4ca9..6d9d1f222 100644 --- a/composer/installed.php +++ b/composer/installed.php @@ -236,9 +236,9 @@ 'dev_requirement' => false, ), 'laravel/serializable-closure' => array( - 'pretty_version' => 'v2.0.4', - 'version' => '2.0.4.0', - 'reference' => 'b352cf0534aa1ae6b4d825d1e762e35d43f8a841', + 'pretty_version' => 'v2.0.15', + 'version' => '2.0.15.0', + 'reference' => 'dccd8bcb851bb03fcc005df650b708b57cc52661', 'type' => 'library', 'install_path' => __DIR__ . '/../laravel/serializable-closure', 'aliases' => array(), diff --git a/laravel/serializable-closure/src/Exceptions/PhpVersionNotSupportedException.php b/laravel/serializable-closure/src/Exceptions/PhpVersionNotSupportedException.php deleted file mode 100644 index b664e00aa..000000000 --- a/laravel/serializable-closure/src/Exceptions/PhpVersionNotSupportedException.php +++ /dev/null @@ -1,19 +0,0 @@ -getClosureScopeClass()) { - $scope = $scope->name; + if (! $scope->isAnonymous() || $reflector->isBindingRequired() || $reflector->isScopeRequired()) { + $scope = $scope->name; + } else { + $scope = null; + } } $this->reference = spl_object_hash($this->closure); @@ -197,7 +201,13 @@ public function __unserialize($data) if (! empty($this->code['objects'])) { foreach ($this->code['objects'] as $item) { - $item['property']->setValue($item['instance'], $item['object']->getClosure()); + static::setPropertyValue( + $item['property'], + $item['instance'], + $item['object'] instanceof SerializableClosure || $item['object'] instanceof UnsignedSerializableClosure + ? $item['object'] + : $item['object']->getClosure() + ); } } @@ -269,23 +279,27 @@ public static function wrapClosures(&$data, $storage) } foreach ($reflection->getProperties() as $property) { - if ($property->isStatic() || ! $property->getDeclaringClass()->isUserDefined()) { + if ($property->isStatic() || ! $property->getDeclaringClass()->isUserDefined() || static::isVirtualProperty($property)) { continue; } - $property->setAccessible(true); - if (! $property->isInitialized($instance)) { continue; } - $value = $property->getValue($instance); + $value = static::getPropertyValue($property, $instance); + + if (static::isClosureTypedProperty($property)) { + static::setPropertyValue($property, $data, $value); + + continue; + } if (is_array($value) || is_object($value)) { static::wrapClosures($value, $storage); } - $property->setValue($data, $value); + static::setPropertyValue($property, $data, $value); } } while ($reflection = $reflection->getParentClass()); } @@ -314,6 +328,10 @@ public function getReflector() */ protected function mapPointers(&$data) { + if ($data instanceof SerializableClosure || $data instanceof UnsignedSerializableClosure) { + return; + } + $scope = $this->scope; if ($data instanceof static) { @@ -349,6 +367,8 @@ protected function mapPointers(&$data) foreach ($data as $key => &$value) { if ($value instanceof SelfReference && $value->hash === $this->code['self']) { $data->{$key} = &$this->closure; + } elseif ($value instanceof static) { + $data->{$key} = &$value->closure; } elseif (is_array($value) || is_object($value)) { $this->mapPointers($value); } @@ -369,17 +389,15 @@ protected function mapPointers(&$data) } foreach ($reflection->getProperties() as $property) { - if ($property->isStatic() || ! $property->getDeclaringClass()->isUserDefined()) { + if ($property->isStatic() || ! $property->getDeclaringClass()->isUserDefined() || static::isVirtualProperty($property)) { continue; } - $property->setAccessible(true); - if (! $property->isInitialized($data) || $property->isReadOnly()) { continue; } - $item = $property->getValue($data); + $item = static::getPropertyValue($property, $data); if ($item instanceof SerializableClosure || $item instanceof UnsignedSerializableClosure || ($item instanceof SelfReference && $item->hash === $this->code['self'])) { $this->code['objects'][] = [ @@ -387,9 +405,11 @@ protected function mapPointers(&$data) 'property' => $property, 'object' => $item instanceof SelfReference ? $this : $item, ]; + } elseif ($item instanceof static) { + static::setPropertyValue($property, $data, $item->closure); } elseif (is_array($item) || is_object($item)) { $this->mapPointers($item); - $property->setValue($data, $item); + static::setPropertyValue($property, $data, $item); } } } while ($reflection = $reflection->getParentClass()); @@ -491,36 +511,94 @@ protected function mapByReference(&$data) } foreach ($reflection->getProperties() as $property) { - if ($property->isStatic() || ! $property->getDeclaringClass()->isUserDefined() || $this->isVirtualProperty($property)) { + if ($property->isStatic() || ! $property->getDeclaringClass()->isUserDefined() || static::isVirtualProperty($property)) { continue; } - $property->setAccessible(true); - if (! $property->isInitialized($instance) || ($property->isReadOnly() && $property->class !== $reflection->name)) { continue; } - $value = $property->getValue($instance); + $value = static::getPropertyValue($property, $instance); + + if (static::isClosureTypedProperty($property)) { + static::setPropertyValue($property, $data, $value); + + continue; + } if (is_array($value) || is_object($value)) { $this->mapByReference($value); } - $property->setValue($data, $value); + static::setPropertyValue($property, $data, $value); } } while ($reflection = $reflection->getParentClass()); } } + /** + * Get the value of a property, bypassing hooks on PHP 8.4+. + * + * @param \ReflectionProperty $property + * @param object $object + * @return mixed + */ + protected static function getPropertyValue(ReflectionProperty $property, object $object): mixed + { + return PHP_VERSION_ID >= 80400 + ? $property->getRawValue($object) + : $property->getValue($object); + } + + /** + * Set the value of a property, bypassing hooks on PHP 8.4+. + * + * @param \ReflectionProperty $property + * @param object $object + * @param mixed $value + * @return void + */ + protected static function setPropertyValue(ReflectionProperty $property, object $object, mixed $value): void + { + PHP_VERSION_ID >= 80400 + ? $property->setRawValue($object, $value) + : $property->setValue($object, $value); + } + /** * Determine is virtual property. * * @param \ReflectionProperty $property * @return bool */ - protected function isVirtualProperty(ReflectionProperty $property): bool + protected static function isVirtualProperty(ReflectionProperty $property): bool { return method_exists($property, 'isVirtual') && $property->isVirtual(); } + + /** + * Determine if property is typed as Closure. + * + * @param \ReflectionProperty $property + * @return bool + */ + protected static function isClosureTypedProperty(ReflectionProperty $property): bool + { + $type = $property->getType(); + + if ($type instanceof \ReflectionNamedType) { + return $type->getName() === 'Closure'; + } + + if ($type instanceof \ReflectionUnionType || $type instanceof \ReflectionIntersectionType) { + foreach ($type->getTypes() as $t) { + if ($t instanceof \ReflectionNamedType && $t->getName() === 'Closure') { + return true; + } + } + } + + return false; + } } diff --git a/laravel/serializable-closure/src/Support/ReflectionClosure.php b/laravel/serializable-closure/src/Support/ReflectionClosure.php index fb90bef08..d5d5d70d8 100644 --- a/laravel/serializable-closure/src/Support/ReflectionClosure.php +++ b/laravel/serializable-closure/src/Support/ReflectionClosure.php @@ -2,11 +2,6 @@ namespace Laravel\SerializableClosure\Support; -defined('T_NAME_QUALIFIED') || define('T_NAME_QUALIFIED', -4); -defined('T_NAME_FULLY_QUALIFIED') || define('T_NAME_FULLY_QUALIFIED', -5); -defined('T_FN') || define('T_FN', -6); -defined('T_NULLSAFE_OBJECT_OPERATOR') || define('T_NULLSAFE_OBJECT_OPERATOR', -7); - use Closure; use ReflectionFunction; @@ -125,6 +120,11 @@ public function getCode() $isUsingScope = false; $isUsingThisObject = false; + $closureArgsInnerFuncCount = 0; + $closureArgsBraceDepth = 0; + + $candidates = []; + for ($i = 0, $l = count($tokens); $i < $l; $i++) { $token = $tokens[$i]; @@ -188,7 +188,41 @@ public function getCode() } break; case 'closure_args': + $insideClosureArgsInner = $closureArgsBraceDepth > 0 || $closureArgsInnerFuncCount > 0; + + if ($insideClosureArgsInner) { + switch ($token[0]) { + case T_FUNCTION: + $closureArgsInnerFuncCount++; + $code .= $token[1]; + break; + case T_CURLY_OPEN: + case T_DOLLAR_OPEN_CURLY_BRACES: + $closureArgsBraceDepth++; + $code .= $token[1]; + break; + case '{': + if ($closureArgsInnerFuncCount > 0) { + $closureArgsInnerFuncCount--; + } + $closureArgsBraceDepth++; + $code .= '{'; + break; + case '}': + $closureArgsBraceDepth--; + $code .= '}'; + break; + default: + $code .= is_array($token) ? $token[1] : $token; + } + break; + } + switch ($token[0]) { + case T_FUNCTION: + $closureArgsInnerFuncCount++; + $code .= $token[1]; + break; case T_NAME_QUALIFIED: [$id_start, $id_start_ci, $id_name] = $this->parseNameQualified($token[1]); $context = 'args'; @@ -296,7 +330,16 @@ public function getCode() case '}': $code .= '}'; if (--$open === 0 && ! $isShortClosure) { - break 3; + $reset = $this->collectCandidate($candidates, $code, $use, $isShortClosure, $isUsingThisObject, $isUsingScope); + $code = $reset['code']; + $state = $reset['state']; + $open = $reset['open']; + $use = $reset['use']; + $isShortClosure = $reset['isShortClosure']; + $isUsingThisObject = $reset['isUsingThisObject']; + $isUsingScope = $reset['isUsingScope']; + $closureArgsInnerFuncCount = $reset['closureArgsInnerFuncCount']; + $closureArgsBraceDepth = $reset['closureArgsBraceDepth']; } elseif ($inside_structure) { $inside_structure = ! ($open === $inside_structure_mark); } @@ -312,7 +355,17 @@ public function getCode() case ']': if ($isShortClosure) { if ($open === 0) { - break 3; + $reset = $this->collectCandidate($candidates, $code, $use, $isShortClosure, $isUsingThisObject, $isUsingScope); + $code = $reset['code']; + $state = $reset['state']; + $open = $reset['open']; + $use = $reset['use']; + $isShortClosure = $reset['isShortClosure']; + $isUsingThisObject = $reset['isUsingThisObject']; + $isUsingScope = $reset['isUsingScope']; + $closureArgsInnerFuncCount = $reset['closureArgsInnerFuncCount']; + $closureArgsBraceDepth = $reset['closureArgsBraceDepth']; + continue 3; } $open--; } @@ -321,7 +374,17 @@ public function getCode() case ',': case ';': if ($isShortClosure && $open === 0) { - break 3; + $reset = $this->collectCandidate($candidates, $code, $use, $isShortClosure, $isUsingThisObject, $isUsingScope); + $code = $reset['code']; + $state = $reset['state']; + $open = $reset['open']; + $use = $reset['use']; + $isShortClosure = $reset['isShortClosure']; + $isUsingThisObject = $reset['isUsingThisObject']; + $isUsingScope = $reset['isUsingScope']; + $closureArgsInnerFuncCount = $reset['closureArgsInnerFuncCount']; + $closureArgsBraceDepth = $reset['closureArgsBraceDepth']; + continue 3; } $code .= $token[0]; break; @@ -501,6 +564,16 @@ public function getCode() $code .= $token[1]; $state = 'anonymous'; break; + case '(': + if ($context === 'instanceof') { + $code .= '('; + if ($isShortClosure) { + $open++; + } + $state = $lastState; + break; + } + // no break default: $i--; //reprocess last $state = 'id_name'; @@ -571,7 +644,7 @@ public function getCode() if (! $inside_structure) { $isUsingScope = $token[0] === T_DOUBLE_COLON; } - } elseif (! (\PHP_MAJOR_VERSION >= 7 && in_array($id_start_ci, $builtin_types))) { + } elseif (! in_array($id_start_ci, $builtin_types)) { if ($classes === null) { $classes = $this->getClasses(); } @@ -618,10 +691,10 @@ public function getCode() $context === 'root' ) { if (in_array($id_start_ci, $class_keywords)) { - if (! $inside_structure && ! $id_start_ci === 'static') { + if (! $inside_structure && $id_start_ci !== 'static') { $isUsingScope = true; } - } elseif (! (\PHP_MAJOR_VERSION >= 7 && in_array($id_start_ci, $builtin_types))) { + } elseif (! in_array($id_start_ci, $builtin_types)) { if ($classes === null) { $classes = $this->getClasses(); } @@ -670,22 +743,29 @@ public function getCode() } } - if ($isShortClosure) { - $this->useVariables = $this->getStaticVariables(); - } else { - $this->useVariables = empty($use) ? $use : array_intersect_key($this->getStaticVariables(), array_flip($use)); - } + $attributesCode = array_values(array_filter(array_map(function ($attribute) { + $name = $attribute->getName(); - $this->isShortClosure = $isShortClosure; - $this->isBindingRequired = $isUsingThisObject; - $this->isScopeRequired = $isUsingScope; + // Skip attributes that cannot target functions. When a closure is + // created from a method (e.g. `$obj->method(...)`), the method's + // attributes are inherited. Attributes that only target methods + // (like #[\Override]) would cause a fatal error when applied to + // the serialized closure function. + if (class_exists($name)) { + $ref = new \ReflectionClass($name); + $attrAttributes = $ref->getAttributes(\Attribute::class); + + if (! empty($attrAttributes)) { + $flags = $attrAttributes[0]->getArguments()[0] ?? \Attribute::TARGET_ALL; + if (($flags & \Attribute::TARGET_FUNCTION) === 0) { + return null; + } + } + } - $attributesCode = array_map(function ($attribute) { $arguments = $attribute->getArguments(); - - $name = $attribute->getName(); $arguments = implode(', ', array_map(function ($argument, $key) { - $argument = sprintf("'%s'", str_replace("'", "\\'", $argument)); + $argument = var_export($argument, true); if (is_string($key)) { $argument = sprintf('%s: %s', $key, $argument); @@ -695,7 +775,48 @@ public function getCode() }, $arguments, array_keys($arguments))); return "#[$name($arguments)]"; - }, $this->getAttributes()); + }, $this->getAttributes()))); + + if (count($candidates) > 1) { + $lastItem = array_pop($candidates); + + foreach ($candidates as $candidate) { + if (! $this->verifyCandidateSignature($candidate)) { + continue; + } + + $this->applyCandidate($candidate); + + $code = $candidate['code']; + + if (! empty($attributesCode)) { + $code = implode("\n", array_merge($attributesCode, [$code])); + } + + $this->code = $code; + + return $this->code; + } + + $candidates[] = $lastItem; + } + + $lastItem = array_pop($candidates); + + if ($lastItem) { + $this->applyCandidate($lastItem); + $code = $lastItem['code']; + } else { + if ($isShortClosure) { + $this->useVariables = $this->getStaticVariables(); + } else { + $this->useVariables = empty($use) ? $use : array_intersect_key($this->getStaticVariables(), array_flip($use)); + } + + $this->isShortClosure = $isShortClosure; + $this->isBindingRequired = $isUsingThisObject; + $this->isScopeRequired = $isUsingScope; + } if (! empty($attributesCode)) { $code = implode("\n", array_merge($attributesCode, [$code])); @@ -713,7 +834,7 @@ public function getCode() */ protected static function getBuiltinTypes() { - return ['array', 'callable', 'string', 'int', 'bool', 'float', 'iterable', 'void', 'object', 'mixed', 'false', 'null', 'never']; + return ['array', 'callable', 'string', 'int', 'bool', 'float', 'iterable', 'void', 'object', 'mixed', 'false', 'null', 'never', 'true']; } /** @@ -727,6 +848,10 @@ public function getUseVariables() return $this->useVariables; } + if ($this->isShortClosure()) { + return $this->useVariables; + } + $tokens = $this->getTokens(); $use = []; $state = 'start'; @@ -1240,4 +1365,137 @@ protected function parseNameQualified($token) return [$id_start, $id_start_ci, $id_name]; } + + /** + * Collect a closure candidate and reset state for finding the next one. + * + * @param array $candidates + * @param string $code + * @param array $use + * @param bool $isShortClosure + * @param bool $isUsingThisObject + * @param bool $isUsingScope + * @return array + */ + protected function collectCandidate(&$candidates, $code, $use, $isShortClosure, $isUsingThisObject, $isUsingScope) + { + $candidates[] = [ + 'code' => $code, + 'use' => $use, + 'isShortClosure' => $isShortClosure, + 'isUsingThisObject' => $isUsingThisObject, + 'isUsingScope' => $isUsingScope, + ]; + + return [ + 'code' => '', + 'state' => 'start', + 'open' => 0, + 'use' => [], + 'isShortClosure' => false, + 'isUsingThisObject' => false, + 'isUsingScope' => false, + 'closureArgsInnerFuncCount' => 0, + 'closureArgsBraceDepth' => 0, + ]; + } + + /** + * Apply a candidate's properties to this instance. + * + * @param array $candidate + * @return void + */ + protected function applyCandidate($candidate) + { + if ($candidate['isShortClosure']) { + $this->useVariables = $this->getStaticVariables(); + } else { + $this->useVariables = empty($candidate['use']) + ? $candidate['use'] + : array_intersect_key($this->getStaticVariables(), array_flip($candidate['use'])); + } + + $this->isShortClosure = $candidate['isShortClosure']; + $this->isBindingRequired = $candidate['isUsingThisObject']; + $this->isScopeRequired = $candidate['isUsingScope']; + } + + /** + * Verify that a candidate matches the closure's signature. + * + * @param array $candidate + * @return bool + */ + protected function verifyCandidateSignature($candidate) + { + $code = $candidate['code']; + $use = $candidate['use']; + $isShortClosure = $candidate['isShortClosure']; + + // Check if code starts with 'static' (more precise than searching anywhere in code) + $isStaticCode = strtolower(substr(ltrim($code), 0, 6)) === 'static'; + if (parent::isStatic() !== $isStaticCode) { + return false; + } + + // Parse the candidate to extract parameters and variables + $tokens = token_get_all(' 0) { + return false; + } + } else { + $actualStaticVariables = array_keys(parent::getStaticVariables()); + + if (! empty($use) && count(array_diff($use, $actualStaticVariables)) > 0) { + return false; + } + + if (count($use) !== count(parent::getStaticVariables())) { + return false; + } + } + + return true; + } }