Skip to content

Commit 67af0ea

Browse files
committed
Adds support for first class callable syntax
1 parent b88a441 commit 67af0ea

File tree

3 files changed

+248
-5
lines changed

3 files changed

+248
-5
lines changed

src/Support/ReflectionClosure.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,9 @@ public function getCode()
112112
$tokens = $this->getTokens();
113113
$state = $lastState = 'start';
114114
$inside_structure = false;
115+
$isFirstClassCallable = false;
115116
$isShortClosure = false;
117+
116118
$inside_structure_mark = 0;
117119
$open = 0;
118120
$code = '';
@@ -130,11 +132,15 @@ public function getCode()
130132
case 'start':
131133
if ($token[0] === T_FUNCTION || $token[0] === T_STATIC) {
132134
$code .= $token[1];
135+
133136
$state = $token[0] === T_FUNCTION ? 'function' : 'static';
134137
} elseif ($token[0] === T_FN) {
135138
$isShortClosure = true;
136139
$code .= $token[1];
137140
$state = 'closure_args';
141+
} elseif ($token[0] === T_PUBLIC || $token[0] === T_PROTECTED || $token[0] === T_PRIVATE) {
142+
$code = '';
143+
$isFirstClassCallable = true;
138144
}
139145
break;
140146
case 'static':
@@ -155,6 +161,11 @@ public function getCode()
155161
case 'function':
156162
switch ($token[0]) {
157163
case T_STRING:
164+
if ($isFirstClassCallable) {
165+
$state = 'closure_args';
166+
break;
167+
}
168+
158169
$code = '';
159170
$state = 'named_function';
160171
break;

tests/ReflectionClosurePhp81Test.php

Lines changed: 129 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,21 +107,21 @@ enum ScopedBackedEnum: string {
107107

108108
test('readonly properties', function () {
109109
$f = function () {
110-
$controller = new SerializerPhp81Controller();
110+
$controller = new ReflectionClosurePhp81Controller();
111111

112112
$controller->service = 'foo';
113113
};
114114

115115
$e = 'function () {
116-
$controller = new \SerializerPhp81Controller();
116+
$controller = new \ReflectionClosurePhp81Controller();
117117
118118
$controller->service = \'foo\';
119119
}';
120120

121121
expect($f)->toBeCode($e);
122122
});
123123

124-
test('first-class callable', function () {
124+
test('first-class callable with closures', function () {
125125
$f = function ($a) {
126126
$f = fn ($b) => $a + $b + 1;
127127

@@ -137,6 +137,77 @@ enum ScopedBackedEnum: string {
137137
expect($f)->toBeCode($e);
138138
});
139139

140+
test('first-class callable with methods', function () {
141+
$f = (new ReflectionClosurePhp81Controller())->publicGetter(...);
142+
143+
$e = 'function ()
144+
{
145+
return $this->privateGetter();
146+
}';
147+
148+
expect($f)->toBeCode($e);
149+
150+
$f = (new ReflectionClosurePhp81Controller())->publicGetterResolver(...);
151+
152+
$e = 'function ()
153+
{
154+
return $this->privateGetterResolver(...);
155+
}';
156+
157+
expect($f)->toBeCode($e);
158+
});
159+
160+
test('first-class callable with static methods', function () {
161+
$f = ReflectionClosurePhp81Controller::publicStaticGetter(...);
162+
163+
$e = 'static function ()
164+
{
165+
return static::privateStaticGetter();
166+
}';
167+
168+
expect($f)->toBeCode($e);
169+
170+
$f = ReflectionClosurePhp81Controller::publicStaticGetterResolver(...);
171+
172+
$e = 'static function ()
173+
{
174+
return static::privateStaticGetterResolver(...);
175+
}';
176+
177+
expect($f)->toBeCode($e);
178+
});
179+
180+
test('first-class callable final method', function () {
181+
$f = (new ReflectionClosurePhp81Controller())->finalPublicGetterResolver(...);
182+
183+
$e = 'function ()
184+
{
185+
return $this->privateGetterResolver(...);
186+
}';
187+
188+
expect($f)->toBeCode($e);
189+
190+
$f = ReflectionClosurePhp81Controller::finalPublicStaticGetterResolver(...);
191+
192+
$e = 'static function ()
193+
{
194+
return static::privateStaticGetterResolver(...);
195+
}';
196+
197+
expect($f)->toBeCode($e);
198+
});
199+
200+
test('first-class callable self return type', function () {
201+
$f = (new ReflectionClosurePhp81Controller())->getSelf(...);
202+
203+
$e = 'function (self $instance): self
204+
{
205+
return $instance;
206+
}';
207+
208+
expect($f)->toBeCode($e);
209+
});
210+
140211
test('intersection types', function () {
141212
$f = function (ClassAlias&Forest $service): ClassAlias&Forest {
142213
return $service;
@@ -196,5 +267,59 @@ public function __construct(
196267
) {
197268
// ..
198269
}
199-
}
200270

271+
public function publicGetter()
272+
{
273+
return $this->privateGetter();
274+
}
275+
276+
private function privateGetter()
277+
{
278+
return $this->service;
279+
}
280+
281+
public static function publicStaticGetter()
282+
{
283+
return static::privateStaticGetter();
284+
}
285+
286+
public static function privateStaticGetter()
287+
{
288+
return (new ReflectionClosurePhp81Controller())->service;
289+
}
290+
291+
public function publicGetterResolver()
292+
{
293+
return $this->privateGetterResolver(...);
294+
}
295+
296+
private function privateGetterResolver()
297+
{
298+
return fn () => $this->service;
299+
}
300+
301+
public static function publicStaticGetterResolver()
302+
{
303+
return static::privateStaticGetterResolver(...);
304+
}
305+
306+
public static function privateStaticGetterResolver()
307+
{
308+
return fn () => (new ReflectionClosurePhp81Controller())->service;
309+
}
310+
311+
final public function finalPublicGetterResolver()
312+
{
313+
return $this->privateGetterResolver(...);
314+
}
315+
316+
final public static function finalPublicStaticGetterResolver()
317+
{
318+
return static::privateStaticGetterResolver(...);
319+
}
320+
321+
public function getSelf(self $instance): self
322+
{
323+
return $instance;
324+
}
325+
}

tests/SerializerPhp81Test.php

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ enum SerializerScopedBackedEnum: string {
154154
});
155155
})->with('serializers');
156156

157-
test('first-class callable', function () {
157+
test('first-class callable with closures', function () {
158158
$f = function ($value) {
159159
return $value;
160160
};
@@ -174,6 +174,58 @@ enum SerializerScopedBackedEnum: string {
174174
expect($f(1)(2))->toBe(4);
175175
})->with('serializers');
176176

177+
test('first-class callable with methods', function () {
178+
$f = (new SerializerPhp81Controller())->publicGetter(...);
179+
180+
$f = s($f);
181+
182+
expect($f())->toBeInstanceOf(SerializerPhp81Service::class);
183+
184+
$f = (new SerializerPhp81Controller())->publicGetterResolver(...);
185+
186+
$f = s($f);
187+
188+
expect($f()()())->toBeInstanceOf(SerializerPhp81Service::class);
189+
})->with('serializers');
190+
191+
test('first-class callable with static methods', function () {
192+
$f = SerializerPhp81Controller::publicStaticGetter(...);
193+
194+
$f = s($f);
195+
196+
expect($f())->toBeInstanceOf(SerializerPhp81Service::class);
197+
198+
$f = SerializerPhp81Controller::publicStaticGetterResolver(...);
199+
200+
$f = s($f);
201+
202+
expect($f()()())->toBeInstanceOf(SerializerPhp81Service::class);
203+
})->with('serializers');
204+
205+
test('first-class callable final method', function () {
206+
$f = (new SerializerPhp81Controller())->finalPublicGetterResolver(...);
207+
208+
$f = s($f);
209+
210+
expect($f()()())->toBeInstanceOf(SerializerPhp81Service::class);
211+
212+
$f = SerializerPhp81Controller::finalPublicStaticGetterResolver(...);
213+
214+
$f = s($f);
215+
216+
expect($f()()())->toBeInstanceOf(SerializerPhp81Service::class);
217+
})->with('serializers');
218+
219+
test('first-class callable self return type', function () {
220+
$f = (new SerializerPhp81Controller())->getSelf(...);
221+
222+
$f = s($f);
223+
224+
$controller = new SerializerPhp81Controller();
225+
226+
expect($f($controller))->toBeInstanceOf(SerializerPhp81Controller::class);
227+
})->with('serializers');
228+
177229
test('intersection types', function () {
178230
$f = function (SerializerPhp81HasName&SerializerPhp81HasId $service): SerializerPhp81HasName&SerializerPhp81HasId {
179231
return $service;
@@ -243,5 +295,60 @@ public function __construct(
243295
) {
244296
// ..
245297
}
298+
299+
public function publicGetter()
300+
{
301+
return $this->privateGetter();
302+
}
303+
304+
private function privateGetter()
305+
{
306+
return $this->service;
307+
}
308+
309+
public static function publicStaticGetter()
310+
{
311+
return static::privateStaticGetter();
312+
}
313+
314+
public static function privateStaticGetter()
315+
{
316+
return (new SerializerPhp81Controller())->service;
317+
}
318+
319+
public function publicGetterResolver()
320+
{
321+
return $this->privateGetterResolver(...);
322+
}
323+
324+
private function privateGetterResolver()
325+
{
326+
return fn () => $this->service;
327+
}
328+
329+
public static function publicStaticGetterResolver()
330+
{
331+
return static::privateStaticGetterResolver(...);
332+
}
333+
334+
public static function privateStaticGetterResolver()
335+
{
336+
return fn () => (new SerializerPhp81Controller())->service;
337+
}
338+
339+
final public function finalPublicGetterResolver()
340+
{
341+
return $this->privateGetterResolver(...);
342+
}
343+
344+
final public static function finalPublicStaticGetterResolver()
345+
{
346+
return static::privateStaticGetterResolver(...);
347+
}
348+
349+
public function getSelf(self $instance): self
350+
{
351+
return $instance;
352+
}
246353
}
247354

0 commit comments

Comments
 (0)