Skip to content

Commit 3cdb166

Browse files
authored
Merge pull request #46 from laravel/feat/function-attributes
Adds Function Attributes support
2 parents f40c2e8 + 8d89f04 commit 3cdb166

File tree

5 files changed

+206
-1
lines changed

5 files changed

+206
-1
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ echo $closure(); // james;
5151

5252
### Caveats
5353

54-
Creating **anonymous classes** within closures is not supported.
54+
1. Creating **anonymous classes** within closures is not supported.
55+
2. Using attributes within closures is not supported.
5556

5657
## Contributing
5758

src/Support/ReflectionClosure.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -676,6 +676,30 @@ public function getCode()
676676
$this->isShortClosure = $isShortClosure;
677677
$this->isBindingRequired = $isUsingThisObject;
678678
$this->isScopeRequired = $isUsingScope;
679+
680+
if (PHP_VERSION_ID >= 80100) {
681+
$attributesCode = array_map(function ($attribute) {
682+
$arguments = $attribute->getArguments();
683+
684+
$name = $attribute->getName();
685+
$arguments = implode(', ', array_map(function ($argument, $key) {
686+
$argument = sprintf("'%s'", str_replace("'", "\\'", $argument));
687+
688+
if (is_string($key)) {
689+
$argument = sprintf('%s: %s', $key, $argument);
690+
}
691+
692+
return $argument;
693+
}, $arguments, array_keys($arguments)));
694+
695+
return "#[$name($arguments)]";
696+
}, $this->getAttributes());
697+
698+
if (! empty($attributesCode)) {
699+
$code = implode("\n", array_merge($attributesCode, [$code]));
700+
}
701+
}
702+
679703
$this->code = $code;
680704

681705
return $this->code;

tests/Fixtures/ModelAttribute.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
namespace Tests\Fixtures;
4+
5+
#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION)]
6+
class ModelAttribute
7+
{
8+
// ..
9+
}

tests/ReflectionClosurePhp81Test.php

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,74 @@ enum ScopedBackedEnum: string {
284284
expect($f)->toBeCode($e);
285285
});
286286

287+
test('function attributes without arguments', function () {
288+
$model = new Model();
289+
290+
$f = #[MyAttribute] function () {
291+
return true;
292+
};
293+
294+
$e = <<<EOF
295+
#[MyAttribute()]
296+
function () {
297+
return true;
298+
}
299+
EOF;
300+
301+
expect($f)->toBeCode($e);
302+
});
303+
304+
test('function attributes with arguments', function () {
305+
$model = new Model();
306+
307+
$f = #[MyAttribute('My " \' Argument 1', Model::class)] function () {
308+
return true;
309+
};
310+
311+
$e = <<<EOF
312+
#[MyAttribute('My " \' Argument 1', 'Tests\Fixtures\Model')]
313+
function () {
314+
return true;
315+
}
316+
EOF;
317+
318+
expect($f)->toBeCode($e);
319+
});
320+
321+
test('function attributes with named arguments', function () {
322+
$model = new Model();
323+
324+
$f = #[MyAttribute(string: 'My " \' Argument 1', model:Model::class)] function () {
325+
return false;
326+
};
327+
328+
$e = <<<EOF
329+
#[MyAttribute(string: 'My " \' Argument 1', model: 'Tests\Fixtures\Model')]
330+
function () {
331+
return false;
332+
}
333+
EOF;
334+
335+
expect($f)->toBeCode($e);
336+
});
337+
338+
test('function attributes with first-class callable with methods', function () {
339+
$model = new Model();
340+
341+
$f = (new SerializerPhp81Controller())->publicGetter(...);
342+
343+
$e = <<<EOF
344+
#[Tests\Fixtures\ModelAttribute()]
345+
#[MyAttribute('My " \' Argument 1', 'Tests\Fixtures\Model')]
346+
function ()
347+
{
348+
return \$this->privateGetter();
349+
}
350+
EOF;
351+
352+
expect($f)->toBeCode($e);
353+
});
354+
287355
class ReflectionClosurePhp81Service
288356
{
289357
}

