Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: Set IDE Helper definitions using PHP 8 Attributes #1396

Open
JackWH opened this issue Nov 26, 2022 · 0 comments
Open

Proposal: Set IDE Helper definitions using PHP 8 Attributes #1396

JackWH opened this issue Nov 26, 2022 · 0 comments

Comments

@JackWH
Copy link

JackWH commented Nov 26, 2022

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 {
    // ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant