From 29dd57e1e27540cefea914500fc6cca3ee33dbed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20J=C3=B8nsson?= Date: Mon, 3 Jan 2022 10:02:18 +0100 Subject: [PATCH] Support Laravel 8.77 Attributes (#1289) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ Set properties from model functions returning an Attribute * ✅ Add test for model Attributes * 🎨 Fix code style * 🔖 Update changelog * ✅ Update test to not require php 8 * 🐛 Fix PHP 7.3 incompatibility * ✅ Update tests to only run when Illuminate Attribute exists --- CHANGELOG.md | 3 ++ src/Console/ModelsCommand.php | 54 +++++++++++++++++-- .../Attributes/Models/Simple.php | 23 ++++++++ .../Console/ModelsCommand/Attributes/Test.php | 33 ++++++++++++ .../__snapshots__/Test__test__1.php | 34 ++++++++++++ 5 files changed, 142 insertions(+), 5 deletions(-) create mode 100644 tests/Console/ModelsCommand/Attributes/Models/Simple.php create mode 100644 tests/Console/ModelsCommand/Attributes/Test.php create mode 100644 tests/Console/ModelsCommand/Attributes/__snapshots__/Test__test__1.php diff --git a/CHANGELOG.md b/CHANGELOG.md index c95cf49..dcd8828 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file. [Next release](https://github.com/barryvdh/laravel-ide-helper/compare/v2.10.0...master) -------------- +### Added +- Add support for Laravel 8.77 Attributes [\#1289 / SimonJnsson](https://github.com/barryvdh/laravel-ide-helper/pull/1289) + ### Added - Add support for cast types `decimal:*`, `encrypted:*`, `immutable_date`, `immutable_datetime`, `custom_datetime`, and `immutable_custom_datetime` [#1262 / miken32](https://github.com/barryvdh/laravel-ide-helper/pull/1262) diff --git a/src/Console/ModelsCommand.php b/src/Console/ModelsCommand.php index c3c08c4..d7ad3f0 100644 --- a/src/Console/ModelsCommand.php +++ b/src/Console/ModelsCommand.php @@ -21,6 +21,7 @@ use Doctrine\DBAL\Exception as DBALException; use Doctrine\DBAL\Types\Type; use Illuminate\Console\Command; use Illuminate\Contracts\Database\Eloquent\CastsAttributes; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -35,6 +36,7 @@ use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Database\Eloquent\Relations\MorphToMany; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Filesystem\Filesystem; +use Illuminate\Support\Collection; use Illuminate\Support\Str; use phpDocumentor\Reflection\Types\ContextFactory; use ReflectionClass; @@ -559,6 +561,9 @@ class ModelsCommand extends Command if ($methods) { sort($methods); foreach ($methods as $method) { + $reflection = new \ReflectionMethod($model, $method); + $type = $this->getReturnType($reflection); + $isAttribute = is_a($type, '\Illuminate\Database\Eloquent\Casts\Attribute', true); if ( Str::startsWith($method, 'get') && Str::endsWith( $method, @@ -568,12 +573,25 @@ class ModelsCommand extends Command //Magic getAttribute $name = Str::snake(substr($method, 3, -9)); if (!empty($name)) { - $reflection = new \ReflectionMethod($model, $method); $type = $this->getReturnType($reflection); $type = $this->getTypeInModel($model, $type); $comment = $this->getCommentFromDocBlock($reflection); $this->setProperty($name, $type, true, null, $comment); } + } elseif ($isAttribute) { + $name = Str::snake($method); + $types = $this->getAttributeReturnType($model, $method); + + if ($types->has('get')) { + $type = $this->getTypeInModel($model, $types['get']); + $comment = $this->getCommentFromDocBlock($reflection); + $this->setProperty($name, $type, true, null, $comment); + } + + if ($types->has('set')) { + $comment = $this->getCommentFromDocBlock($reflection); + $this->setProperty($name, null, null, true, $comment); + } } elseif ( Str::startsWith($method, 'set') && Str::endsWith( $method, @@ -583,7 +601,6 @@ class ModelsCommand extends Command //Magic setAttribute $name = Str::snake(substr($method, 3, -9)); if (!empty($name)) { - $reflection = new \ReflectionMethod($model, $method); $comment = $this->getCommentFromDocBlock($reflection); $this->setProperty($name, null, null, true, $comment); } @@ -591,7 +608,6 @@ class ModelsCommand extends Command //Magic setAttribute $name = Str::camel(substr($method, 5)); if (!empty($name)) { - $reflection = new \ReflectionMethod($model, $method); $comment = $this->getCommentFromDocBlock($reflection); $args = $this->getParameters($reflection); //Remove the first ($query) argument @@ -622,8 +638,6 @@ class ModelsCommand extends Command && !Str::startsWith($method, 'get') ) { //Use reflection to inspect the code, based on Illuminate/Support/SerializableClosure.php - $reflection = new \ReflectionMethod($model, $method); - if ($returnType = $reflection->getReturnType()) { $type = $returnType instanceof ReflectionNamedType ? $returnType->getName() @@ -1056,6 +1070,36 @@ class ModelsCommand extends Command return $this->laravel['config']->get('ide-helper.model_camel_case_properties', false); } + protected function getAttributeReturnType(Model $model, string $method): Collection + { + /** @var Attribute $attribute */ + $attribute = $model->{$method}(); + + return collect([ + 'get' => $attribute->get ? optional(new \ReflectionFunction($attribute->get))->getReturnType() : null, + 'set' => $attribute->set ? optional(new \ReflectionFunction($attribute->set))->getReturnType() : null, + ]) + ->filter() + ->map(function ($type) { + if ($type instanceof \ReflectionUnionType) { + $types =collect($type->getTypes()) + /** @var ReflectionType $reflectionType */ + ->map(function ($reflectionType) { + return collect($this->extractReflectionTypes($reflectionType)); + }) + ->flatten(); + } else { + $types = collect($this->extractReflectionTypes($type)); + } + + if ($type->allowsNull()) { + $types->push('null'); + } + + return $types->join('|'); + }); + } + protected function getReturnType(\ReflectionMethod $reflection): ?string { $type = $this->getReturnTypeFromDocBlock($reflection); diff --git a/tests/Console/ModelsCommand/Attributes/Models/Simple.php b/tests/Console/ModelsCommand/Attributes/Models/Simple.php new file mode 100644 index 0000000..6457000 --- /dev/null +++ b/tests/Console/ModelsCommand/Attributes/Models/Simple.php @@ -0,0 +1,23 @@ +markTestSkipped('This test requires Laravel 8.77 or newer'); + } + } + + public function test(): void + { + $command = $this->app->make(ModelsCommand::class); + + $tester = $this->runCommand($command, [ + '--write' => true, + ]); + + $this->assertSame(0, $tester->getStatusCode()); + $this->assertStringContainsString('Written new phpDocBlock to', $tester->getDisplay()); + $this->assertMatchesMockedSnapshot(); + } +} diff --git a/tests/Console/ModelsCommand/Attributes/__snapshots__/Test__test__1.php b/tests/Console/ModelsCommand/Attributes/__snapshots__/Test__test__1.php new file mode 100644 index 0000000..7e8fcd4 --- /dev/null +++ b/tests/Console/ModelsCommand/Attributes/__snapshots__/Test__test__1.php @@ -0,0 +1,34 @@ +