tests/SerializerPhp81Test.php

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<?php
22

33
use Tests\Fixtures\Model;
4+
use Tests\Fixtures\ModelAttribute;
45

56
enum SerializerGlobalEnum {
67
case Admin;
@@ -302,6 +303,97 @@ enum SerializerScopedBackedEnum: string {
302303
expect($f(new Model))->toBeInstanceOf(Model::class);
303304
})->with('serializers');
304305

306+
test('function attributes without arguments', function () {
307+
$model = new Model();
308+
309+
$f = #[MyAttribute] function () {
310+
return true;
311+
};
312+
313+
$f = s($f);
314+
315+
$reflector = new ReflectionFunction($f);
316+
317+
expect($reflector->getAttributes())->sequence(
318+
fn ($attribute) => $attribute
319+
->getName()->toBe(MyAttribute::class)
320+
->getArguments()->toBeEmpty(),
321+
);
322+
323+
expect($f())->toBeTrue();
324+
})->with('serializers');
325+
326+
test('function attributes with arguments', function () {
327+
$model = new Model();
328+
329+
$f = #[MyAttribute('My " \' Argument 1', Model::class)] function () {
330+
return false;
331+
};
332+
333+
$f = s($f);
334+
335+
$reflector = new ReflectionFunction($f);
336+
337+
expect($reflector->getAttributes())->sequence(
338+
fn ($attribute) => $attribute
339+
->getName()->toBe(MyAttribute::class)
340+
->getArguments()->toBe([
341+
'My " \' Argument 1', Model::class,
342+
])
343+
);
344+
345+
expect($f())->toBeFalse();
346+
})->with('serializers');
347+
348+
test('function attributes with named arguments', function () {
349+
$model = new Model();
350+
351+
$f = #[MyAttribute(string: 'My " \' Argument 1', model:Model::class)] function () {
352+
return false;
353+
};
354+
355+
$f = s($f);
356+
357+
$reflector = new ReflectionFunction($f);
358+
359+
expect($reflector->getAttributes())->sequence(function ($attribute) {
360+
361+
$attribute
362+
->getName()->toBe(MyAttribute::class)
363+
->getArguments()->toBe([
364+
'string' => 'My " \' Argument 1',
365+
'model' => Model::class,
366+
]);
367+
368+
expect($attribute->value->newInstance())
369+
->string->toBe('My " \' Argument 1')
370+
->model->toBe(Model::class);
371+
});
372+
373+
expect($f())->toBeFalse();
374+
})->with('serializers');
375+
376+
test('function attributes with first-class callable with methods', function () {
377+
$f = (new SerializerPhp81Controller())->publicGetter(...);
378+
379+
$f = s($f);
380+
381+
$reflector = new ReflectionFunction($f);
382+
383+
expect($reflector->getAttributes())->sequence(
384+
fn ($attribute) => $attribute
385+
->getName()->toBe(ModelAttribute::class)
386+
->getArguments()->toBe([]),
387+
fn ($attribute) => $attribute
388+
->getName()->toBe(MyAttribute::class)
389+
->getArguments()->toBe([
390+
'My " \' Argument 1', Model::class,
391+
])
392+
);
393+
394+
expect($f())->toBeInstanceOf(SerializerPhp81Service::class);
395+
})->with('serializers');
396+
305397
interface SerializerPhp81HasId {}
306398
interface SerializerPhp81HasName {}
307399

@@ -318,6 +410,8 @@ public function __construct(
318410
// ..
319411
}
320412

413+
#[ModelAttribute]
414+
#[MyAttribute('My " \' Argument 1', Model::class)]
321415
public function publicGetter()
322416
{
323417
return $this->privateGetter();
@@ -374,3 +468,12 @@ public function getSelf(self $instance): self
374468
}
375469
}
376470

471+
#[Attribute(Attribute::TARGET_METHOD|Attribute::TARGET_FUNCTION)]
472+
class MyAttribute
473+
{
474+
public function __construct(public $string, public $model)
475+
{
476+
// ..
477+
}
478+
}
479+

0 commit comments

Comments
 (0)