Skip to content

Commit 5232e64

Browse files
Merge pull request #12 from Laragear/feat/5.x/attr-and-more
[5.x] Adds Attr helpers and some other.
2 parents 8738665 + fec3a92 commit 5232e64

File tree

8 files changed

+595
-11
lines changed

8 files changed

+595
-11
lines changed

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: 107 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@
99
use Illuminate\Contracts\Http\Kernel as KernelContract;
1010
use Illuminate\Contracts\Validation\Factory;
1111
use Illuminate\Routing\Router;
12+
use Illuminate\Support\Str;
13+
use Illuminate\View\Compilers\BladeCompiler;
14+
use Laragear\Meta\Attributes\RegisterRule;
1215
use Laragear\Meta\Http\Middleware\MiddlewareDeclaration;
16+
use ReflectionClass;
1317

1418
use function array_fill;
1519
use function array_fill_keys;
@@ -19,6 +23,13 @@
1923

2024
trait BootHelpers
2125
{
26+
/**
27+
* Cached closures for registered validation rules.
28+
*
29+
* @var array<class-string, \Closure>
30+
*/
31+
protected static array $cachedValidationRules = [];
32+
2233
/**
2334
* Extends a manager-like service.
2435
*
@@ -53,7 +64,7 @@ protected function withValidationRule(
5364
string $rule,
5465
callable|string $callback,
5566
callable|string|null $message = null,
56-
bool $implicit = false
67+
bool $implicit = false,
5768
): void {
5869
$this->callAfterResolving(
5970
'validator',
@@ -63,7 +74,50 @@ static function (Factory $validator, Application $app) use ($message, $callback,
6374
$implicit
6475
? $validator->extendImplicit($rule, $callback, $message)
6576
: $validator->extend($rule, $callback, $message);
66-
}
77+
},
78+
);
79+
}
80+
81+
/**
82+
* Registers all Validation Rules found in a directory with a message from a translation prefix.
83+
*
84+
* @param class-string|class-string[] $classes
85+
* @param string $keyPrefix If you register a translation key as "my-package", the validation
86+
* rules will use "my-package::validation.{rule}".
87+
*/
88+
protected function withValidationRulesFrom(string|array $classes, string $keyPrefix): void
89+
{
90+
$this->callAfterResolving(
91+
'validator',
92+
static function (Factory $validator) use ($classes, $keyPrefix): void {
93+
foreach ((array) $classes as $class) {
94+
if (! isset(static::$cachedValidationRules[$class])) {
95+
static::$cachedValidationRules[$class] = [];
96+
97+
foreach ((new ReflectionClass($class))->getMethods() as $method) {
98+
/** @var \Laragear\Meta\Attributes\RegisterRule|null $attribute */
99+
if (
100+
$method->isPublic() && $method->isStatic() &&
101+
$attribute = Attr::of($method)->first(RegisterRule::class)
102+
) {
103+
static::$cachedValidationRules[$class][$attribute->name] = [
104+
$method->getClosure(),
105+
"$keyPrefix::validation.".(
106+
$attribute->translationKey ?: Str::snake($method->getName())
107+
),
108+
$attribute->implicit,
109+
];
110+
}
111+
}
112+
}
113+
114+
foreach (static::$cachedValidationRules[$class] as $name => [$callback, $message, $implicit]) {
115+
$implicit
116+
? $validator->extendImplicit($name, $callback, $message)
117+
: $validator->extend($name, $callback, $message);
118+
}
119+
}
120+
},
67121
);
68122
}
69123

@@ -78,7 +132,7 @@ static function (Factory $validator, Application $app) use ($message, $callback,
78132
protected function withMiddleware(string $class): MiddlewareDeclaration
79133
{
80134
return new MiddlewareDeclaration(
81-
$this->app->make(Router::class), $this->app->make(KernelContract::class), $class
135+
$this->app->make(Router::class), $this->app->make(KernelContract::class), $class,
82136
);
83137
}
84138

@@ -137,7 +191,6 @@ protected function withPolicy(string $model, string $policy): void
137191
* Schedule a Job or Command using a callback.
138192
*
139193
* @param callable(\Illuminate\Console\Scheduling\Schedule):mixed $callback
140-
* @return void
141194
*
142195
* @see https://www.laravelpackage.com/06-artisan-commands/#scheduling-a-command-in-the-service-provider
143196
*/
@@ -160,8 +213,57 @@ protected function withPublishableMigrations(array|string $directories, array|st
160213
$directories = (array) $directories;
161214

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

0 commit comments

Comments
 (0)