Description
Set IDE Helper properties using PHP 8 Attributes
Hi! 👋 There are several recent issues and PRs about the lack of support for Laravel's protected Attribute
return-type methods (see #1378 and #1339 for two examples).
I needed support for this in my app, but went about it in a slightly different way: by using PHP 8 Attributes to tell IDE Helper how to describe methods and properties. (...not to be confused with Laravel's Attributes!)
I haven't seen any other discussions about this approach. Assuming I've not missed anything, this feels like it could be a better or more flexible way to give developers more control over what IDE Helper produces.
The code below is specific to my implementation, but if any of the maintainers feel this could be expanded upon and included natively, I'll be happy to put a PR together. Or feel free to borrow the code for your own project 😄
1. The LaravelAttribute
class
I created a custom PHP Attribute called LaravelAttribute
, to annotate protected methods which set class properties that IDE Helper should recognise. It has sensible defaults set, but developers can customise how IDE Helper sets the property if they need to.
<?php
namespace App\Models\Hooks;
use Attribute;
use Barryvdh\LaravelIdeHelper\Console\ModelsCommand;
use Illuminate\Support\Str;
/** This attribute specifies the expected return type for a Laravel Attribute accessor. */
#[Attribute(Attribute::TARGET_METHOD)]
class LaravelAttribute
{
public function __construct(
public string|array|null $returnTypes,
public bool $get = true,
public bool $set = true,
public bool $nullable = true,
public ?string $comment = null,
) {
}
/** Automatically apply the Attribute's properties to an IDE Helper docblock. */
public function apply(ModelsCommand $command, string $methodName): void
{
$command->setProperty(
Str::of($methodName)->snake()->toString(),
collect($this->returnTypes)
->transform(function(string $type) {
$baseClass = Str::of($type)->before('<')->toString();
return (class_exists($baseClass) || interface_exists($baseClass))
&& (! Str::startsWith($baseClass, '\\')) ? ('\\' . $type) : $type;
})->join('|') ?: 'mixed',
$this->get,
$this->set,
$this->comment,
$this->nullable,
);
}
}
2. The ModelHooks
class
IDE Helper supports hooks, so I created a new hook class. It collects all protected methods in each Model class, and checks if they have the LaravelAttribute
attribute. If so, it passes the ModelsCommand
instance to the apply()
method of LaravelAttribute
.
<?php
namespace App\Models\Hooks;
use Barryvdh\LaravelIdeHelper\Console\ModelsCommand;
use Illuminate\Database\Eloquent\Model;
use ReflectionAttribute;
use ReflectionClass;
use ReflectionMethod;
class ModelHooks implements \Barryvdh\LaravelIdeHelper\Contracts\ModelHookInterface
{
/** Use reflection to find LaravelAttributes on class methods, then apply properties with IDE Helper. */
public function run(ModelsCommand $command, Model $model): void
{
collect(
(new ReflectionClass($model::class))->getMethods(ReflectionMethod::IS_PROTECTED)
)->mapWithKeys(fn(ReflectionMethod $method) => [
$method->getName() => collect($method->getAttributes(
LaravelAttribute::class,
ReflectionAttribute::IS_INSTANCEOF
))->transform(fn(ReflectionAttribute $attribute) => $attribute->newInstance())->first(),
])->filter()->each(
fn(LaravelAttribute $attribute, string $method) => $attribute->apply($command, $method)
);
}
}
Then registered it in the config/ide-helper.php
file:
// config/ide-helper.php:
'model_hooks' => [
\App\Models\Hooks\ModelHooks::class
],
3. Applying LaravelAttribute
to Model methods
With the above in place, I can now add LaravelAttribute
to any protected methods in my models. Here's an example for a User
. You can specify multiple return types too, and it'll concatenate them (e.g. ['string', 'int']
is treated as a string|int
union return type):
// app/Models/User.php:
/** Get the User's full name. */
#[LaravelAttribute('string', set: false, nullable: false)]
protected function name(): Attribute
{
return Attribute::make(
get: fn($value, $attributes) => $attributes['first_name'] . ' ' . $attributes['last_name'],
);
}
/** Get or update an Address for the User. */
#[LaravelAttribute(Address::class)]
protected function address(): Attribute
{
return Attribute::make(
get: fn($value, $attributes) => new Address($attributes['line_1'], $attributes['city']),
set: fn(Address $value) => [
'line_1' => $value->line_1,
'city' => $value->city,
],
);
}
/** Map the User's settings to a Collection. */
#[LaravelAttribute([Collection::class . '<string, bool>'], set: false)]
protected function settings(): Attribute
{
return Attribute::make(
get: fn($value, $attributes) => collect([
'dark_mode' => $attributes['dark_mode'],
'timezone' => $attributes['timezone'],
'2fa_enabled' => $attributes['2fa_enabled'],
])
)->shouldCache();
}
When IDE Helper runs, it will automatically apply properties based on the LaravelAttribute
annotations. None of these were previously recognised by IDE Helper, but have now been added to the docblock:
/**
* App\Auth\User
*
* ...
* @property-read string $name
* @property \App\Auth\Address|null $address
* @property-read \Illuminate\Support\Collection<string, bool>|null $settings
* ...
*/
class User extends Authenticatable {
// ...