Skip to content

Commit eade0e0

Browse files
[5.x] Adds Attr helpers and some other.
1 parent 0ad3823 commit eade0e0

8 files changed

Lines changed: 590 additions & 15 deletions

File tree

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,24 @@ class AddServiceKey extends Command
135135
}
136136
```
137137

138+
## Attribute extractor
139+
140+
The package contains the `Attr` helper that receives a target class, object, function, and allows to retrieve all or one attribute. Better yet, you can directly call a method or retrieve an attribute property.
141+
142+
```php
143+
use Laragear\Meta\Attr;use Vendor\Package\Attributes\MyCustomAttribute;
144+
145+
#[MyCustomAttribute(color: 'blue')]
146+
class Car
147+
{
148+
//
149+
}
150+
151+
$car = new Car;
152+
153+
echo Attr::of($car)->get(MyCustomAttribute::class, 'color'); // "blue"
154+
```
155+
138156
## Laravel Octane compatibility
139157

140158
- There are no singletons using a stale application instance.

phpunit.xml

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
11
<?xml version="1.0" encoding="UTF-8"?>
2-
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" bootstrap="vendor/autoload.php" colors="true" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/11.0/phpunit.xsd" cacheDirectory=".phpunit.cache">
3-
<coverage>
4-
<report>
5-
<clover outputFile="build/logs/clover.xml"/>
6-
</report>
7-
</coverage>
2+
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" bootstrap="vendor/autoload.php" colors="true" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/13.0/phpunit.xsd" cacheDirectory=".phpunit.cache">
83
<testsuites>
94
<testsuite name="Test Suite">
105
<directory>tests</directory>

src/Attr.php

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
<?php
2+
3+
namespace Laragear\Meta;
4+
5+
use Closure;
6+
use Countable;
7+
use Illuminate\Support\Collection;
8+
use InvalidArgumentException;
9+
use ReflectionAttribute;
10+
use ReflectionClass;
11+
use ReflectionFunction;
12+
use ReflectionMethod;
13+
use ReflectionParameter;
14+
use ReflectionProperty;
15+
16+
use function class_exists;
17+
use function function_exists;
18+
use function is_array;
19+
use function is_object;
20+
use function is_string;
21+
use function method_exists;
22+
use function property_exists;
23+
24+
/** @phpstan-consistent-constructor */
25+
class Attr implements Countable
26+
{
27+
/**
28+
* Cached reflection of the target.
29+
*/
30+
protected ReflectionClass|ReflectionMethod|ReflectionFunction|ReflectionParameter|ReflectionProperty $reflection;
31+
32+
/**
33+
* Create a new Attribute instance.
34+
*
35+
* @noinspection PhpUnhandledExceptionInspection
36+
*/
37+
public function __construct(mixed $target)
38+
{
39+
if (
40+
$target instanceof ReflectionClass ||
41+
$target instanceof ReflectionMethod ||
42+
$target instanceof ReflectionFunction ||
43+
$target instanceof ReflectionParameter ||
44+
$target instanceof ReflectionProperty
45+
) {
46+
$this->reflection = $target;
47+
} elseif (is_string($target)) {
48+
if (function_exists($target)) {
49+
$this->reflection = new ReflectionFunction($target);
50+
} elseif (str_contains($target, '::')) {
51+
$this->reflection = new ReflectionMethod($target);
52+
} elseif (class_exists($target)) {
53+
$this->reflection = new ReflectionClass($target);
54+
}
55+
} elseif (is_object($target)) {
56+
if ($target instanceof Closure) {
57+
$this->reflection = new ReflectionFunction($target);
58+
} else {
59+
$this->reflection = new ReflectionClass($target);
60+
}
61+
} elseif (is_array($target)) {
62+
if (method_exists($target[0], $target[1])) {
63+
$this->reflection = new ReflectionMethod($target[0], $target[1]);
64+
}
65+
66+
if (property_exists($target[0], $target[1])) {
67+
$this->reflection = new ReflectionProperty($target[0], $target[1]);
68+
}
69+
} else {
70+
throw new InvalidArgumentException(
71+
'The target must be a class, object, callable, or class-property array.'
72+
);
73+
}
74+
}
75+
76+
/**
77+
* Retrieve a collection of all Reflection Attributes.
78+
*
79+
* @param class-string|null $attribute
80+
* @return \Illuminate\Support\Collection<int, \ReflectionAttribute>
81+
*/
82+
protected function collect(?string $attribute): Collection
83+
{
84+
return new Collection(
85+
$this->reflection->getAttributes($attribute, ReflectionAttribute::IS_INSTANCEOF), // @phpstan-ignore-line
86+
);
87+
}
88+
89+
/**
90+
* Retrieves all the instanced attributes of the target, optionally filtered by the given classes.
91+
*
92+
* @template TAttribute of object
93+
*
94+
* @param class-string<TAttribute>|null $attribute
95+
* @return ($attribute is empty ? \Illuminate\Support\Collection<int, object> : \Illuminate\Support\Collection<int, TAttribute>)
96+
*/
97+
public function all(?string $attribute = null): Collection
98+
{
99+
return $this->collect($attribute)->map(static function (ReflectionAttribute $attribute): object {
100+
return $attribute->newInstance();
101+
});
102+
}
103+
104+
/**
105+
* Retrieves the first instanced attribute value from a class, method, or property.
106+
*
107+
* @template TAttribute of object
108+
*
109+
* @param class-string<TAttribute>|null $attribute
110+
* @return TAttribute|null
111+
*/
112+
public function first(?string $attribute = null): ?object
113+
{
114+
return $this->collect($attribute)->first()?->newInstance();
115+
}
116+
117+
/**
118+
* Executes a method from the first instanced attribute.
119+
*
120+
* @template TAttribute of object
121+
*
122+
* @param class-string<TAttribute> $attribute
123+
*/
124+
public function call(string $attribute, string $method, mixed ...$arguments): mixed
125+
{
126+
return $this->first($attribute)?->{$method}(...$arguments);
127+
}
128+
129+
/**
130+
* Retrieves a value from the first instanced attribute.
131+
*
132+
* @template TAttribute of object
133+
*
134+
* @param class-string<TAttribute> $attribute
135+
*/
136+
public function get(string $attribute, string $property): mixed
137+
{
138+
return $this->first($attribute)?->$property;
139+
}
140+
141+
/**
142+
* @inheritDoc
143+
*/
144+
public function count(): int
145+
{
146+
return $this->collect(null)->count();
147+
}
148+
149+
/**
150+
* Creates a new Attributes instance for the given target.
151+
*
152+
* @param \Closure|object|class-string|callable-string|array{0: string|object, 1: string} $target
153+
*/
154+
public static function of(mixed $target): static
155+
{
156+
return new static($target);
157+
}
158+
}

src/Attributes/RegisterRule.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace Laragear\Meta\Attributes;
4+
5+
use Attribute;
6+
7+
#[Attribute(Attribute::TARGET_METHOD)]
8+
readonly class RegisterRule
9+
{
10+
/**
11+
* Create a new Validation Key instance.
12+
*/
13+
public function __construct(public string $name, public ?string $translationKey = null, public bool $implicit = false)
14+
{
15+
//
16+
}
17+
}

src/BootHelpers.php

Lines changed: 102 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,28 @@
99
use Illuminate\Contracts\Http\Kernel as KernelContract;
1010
use Illuminate\Contracts\Validation\Factory;
1111
use Illuminate\Routing\Router;
12-
use Illuminate\Support\Collection;
12+
use Illuminate\Support\Facades\Blade;
1313
use Illuminate\Support\Str;
14+
use Illuminate\View\Compilers\BladeCompiler;
15+
use Laragear\Meta\Attributes\RegisterRule;
1416
use Laragear\Meta\Http\Middleware\MiddlewareDeclaration;
15-
use SplFileInfo;
17+
use ReflectionClass;
1618

1719
use function array_fill;
1820
use function array_fill_keys;
1921
use function count;
2022
use function is_callable;
2123
use function is_string;
22-
use function method_exists;
23-
use function now;
2424

2525
trait BootHelpers
2626
{
27+
/**
28+
* Cached closures for registered validation rules.
29+
*
30+
* @var array<class-string, \Closure>
31+
*/
32+
protected static array $cachedValidationRules = [];
33+
2734
/**
2835
* Extends a manager-like service.
2936
*
@@ -58,7 +65,7 @@ protected function withValidationRule(
5865
string $rule,
5966
callable|string $callback,
6067
callable|string|null $message = null,
61-
bool $implicit = false
68+
bool $implicit = false,
6269
): void {
6370
$this->callAfterResolving(
6471
'validator',
@@ -68,7 +75,45 @@ static function (Factory $validator, Application $app) use ($message, $callback,
6875
$implicit
6976
? $validator->extendImplicit($rule, $callback, $message)
7077
: $validator->extend($rule, $callback, $message);
71-
}
78+
},
79+
);
80+
}
81+
82+
/**
83+
* Registers all Validation Rules found in a directory with a message from a translation prefix.
84+
*
85+
* @param class-string|class-string[] $classes
86+
* @param string $keyPrefix If you register a translation key as "my-package", the validation
87+
* rules will use "my-package::validation.{rule}".
88+
*/
89+
protected function withValidationRulesFrom(string|array $classes, string $keyPrefix): void
90+
{
91+
$this->callAfterResolving(
92+
'validator',
93+
static function (Factory $validator) use ($classes, $keyPrefix): void {
94+
foreach ((array) $classes as $class) {
95+
if (! isset(static::$cachedValidationRules[$class])) {
96+
static::$cachedValidationRules[$class] = [];
97+
98+
foreach ((new ReflectionClass($class))->getMethods() as $method) {
99+
/** @var \Laragear\Meta\Attributes\RegisterRule|null $attribute */
100+
if ($method->isPublic() && $method->isStatic() && $attribute = Attr::of($method)->first(RegisterRule::class)) {
101+
static::$cachedValidationRules[$class][$attribute->name] = [
102+
$method->getClosure(),
103+
"$keyPrefix::validation.".($attribute->translationKey ?: Str::snake($method->getName())),
104+
$attribute->implicit,
105+
];
106+
}
107+
}
108+
}
109+
110+
foreach (static::$cachedValidationRules[$class] as $name => [$callback, $message, $implicit]) {
111+
$implicit
112+
? $validator->extendImplicit($name, $callback, $message)
113+
: $validator->extend($name, $callback, $message);
114+
}
115+
}
116+
},
72117
);
73118
}
74119

@@ -83,7 +128,7 @@ static function (Factory $validator, Application $app) use ($message, $callback,
83128
protected function withMiddleware(string $class): MiddlewareDeclaration
84129
{
85130
return new MiddlewareDeclaration(
86-
$this->app->make(Router::class), $this->app->make(KernelContract::class), $class
131+
$this->app->make(Router::class), $this->app->make(KernelContract::class), $class,
87132
);
88133
}
89134

@@ -142,7 +187,6 @@ protected function withPolicy(string $model, string $policy): void
142187
* Schedule a Job or Command using a callback.
143188
*
144189
* @param callable(\Illuminate\Console\Scheduling\Schedule):mixed $callback
145-
* @return void
146190
*
147191
* @see https://www.laravelpackage.com/06-artisan-commands/#scheduling-a-command-in-the-service-provider
148192
*/
@@ -165,8 +209,57 @@ protected function withPublishableMigrations(array|string $directories, array|st
165209
$directories = (array) $directories;
166210

167211
$this->publishesMigrations(array_fill_keys(
168-
$directories, array_fill(0, count($directories), $this->app->databasePath('migrations'))
212+
$directories, array_fill(0, count($directories), $this->app->databasePath('migrations')),
169213
), $groups);
170214
}
171215
}
216+
217+
/**
218+
* Registers a simple Blade directive.
219+
*
220+
* @param array<string, callable>|string $name
221+
* @param ($name is string ? callable : null) $handler
222+
*/
223+
protected function withBladeDirectives(string|array $name, ?callable $handler = null): void
224+
{
225+
$name = $handler ? [$name => $handler] : $name;
226+
227+
$this->callAfterResolving(
228+
BladeCompiler::class,
229+
static function (BladeCompiler $blade) use ($name): void {
230+
foreach ($name as $key => $handler) {
231+
$blade->directive($key, $handler);
232+
}
233+
},
234+
);
235+
}
236+
237+
/**
238+
* Registers a directory of Blade components under a prefix.
239+
*/
240+
protected function withBladeComponents(string $path, string $prefix): void
241+
{
242+
$this->callAfterResolving(
243+
BladeCompiler::class,
244+
static function (BladeCompiler $blade) use ($path, $prefix): void {
245+
$blade->componentNamespace($path, $prefix);
246+
},
247+
);
248+
}
249+
250+
/**
251+
* Returns the cached validation rules.
252+
*/
253+
public static function cachedValidationRules(): array
254+
{
255+
return static::$cachedValidationRules;
256+
}
257+
258+
/**
259+
* Flushes cached validation rules retrieved by reflection.
260+
*/
261+
public static function flushCachedValidationRules(): void
262+
{
263+
static::$cachedValidationRules = [];
264+
}
172265
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<?php

0 commit comments

Comments
 (0)