55namespace Boson \Component \WeakType ;
66
77/**
8- * A wrapper class that maintains a weak reference to the bound object of a closure
8+ * A wrapper class that maintains a weak reference to the bound object
9+ * of a closure.
910 *
1011 * This class allows callbacks to be executed even if their originally bound
1112 * object has been garbage collected. When the object is alive, the callback
1213 * executes with the object as context. When the object is dead, it falls back
1314 * to executing the callback without object context (statically), but within
14- * the original class scope
15+ * the original class scope.
1516 *
1617 * ```
1718 * $closure = WeakClosure::create(function() {
2728final readonly class WeakClosure
2829{
2930 /**
30- * Weak reference to the bound object of the original closure
31+ * The class name of the bound object.
3132 *
32- * @var \WeakReference<TThis>
33+ * Used for static callback execution when the object is garbage collected.
34+ *
35+ * @var class-string<TThis>
3336 */
34- private \ WeakReference $ reference ;
37+ private string $ class ;
3538
3639 /**
37- * The class name of the bound object
38- *
39- * Used for static callback execution when the object is garbage collected
40+ * Weak reference to the bound object of the original closure.
4041 *
41- * @var class-string <TThis>
42+ * @var \WeakReference <TThis>
4243 */
43- private string $ class ;
44+ private \ WeakReference $ object ;
4445
4546 /**
46- * The original closure wrapped by this {@see WeakClosure} instance
47+ * The original closure wrapped by this {@see WeakClosure} instance.
4748 */
4849 private \Closure $ callback ;
4950
51+ /**
52+ * Indicates whether the closure is an internal PHP function.
53+ */
54+ private bool $ isInternal ;
55+
5056 /**
5157 * @param TThis $reference The object bound to the original closure
5258 *
5359 * @internal Use the {@see WeakClosure::create()} instead
5460 */
55- private function __construct (object $ reference , \Closure $ callback )
61+ private function __construct (
62+ object $ reference ,
63+ \Closure $ callback ,
64+ \ReflectionFunction $ reflection ,
65+ ) {
66+ $ this ->object = \WeakReference::create ($ reference );
67+ $ this ->callback = self ::unbind ($ this ->object , $ callback , $ reflection );
68+ $ this ->class = self ::getClass ($ reference , $ reflection );
69+ $ this ->isInternal = $ reflection ->isInternal ();
70+ }
71+
72+ /**
73+ * @param TThis $reference
74+ *
75+ * @return class-string
76+ */
77+ private static function getClass (object $ reference , \ReflectionFunction $ reflection ): string
5678 {
57- $ this ->reference = \WeakReference::create ($ reference );
58- $ this ->callback = $ callback ->bindTo ($ this );
59- $ this ->class = $ reference ::class;
79+ $ class = $ reflection ->getClosureScopeClass ();
80+
81+ if ($ class === null ) {
82+ return $ reference ::class;
83+ }
84+
85+ return $ class ->name ;
86+ }
87+
88+ /**
89+ * Unbind `$this` context from a passed closure and creates a new
90+ * closure that references the target object weakly.
91+ */
92+ private static function unbind (object $ target , \Closure $ callback , \ReflectionFunction $ reflection ): \Closure
93+ {
94+ if ($ reflection ->isAnonymous ()) {
95+ return $ callback ->bindTo ($ target );
96+ }
97+
98+ $ method = $ reflection ->getShortName ();
99+
100+ return function (mixed ...$ args ) use ($ method ): mixed {
101+ return $ this ->{$ method }(...$ args );
102+ };
60103 }
61104
62105 /**
63- * Creates a {@see WeakClosure} from a callable, or returns the callable
64- * unchanged if it is not bound to an object
106+ * Creates a weak closure from the passed.
107+ *
108+ * If the closure is not bound to an object, returns it unchanged.
109+ *
110+ * Otherwise, wraps it in a {@see WeakClosure} that maintains a weak
111+ * reference to the bound object
65112 *
66113 * @template TArgClosure of \Closure
67114 *
68115 * @param TArgClosure $callback The callable to potentially wrap
69116 *
70- * @return TArgClosure Returns a {@see \Closure} instance with weak
71- * reference to `$this`
117+ * @return TArgClosure|\Closure Returns a {@see \Closure}
118+ * instance with weak reference to `$this`
72119 *
73120 * @noinspection PhpDocMissingThrowsInspection An exception never throws
74121 */
75122 public static function create (\Closure $ callback ): \Closure
76123 {
77- $ reference = new \ReflectionFunction ($ callback )
78- ->getClosureThis ();
124+ $ reflection = new \ReflectionFunction ($ callback );
79125
80- if ($ reference === null ) {
126+ $ context = $ reflection ->getClosureThis ();
127+
128+ if ($ context === null ) {
81129 return $ callback ;
82130 }
83131
84- return (new self ($ reference , $ callback ))(...);
132+ return (new self ($ context , $ callback, $ reflection ))(...);
85133 }
86134
87135 /**
@@ -94,15 +142,27 @@ public static function create(\Closure $callback): \Closure
94142 * @param mixed ...$args Arguments to pass to the callback
95143 *
96144 * @return mixed The result of the callback execution
145+ * @throws \RuntimeException If the GC has freed the bound object
97146 */
98147 public function __invoke (mixed ...$ args ): mixed
99148 {
100- $ self = $ this ->reference ->get ();
149+ $ self = $ this ->object ->get ();
101150
102151 if ($ self === null ) {
103- $ callback = $ this ->callback ->bindTo (null , $ this ->class );
152+ $ shortClassName = $ this ->class ;
153+
154+ if (\is_int ($ shortClassNameOffset = \strpos ($ shortClassName , "\0" ))) {
155+ $ shortClassName = \substr ($ shortClassName , 0 , $ shortClassNameOffset );
156+ }
157+
158+ throw new \RuntimeException (\sprintf (
159+ 'Cannot call a closure, instance of %s has already been removed by the GC ' ,
160+ $ shortClassName ,
161+ ));
162+ }
104163
105- return $ callback (...$ args );
164+ if ($ this ->isInternal ) {
165+ return $ this ->callback ->bindTo ($ self )(...$ args );
106166 }
107167
108168 return $ this ->callback ->call ($ self , ...$ args );
0 commit comments