Skip to content

Commit de95592

Browse files
committed
Adds function attributes support
1 parent f40c2e8 commit de95592

File tree

4 files changed

+146
-0
lines changed

4 files changed

+146
-0
lines changed

src/Support/ReflectionClosure.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -676,6 +676,22 @@ 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+
$name = $attribute->getName();
683+
$arguments = implode(', ', array_map(function ($argument) {
684+
return sprintf("'%s'", str_replace("'", "\\'", $argument));
685+
}, $attribute->getArguments()));
686+
687+
return "#[$name($arguments)]";
688+
}, $this->getAttributes());
689+
690+
if (! empty($attributesCode)) {
691+
$code = implode("\n", array_merge($attributesCode, [$code]));
692+
}
693+
}
694+
679695
$this->code = $code;
680696

681697
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: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,57 @@ 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 first-class callable with methods', function () {
322+
$model = new Model();
323+
324+
$f = (new SerializerPhp81Controller())->publicGetter(...);
325+
326+
$e = <<<EOF
327+
#[Tests\Fixtures\ModelAttribute()]
328+
#[MyAttribute('My " \' Argument 1', 'Tests\Fixtures\Model')]
329+
function ()
330+
{
331+
return \$this->privateGetter();
332+
}
333+
EOF;
334+
335+
expect($f)->toBeCode($e);
336+
});
337+
287338
class ReflectionClosurePhp81Service
288339
{
289340
}

tests/SerializerPhp81Test.php

Lines changed: 70 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,67 @@ 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 first-class callable with methods', function () {
349+
$f = (new SerializerPhp81Controller())->publicGetter(...);
350+
351+
$reflector = new ReflectionFunction($f);
352+
353+
expect($reflector->getAttributes())->sequence(
354+
fn ($attribute) => $attribute
355+
->getName()->toBe(ModelAttribute::class)
356+
->getArguments()->toBe([]),
357+
fn ($attribute) => $attribute
358+
->getName()->toBe(MyAttribute::class)
359+
->getArguments()->toBe([
360+
'My " \' Argument 1', Model::class,
361+
])
362+
);
363+
364+
expect($f())->toBeInstanceOf(SerializerPhp81Service::class);
365+
})->with('serializers');
366+
305367
interface SerializerPhp81HasId {}
306368
interface SerializerPhp81HasName {}
307369

@@ -318,6 +380,8 @@ public function __construct(
318380
// ..
319381
}
320382

383+
#[ModelAttribute]
384+
#[MyAttribute('My " \' Argument 1', Model::class)]
321385
public function publicGetter()
322386
{
323387
return $this->privateGetter();
@@ -374,3 +438,9 @@ public function getSelf(self $instance): self
374438
}
375439
}
376440

441+
#[Attribute(Attribute::TARGET_METHOD|Attribute::TARGET_FUNCTION)]
442+
class MyAttribute
443+
{
444+
// ..
445+
}
446+

0 commit comments

Comments
 (0)