Skip to content

Commit

Permalink
Adds support for first class callable syntax
Browse files Browse the repository at this point in the history
  • Loading branch information
nunomaduro committed Jan 27, 2022
1 parent b88a441 commit 67af0ea
Show file tree
Hide file tree
Showing 3 changed files with 248 additions and 5 deletions.
11 changes: 11 additions & 0 deletions src/Support/ReflectionClosure.php
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,9 @@ public function getCode()
$tokens = $this->getTokens();
$state = $lastState = 'start';
$inside_structure = false;
$isFirstClassCallable = false;
$isShortClosure = false;

$inside_structure_mark = 0;
$open = 0;
$code = '';
Expand All @@ -130,11 +132,15 @@ public function getCode()
case 'start':
if ($token[0] === T_FUNCTION || $token[0] === T_STATIC) {
$code .= $token[1];

$state = $token[0] === T_FUNCTION ? 'function' : 'static';
} elseif ($token[0] === T_FN) {
$isShortClosure = true;
$code .= $token[1];
$state = 'closure_args';
} elseif ($token[0] === T_PUBLIC || $token[0] === T_PROTECTED || $token[0] === T_PRIVATE) {
$code = '';
$isFirstClassCallable = true;
}
break;
case 'static':
Expand All @@ -155,6 +161,11 @@ public function getCode()
case 'function':
switch ($token[0]) {
case T_STRING:
if ($isFirstClassCallable) {
$state = 'closure_args';
break;
}

$code = '';
$state = 'named_function';
break;
Expand Down
133 changes: 129 additions & 4 deletions tests/ReflectionClosurePhp81Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,21 +107,21 @@ enum ScopedBackedEnum: string {

test('readonly properties', function () {
$f = function () {
$controller = new SerializerPhp81Controller();
$controller = new ReflectionClosurePhp81Controller();

$controller->service = 'foo';
};

$e = 'function () {
$controller = new \SerializerPhp81Controller();
$controller = new \ReflectionClosurePhp81Controller();
$controller->service = \'foo\';
}';

expect($f)->toBeCode($e);
});

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

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

test('first-class callable with methods', function () {
$f = (new ReflectionClosurePhp81Controller())->publicGetter(...);

$e = 'function ()
{
return $this->privateGetter();
}';

expect($f)->toBeCode($e);

$f = (new ReflectionClosurePhp81Controller())->publicGetterResolver(...);

$e = 'function ()
{
return $this->privateGetterResolver(...);
}';

expect($f)->toBeCode($e);
});

test('first-class callable with static methods', function () {
$f = ReflectionClosurePhp81Controller::publicStaticGetter(...);

$e = 'static function ()
{
return static::privateStaticGetter();
}';

expect($f)->toBeCode($e);

$f = ReflectionClosurePhp81Controller::publicStaticGetterResolver(...);

$e = 'static function ()
{
return static::privateStaticGetterResolver(...);
}';

expect($f)->toBeCode($e);
});

test('first-class callable final method', function () {
$f = (new ReflectionClosurePhp81Controller())->finalPublicGetterResolver(...);

$e = 'function ()
{
return $this->privateGetterResolver(...);
}';

expect($f)->toBeCode($e);

$f = ReflectionClosurePhp81Controller::finalPublicStaticGetterResolver(...);

$e = 'static function ()
{
return static::privateStaticGetterResolver(...);
}';

expect($f)->toBeCode($e);
});

test('first-class callable self return type', function () {
$f = (new ReflectionClosurePhp81Controller())->getSelf(...);

$e = 'function (self $instance): self
{
return $instance;
}';

expect($f)->toBeCode($e);
});

test('intersection types', function () {
$f = function (ClassAlias&Forest $service): ClassAlias&Forest {
return $service;
Expand Down Expand Up @@ -196,5 +267,59 @@ public function __construct(
) {
// ..
}
}

public function publicGetter()
{
return $this->privateGetter();
}

private function privateGetter()
{
return $this->service;
}

public static function publicStaticGetter()
{
return static::privateStaticGetter();
}

public static function privateStaticGetter()
{
return (new ReflectionClosurePhp81Controller())->service;
}

public function publicGetterResolver()
{
return $this->privateGetterResolver(...);
}

private function privateGetterResolver()
{
return fn () => $this->service;
}

public static function publicStaticGetterResolver()
{
return static::privateStaticGetterResolver(...);
}

public static function privateStaticGetterResolver()
{
return fn () => (new ReflectionClosurePhp81Controller())->service;
}

final public function finalPublicGetterResolver()
{
return $this->privateGetterResolver(...);
}

final public static function finalPublicStaticGetterResolver()
{
return static::privateStaticGetterResolver(...);
}

public function getSelf(self $instance): self
{
return $instance;
}
}
109 changes: 108 additions & 1 deletion tests/SerializerPhp81Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ enum SerializerScopedBackedEnum: string {
});
})->with('serializers');

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

test('first-class callable with methods', function () {
$f = (new SerializerPhp81Controller())->publicGetter(...);

$f = s($f);

expect($f())->toBeInstanceOf(SerializerPhp81Service::class);

$f = (new SerializerPhp81Controller())->publicGetterResolver(...);

$f = s($f);

expect($f()()())->toBeInstanceOf(SerializerPhp81Service::class);
})->with('serializers');

test('first-class callable with static methods', function () {
$f = SerializerPhp81Controller::publicStaticGetter(...);

$f = s($f);

expect($f())->toBeInstanceOf(SerializerPhp81Service::class);

$f = SerializerPhp81Controller::publicStaticGetterResolver(...);

$f = s($f);

expect($f()()())->toBeInstanceOf(SerializerPhp81Service::class);
})->with('serializers');

test('first-class callable final method', function () {
$f = (new SerializerPhp81Controller())->finalPublicGetterResolver(...);

$f = s($f);

expect($f()()())->toBeInstanceOf(SerializerPhp81Service::class);

$f = SerializerPhp81Controller::finalPublicStaticGetterResolver(...);

$f = s($f);

expect($f()()())->toBeInstanceOf(SerializerPhp81Service::class);
})->with('serializers');

test('first-class callable self return type', function () {
$f = (new SerializerPhp81Controller())->getSelf(...);

$f = s($f);

$controller = new SerializerPhp81Controller();

expect($f($controller))->toBeInstanceOf(SerializerPhp81Controller::class);
})->with('serializers');

test('intersection types', function () {
$f = function (SerializerPhp81HasName&SerializerPhp81HasId $service): SerializerPhp81HasName&SerializerPhp81HasId {
return $service;
Expand Down Expand Up @@ -243,5 +295,60 @@ public function __construct(
) {
// ..
}

public function publicGetter()
{
return $this->privateGetter();
}

private function privateGetter()
{
return $this->service;
}

public static function publicStaticGetter()
{
return static::privateStaticGetter();
}

public static function privateStaticGetter()
{
return (new SerializerPhp81Controller())->service;
}

public function publicGetterResolver()
{
return $this->privateGetterResolver(...);
}

private function privateGetterResolver()
{
return fn () => $this->service;
}

public static function publicStaticGetterResolver()
{
return static::privateStaticGetterResolver(...);
}

public static function privateStaticGetterResolver()
{
return fn () => (new SerializerPhp81Controller())->service;
}

final public function finalPublicGetterResolver()
{
return $this->privateGetterResolver(...);
}

final public static function finalPublicStaticGetterResolver()
{
return static::privateStaticGetterResolver(...);
}

public function getSelf(self $instance): self
{
return $instance;
}
}

0 comments on commit 67af0ea

Please sign in to comment.