Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,24 @@ class AddServiceKey extends Command
}
```

## Attribute extractor

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.

```php
use Laragear\Meta\Attr;use Vendor\Package\Attributes\MyCustomAttribute;

#[MyCustomAttribute(color: 'blue')]
class Car
{
//
}

$car = new Car;

echo Attr::of($car)->get(MyCustomAttribute::class, 'color'); // "blue"
```

## Laravel Octane compatibility

- There are no singletons using a stale application instance.
Expand Down
7 changes: 1 addition & 6 deletions phpunit.xml
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">
<coverage>
<report>
<clover outputFile="build/logs/clover.xml"/>
</report>
</coverage>
<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">
<testsuites>
<testsuite name="Test Suite">
<directory>tests</directory>
Expand Down
158 changes: 158 additions & 0 deletions src/Attr.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
<?php

namespace Laragear\Meta;

use Closure;
use Countable;
use Illuminate\Support\Collection;
use InvalidArgumentException;
use ReflectionAttribute;
use ReflectionClass;
use ReflectionFunction;
use ReflectionMethod;
use ReflectionParameter;
use ReflectionProperty;

use function class_exists;
use function function_exists;
use function is_array;
use function is_object;
use function is_string;
use function method_exists;
use function property_exists;

/** @phpstan-consistent-constructor */
class Attr implements Countable
{
/**
* Cached reflection of the target.
*/
protected ReflectionClass|ReflectionMethod|ReflectionFunction|ReflectionParameter|ReflectionProperty $reflection;

/**
* Create a new Attribute instance.
*
* @noinspection PhpUnhandledExceptionInspection
*/
public function __construct(mixed $target)
{
if (
$target instanceof ReflectionClass ||
$target instanceof ReflectionMethod ||
$target instanceof ReflectionFunction ||
$target instanceof ReflectionParameter ||
$target instanceof ReflectionProperty
) {
$this->reflection = $target;
} elseif (is_string($target)) {
if (function_exists($target)) {
$this->reflection = new ReflectionFunction($target);
} elseif (str_contains($target, '::')) {
$this->reflection = new ReflectionMethod($target);
} elseif (class_exists($target)) {
$this->reflection = new ReflectionClass($target);
}
} elseif (is_object($target)) {
if ($target instanceof Closure) {
$this->reflection = new ReflectionFunction($target);
} else {
$this->reflection = new ReflectionClass($target);
}
} elseif (is_array($target)) {
if (method_exists($target[0], $target[1])) {
$this->reflection = new ReflectionMethod($target[0], $target[1]);
}

if (property_exists($target[0], $target[1])) {
$this->reflection = new ReflectionProperty($target[0], $target[1]);
}
} else {
throw new InvalidArgumentException(
'The target must be a class, object, callable, or class-property array.'
);
}
}

/**
* Retrieve a collection of all Reflection Attributes.
*
* @param class-string|null $attribute
* @return \Illuminate\Support\Collection<int, \ReflectionAttribute>
*/
protected function collect(?string $attribute): Collection
{
return new Collection(
$this->reflection->getAttributes($attribute, ReflectionAttribute::IS_INSTANCEOF), // @phpstan-ignore-line
);
}

/**
* Retrieves all the instanced attributes of the target, optionally filtered by the given classes.
*
* @template TAttribute of object
*
* @param class-string<TAttribute>|null $attribute
* @return ($attribute is empty ? \Illuminate\Support\Collection<int, object> : \Illuminate\Support\Collection<int, TAttribute>)
*/
public function all(?string $attribute = null): Collection
{
return $this->collect($attribute)->map(static function (ReflectionAttribute $attribute): object {
return $attribute->newInstance();
});
}

/**
* Retrieves the first instanced attribute value from a class, method, or property.
*
* @template TAttribute of object
*
* @param class-string<TAttribute>|null $attribute
* @return TAttribute|null
*/
public function first(?string $attribute = null): ?object
{
return $this->collect($attribute)->first()?->newInstance();
}

/**
* Executes a method from the first instanced attribute.
*
* @template TAttribute of object
*
* @param class-string<TAttribute> $attribute
*/
public function call(string $attribute, string $method, mixed ...$arguments): mixed
{
return $this->first($attribute)?->{$method}(...$arguments);
}

/**
* Retrieves a value from the first instanced attribute.
*
* @template TAttribute of object
*
* @param class-string<TAttribute> $attribute
*/
public function get(string $attribute, string $property): mixed
{
return $this->first($attribute)?->$property;
}

/**
* @inheritDoc
*/
public function count(): int
{
return $this->collect(null)->count();
}

/**
* Creates a new Attributes instance for the given target.
*
* @param \Closure|object|class-string|callable-string|array{0: string|object, 1: string} $target
*/
public static function of(mixed $target): static
{
return new static($target);
}
}
17 changes: 17 additions & 0 deletions src/Attributes/RegisterRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace Laragear\Meta\Attributes;

use Attribute;

#[Attribute(Attribute::TARGET_METHOD)]
readonly class RegisterRule
{
/**
* Create a new Validation Key instance.
*/
public function __construct(public string $name, public ?string $translationKey = null, public bool $implicit = false)
{
//
}
}
112 changes: 107 additions & 5 deletions src/BootHelpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
use Illuminate\Contracts\Http\Kernel as KernelContract;
use Illuminate\Contracts\Validation\Factory;
use Illuminate\Routing\Router;
use Illuminate\Support\Str;
use Illuminate\View\Compilers\BladeCompiler;
use Laragear\Meta\Attributes\RegisterRule;
use Laragear\Meta\Http\Middleware\MiddlewareDeclaration;
use ReflectionClass;

use function array_fill;
use function array_fill_keys;
Expand All @@ -19,6 +23,13 @@

trait BootHelpers
{
/**
* Cached closures for registered validation rules.
*
* @var array<class-string, \Closure>
*/
protected static array $cachedValidationRules = [];

/**
* Extends a manager-like service.
*
Expand Down Expand Up @@ -53,7 +64,7 @@ protected function withValidationRule(
string $rule,
callable|string $callback,
callable|string|null $message = null,
bool $implicit = false
bool $implicit = false,
): void {
$this->callAfterResolving(
'validator',
Expand All @@ -63,7 +74,50 @@ static function (Factory $validator, Application $app) use ($message, $callback,
$implicit
? $validator->extendImplicit($rule, $callback, $message)
: $validator->extend($rule, $callback, $message);
}
},
);
}

/**
* Registers all Validation Rules found in a directory with a message from a translation prefix.
*
* @param class-string|class-string[] $classes
* @param string $keyPrefix If you register a translation key as "my-package", the validation
* rules will use "my-package::validation.{rule}".
*/
protected function withValidationRulesFrom(string|array $classes, string $keyPrefix): void
{
$this->callAfterResolving(
'validator',
static function (Factory $validator) use ($classes, $keyPrefix): void {
foreach ((array) $classes as $class) {
if (! isset(static::$cachedValidationRules[$class])) {
static::$cachedValidationRules[$class] = [];

foreach ((new ReflectionClass($class))->getMethods() as $method) {
/** @var \Laragear\Meta\Attributes\RegisterRule|null $attribute */
if (
$method->isPublic() && $method->isStatic() &&
$attribute = Attr::of($method)->first(RegisterRule::class)
) {
static::$cachedValidationRules[$class][$attribute->name] = [
$method->getClosure(),
"$keyPrefix::validation.".(
$attribute->translationKey ?: Str::snake($method->getName())
),
$attribute->implicit,
];
}
}
}

foreach (static::$cachedValidationRules[$class] as $name => [$callback, $message, $implicit]) {
$implicit
? $validator->extendImplicit($name, $callback, $message)
: $validator->extend($name, $callback, $message);
}
}
},
);
}

Expand All @@ -78,7 +132,7 @@ static function (Factory $validator, Application $app) use ($message, $callback,
protected function withMiddleware(string $class): MiddlewareDeclaration
{
return new MiddlewareDeclaration(
$this->app->make(Router::class), $this->app->make(KernelContract::class), $class
$this->app->make(Router::class), $this->app->make(KernelContract::class), $class,
);
}

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

$this->publishesMigrations(array_fill_keys(
$directories, array_fill(0, count($directories), $this->app->databasePath('migrations'))
$directories, array_fill(0, count($directories), $this->app->databasePath('migrations')),
), $groups);
}
}

/**
* Registers a simple Blade directive.
*
* @param array<string, callable>|string $name
* @param ($name is string ? callable : null) $handler
*/
protected function withBladeDirectives(string|array $name, ?callable $handler = null): void
{
$name = $handler ? [$name => $handler] : $name;

$this->callAfterResolving(
BladeCompiler::class,
static function (BladeCompiler $blade) use ($name): void {
foreach ($name as $key => $handler) {
$blade->directive($key, $handler);
}
},
);
}

/**
* Registers a directory of Blade components under a prefix.
*/
protected function withBladeComponents(string $path, string $prefix): void
{
$this->callAfterResolving(
BladeCompiler::class,
static function (BladeCompiler $blade) use ($path, $prefix): void {
$blade->componentNamespace($path, $prefix);
},
);
}

/**
* Returns the cached validation rules.
*/
public static function cachedValidationRules(): array
{
return static::$cachedValidationRules;
}

/**
* Flushes cached validation rules retrieved by reflection.
*/
public static function flushCachedValidationRules(): void
{
static::$cachedValidationRules = [];
}
}
1 change: 1 addition & 0 deletions stubs/components/stub-component.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<?php
Loading
Loading