This commit is contained in:
Andrew Nicols
2026-03-15 20:54:28 +08:00
5 changed files with 322 additions and 54 deletions
@@ -21,11 +21,11 @@
"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"
},
"autoload": {
"psr-4": {
@@ -43,10 +43,13 @@
}
},
"config": {
"sort-packages": true,
"allow-plugins": {
"pestphp/pest-plugin": true
}
},
"audit": {
"block-insecure": false
},
"sort-packages": true
},
"minimum-stability": "dev",
"prefer-stable": true
@@ -1 +1,22 @@
See instructions in lib/php-di/readme_moodle.md
Description of Serializable Closure library import into Moodle.
Source: https://github.com/laravel/serializable-closure
This library is a dependency of php-di/php-di, used by Moodle's dependency
injection container (core\di).
To update:
- Download the desired release from https://github.com/laravel/serializable-closure/releases
- Delete the contents of the src/ directory completely and copy in the new one
- Copy composer.json, LICENSE.md and README.md from the zip root
- Edit public/lib/thirdpartylibs.xml and update the version
To test:
- vendor/bin/phpunit public/lib/tests/di_test.php
History:
- MDL-81671: Successfully upgraded 1.3.2 -> 1.3.3 (Moodle 4.5, Paul Holden)
- MDL-84180: Attempted 1.3.3 -> 2.0.x, closed Won't Fix -- blocked by php-di
only supporting ^1.0 at the time. Andrew Lyons subsequently fixed
php-di upstream (PHP-DI/PHP-DI PR #899), enabling 2.x support.
- MDL-86460: Upgraded 2.0.3 -> 2.0.10 (Moodle 5.2)
@@ -255,7 +255,7 @@ class Native implements Serializable
$instance = $data;
$reflection = new ReflectionObject($instance);
if (! $reflection->isUserDefined()) {
if (! $reflection->isUserDefined() || $reflection->hasMethod('__serialize')) {
$storage[$instance] = $data;
return;
@@ -269,12 +269,10 @@ class Native implements Serializable
}
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;
}
@@ -369,12 +367,10 @@ class Native implements Serializable
}
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;
}
@@ -477,7 +473,7 @@ class Native implements Serializable
$reflection = new ReflectionObject($data);
if (! $reflection->isUserDefined()) {
if (! $reflection->isUserDefined() || $reflection->hasMethod('__serialize')) {
$this->scope[$instance] = $data;
return;
@@ -491,12 +487,10 @@ class Native implements Serializable
}
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;
}
@@ -519,7 +513,7 @@ class Native implements Serializable
* @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();
}
@@ -125,6 +125,8 @@ class ReflectionClosure extends ReflectionFunction
$isUsingScope = false;
$isUsingThisObject = false;
$candidates = [];
for ($i = 0, $l = count($tokens); $i < $l; $i++) {
$token = $tokens[$i];
@@ -296,7 +298,14 @@ class ReflectionClosure extends ReflectionFunction
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'];
} elseif ($inside_structure) {
$inside_structure = ! ($open === $inside_structure_mark);
}
@@ -312,7 +321,15 @@ class ReflectionClosure extends ReflectionFunction
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'];
continue 3;
}
$open--;
}
@@ -321,7 +338,15 @@ class ReflectionClosure extends ReflectionFunction
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'];
continue 3;
}
$code .= $token[0];
break;
@@ -670,22 +695,12 @@ class ReflectionClosure extends ReflectionFunction
}
}
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;
$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);
@@ -697,6 +712,47 @@ class ReflectionClosure extends ReflectionFunction
return "#[$name($arguments)]";
}, $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]));
}
@@ -727,6 +783,10 @@ class ReflectionClosure extends ReflectionFunction
return $this->useVariables;
}
if ($this->isShortClosure()) {
return $this->useVariables;
}
$tokens = $this->getTokens();
$use = [];
$state = 'start';
@@ -865,13 +925,18 @@ class ReflectionClosure extends ReflectionFunction
*/
protected function getClasses()
{
$key = $this->getHashedFileName();
$line = $this->getStartLine();
if (! isset(static::$classes[$key])) {
$this->fetchItems();
foreach ($this->getStructures() as $struct) {
if ($struct['type'] === 'namespace' &&
$struct['start'] <= $line &&
$struct['end'] >= $line
) {
return $struct['classes'];
}
}
return static::$classes[$key];
return [];
}
/**
@@ -945,14 +1010,36 @@ class ReflectionClosure extends ReflectionFunction
$alias = '';
$isFunc = $isConst = false;
$startLine = $endLine = 0;
$startLine = $lastKnownLine = 0;
$structType = $structName = '';
$structIgnore = false;
$namespace = '';
$namespaceStartLine = 0;
$namespaceBraced = false;
$namespaceClasses = [];
foreach ($tokens as $token) {
if (is_array($token)) {
$lastKnownLine = $token[2];
}
switch ($state) {
case 'start':
switch ($token[0]) {
case T_NAMESPACE:
$structures[] = [
'type' => 'namespace',
'name' => $namespace,
'start' => $namespaceStartLine,
'end' => $token[2] - 1,
'classes' => $namespaceClasses,
];
$namespace = '';
$namespaceClasses = [];
$state = 'namespace';
$namespaceStartLine = $token[2];
break;
case T_CLASS:
case T_INTERFACE:
case T_TRAIT:
@@ -978,6 +1065,33 @@ class ReflectionClosure extends ReflectionFunction
case T_DOUBLE_COLON:
$state = 'invoke';
break;
case '}':
if ($namespaceBraced) {
$structures[] = [
'type' => 'namespace',
'name' => $namespace,
'start' => $namespaceStartLine,
'end' => $lastKnownLine,
'classes' => $namespaceClasses,
];
$namespaceBraced = false;
$namespace = '';
$namespaceClasses = [];
}
break;
}
break;
case 'namespace':
switch ($token[0]) {
case T_STRING:
case T_NAME_QUALIFIED:
$namespace = $token[1];
break;
case ';':
case '{':
$state = 'start';
$namespaceBraced = $token[0] === '{';
break;
}
break;
case 'use':
@@ -1022,6 +1136,7 @@ class ReflectionClosure extends ReflectionFunction
$constants[$alias] = $name;
} else {
$classes[strtolower($alias)] = $name;
$namespaceClasses[strtolower($alias)] = $name;
}
}
$name = $alias = '';
@@ -1061,6 +1176,7 @@ class ReflectionClosure extends ReflectionFunction
$constants[$alias] = $prefix.$name;
} else {
$classes[strtolower($alias)] = $prefix.$name;
$namespaceClasses[strtolower($alias)] = $prefix.$name;
}
}
$name = $alias = '';
@@ -1118,22 +1234,26 @@ class ReflectionClosure extends ReflectionFunction
'type' => $structType,
'name' => $structName,
'start' => $startLine,
'end' => $endLine,
'end' => $lastKnownLine,
];
}
$structIgnore = false;
$state = 'start';
}
break;
default:
if (is_array($token)) {
$endLine = $token[2];
}
}
break;
}
}
$structures[] = [
'type' => 'namespace',
'name' => $namespace,
'start' => $namespaceStartLine,
'end' => PHP_INT_MAX,
'classes' => $namespaceClasses,
];
static::$classes[$key] = $classes;
static::$functions[$key] = $functions;
static::$constants[$key] = $constants;
@@ -1147,20 +1267,19 @@ class ReflectionClosure extends ReflectionFunction
*/
protected function getClosureNamespaceName()
{
$ns = $this->getNamespaceName();
$startLine = $this->getStartLine();
$endLine = $this->getEndLine();
$name = $this->getName();
// First class callables...
if ($name !== '{closure}'
&& ! str_contains($name, '{closure:/')
&& ! str_contains($name, '{closure:\\')
&& empty($ns)
&& ! is_null($this->getClosureScopeClass())) {
$ns = $this->getClosureScopeClass()->getNamespaceName();
foreach ($this->getStructures() as $struct) {
if ($struct['type'] === 'namespace' &&
$struct['start'] <= $startLine &&
$struct['end'] >= $endLine
) {
return $struct['name'];
}
}
return $ns;
return '';
}
/**
@@ -1181,4 +1300,135 @@ class ReflectionClosure extends ReflectionFunction
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,
];
}
/**
* 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('<?php '.$code);
$params = [];
$vars = [];
$state = 'start';
foreach ($tokens as $token) {
if (! is_array($token)) {
if ($token === '(' && $state === 'start') {
$state = 'params';
} elseif ($token === ')' && $state === 'params') {
$state = 'body';
}
continue;
}
if ($token[0] === T_VARIABLE) {
$name = substr($token[1], 1);
if ($state === 'params') {
$params[] = $name;
} elseif ($state === 'body' && $name !== 'this') {
$vars[$name] = true;
}
}
}
// Verify parameter count
if (parent::getNumberOfParameters() !== count($params)) {
return false;
}
// Verify use/captured variables
if ($isShortClosure) {
$actualVars = array_keys(parent::getStaticVariables());
$foundCaptures = array_diff(array_keys($vars), $params);
if (count($foundCaptures) !== count($actualVars)) {
return false;
}
if (count(array_diff($foundCaptures, $actualVars)) > 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;
}
}
+1 -1
View File
@@ -791,7 +791,7 @@ All rights reserved.</copyright>
<library>
<location>laravel/serializable-closure</location>
<name>Serializable Closure</name>
<version>2.0.3</version>
<version>2.0.10</version>
<license>MIT</license>
<repository>https://github.com/laravel/serializable-closure</repository>
</library>