diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a918604f..ca3bb91e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ All notable changes to this project will be documented in this file. - Add support for enum default arguments using enum cases. [#1464 / d8vjork](https://github.com/barryvdh/laravel-ide-helper/pull/1464) - Add support for real-time facades in the helper file. [#1455 / filipac](https://github.com/barryvdh/laravel-ide-helper/pull/1455) - Add support for relations with composite keys. [#1479 / calebdw](https://github.com/barryvdh/laravel-ide-helper/pull/1479) +- Add support for attribute accessors with no backing field or type hinting [#1411 / pindab0ter](https://github.com/barryvdh/laravel-ide-helper/pull/1411). 2024-02-05, 2.14.0 ------------------ @@ -24,12 +25,12 @@ All notable changes to this project will be documented in this file. - Refactor resolving of null information for custom casted attribute types [#1330 / wimski](https://github.com/barryvdh/laravel-ide-helper/pull/1330) ### Fixed -- Add support for attribute accessors marked as protected. [#1339 / pindab0ter](https://github.com/barryvdh/laravel-ide-helper/pull/1339) - Catch exceptions when loading aliases [#1465 / dongm2ez](https://github.com/barryvdh/laravel-ide-helper/pull/1465) ### Added - Add support for nikic/php-parser 5 (next to 4) [#1502 / mfn](https://github.com/barryvdh/laravel-ide-helper/pull/1502) - Add support for `immutable_date:*` and `immutable_datetime:*` casts. [#1380 / thekonz](https://github.com/barryvdh/laravel-ide-helper/pull/1380) +- Add support for attribute accessors marked as protected. [#1339 / pindab0ter](https://github.com/barryvdh/laravel-ide-helper/pull/1339) 2023-02-04, 2.13.0 ------------------ diff --git a/src/Console/ModelsCommand.php b/src/Console/ModelsCommand.php index b350caf7b..98991cdd7 100644 --- a/src/Console/ModelsCommand.php +++ b/src/Console/ModelsCommand.php @@ -620,10 +620,7 @@ public function getPropertiesFromMethods($model) // methods that resemble mutators but aren't. $reflections = array_filter($reflections, function (\ReflectionMethod $methodReflection) { return !$methodReflection->isPrivate() && !( - in_array( - \Illuminate\Database\Eloquent\Concerns\HasAttributes::class, - $methodReflection->getDeclaringClass()->getTraitNames() - ) && ( + $methodReflection->getDeclaringClass()->getName() === \Illuminate\Database\Eloquent\Model::class && ( $methodReflection->getName() === 'setClassCastableAttribute' || $methodReflection->getName() === 'setEnumCastableAttribute' ) @@ -649,18 +646,15 @@ public function getPropertiesFromMethods($model) $this->setProperty($name, $type, true, null, $comment); } } elseif ($isAttribute) { - $name = Str::snake($method); - $types = $this->getAttributeReturnType($model, $reflection); - $comment = $this->getCommentFromDocBlock($reflection); - - if ($types->has('get')) { - $type = $this->getTypeInModel($model, $types['get']); - $this->setProperty($name, $type, true, null, $comment); - } - - if ($types->has('set')) { - $this->setProperty($name, null, null, true, $comment); - } + $types = $this->getAttributeTypes($model, $reflection); + $type = $this->getTypeInModel($model, $types->get('get') ?: $types->get('set')) ?: null; + $this->setProperty( + Str::snake($method), + $type, + $types->has('get'), + $types->has('set'), + $this->getCommentFromDocBlock($reflection) + ); } elseif ( Str::startsWith($method, 'set') && Str::endsWith( $method, @@ -1192,7 +1186,10 @@ protected function hasCamelCaseModelProperties() return $this->laravel['config']->get('ide-helper.model_camel_case_properties', false); } - protected function getAttributeReturnType(Model $model, \ReflectionMethod $reflectionMethod): Collection + /** + * @psalm-suppress NoValue + */ + protected function getAttributeTypes(Model $model, \ReflectionMethod $reflectionMethod): Collection { // Private/protected ReflectionMethods require setAccessible prior to PHP 8.1 $reflectionMethod->setAccessible(true); @@ -1200,13 +1197,25 @@ protected function getAttributeReturnType(Model $model, \ReflectionMethod $refle /** @var Attribute $attribute */ $attribute = $reflectionMethod->invoke($model); - return collect([ - 'get' => $attribute->get ? optional(new \ReflectionFunction($attribute->get))->getReturnType() : null, - 'set' => $attribute->set ? optional(new \ReflectionFunction($attribute->set))->getReturnType() : null, - ]) - ->filter() + $methods = new Collection(); + + if ($attribute->get) { + $methods['get'] = optional(new \ReflectionFunction($attribute->get))->getReturnType(); + } + if ($attribute->set) { + $function = optional(new \ReflectionFunction($attribute->set)); + if ($function->getNumberOfParameters() === 0) { + $methods['set'] = null; + } else { + $methods['set'] = $function->getParameters()[0]->getType(); + } + } + + return $methods ->map(function ($type) { - if ($type instanceof \ReflectionUnionType) { + if ($type === null) { + $types = collect([]); + } elseif ($type instanceof \ReflectionUnionType) { $types = collect($type->getTypes()) /** @var ReflectionType $reflectionType */ ->map(function ($reflectionType) { @@ -1217,7 +1226,7 @@ protected function getAttributeReturnType(Model $model, \ReflectionMethod $refle $types = collect($this->extractReflectionTypes($type)); } - if ($type->allowsNull()) { + if ($type && $type->allowsNull()) { $types->push('null'); } @@ -1467,8 +1476,7 @@ protected function getClassNameInDestinationFile(object $model, string $classNam { $reflection = $model instanceof ReflectionClass ? $model - : new ReflectionObject($model) - ; + : new ReflectionObject($model); $className = trim($className, '\\'); $writingToExternalFile = !$this->write || $this->write_mixin; diff --git a/tests/Console/ModelsCommand/Attributes/Models/Simple.php b/tests/Console/ModelsCommand/Attributes/Models/Simple.php index d2b860920..56d1c7f9c 100644 --- a/tests/Console/ModelsCommand/Attributes/Models/Simple.php +++ b/tests/Console/ModelsCommand/Attributes/Models/Simple.php @@ -9,6 +9,7 @@ class Simple extends Model { + // With a backed property protected function name(): Attribute { return new Attribute( @@ -16,11 +17,84 @@ function (?string $name): ?string { return $name; }, function (?string $name): ?string { - return $name === null ? null : ucfirst($name); + return $name; + } + ); + } + + // Without backed properties + + protected function typeHintedGetAndSet(): Attribute + { + return new Attribute( + function (): ?string { + return $this->name; + }, + function (?string $name) { + $this->name = $name; + } + ); + } + + protected function divergingTypeHintedGetAndSet(): Attribute + { + return new Attribute( + function (): int { + return strlen($this->name); + }, + function (?string $name) { + $this->name = $name; + } + ); + } + + protected function typeHintedGet(): Attribute + { + return Attribute::get(function (): ?string { + return $this->name; + }); + } + + protected function typeHintedSet(): Attribute + { + return Attribute::set(function (?string $name) { + $this->name = $name; + }); + } + + protected function nonTypeHintedGetAndSet(): Attribute + { + return new Attribute( + function () { + return $this->name; + }, + function ($name) { + $this->name = $name; } ); } + protected function nonTypeHintedGet(): Attribute + { + return Attribute::get(function () { + return $this->name; + }); + } + + protected function nonTypeHintedSet(): Attribute + { + return Attribute::set(function ($name) { + $this->name = $name; + }); + } + + protected function parameterlessSet(): Attribute + { + return Attribute::set(function () { + $this->name = null; + }); + } + /** * ide-helper does not recognize this method being an Attribute * because the method has no actual return type; diff --git a/tests/Console/ModelsCommand/Attributes/__snapshots__/Test__test__1.php b/tests/Console/ModelsCommand/Attributes/__snapshots__/Test__test__1.php index ef96b279e..8cb89618f 100644 --- a/tests/Console/ModelsCommand/Attributes/__snapshots__/Test__test__1.php +++ b/tests/Console/ModelsCommand/Attributes/__snapshots__/Test__test__1.php @@ -11,7 +11,15 @@ * Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\Attributes\Models\Simple * * @property integer $id + * @property int $diverging_type_hinted_get_and_set * @property string|null $name + * @property-read mixed $non_type_hinted_get + * @property mixed $non_type_hinted_get_and_set + * @property-write mixed $non_type_hinted_set + * @property-write mixed $parameterless_set + * @property-read string|null $type_hinted_get + * @property string|null $type_hinted_get_and_set + * @property-write string|null $type_hinted_set * @method static \Illuminate\Database\Eloquent\Builder|Simple newModelQuery() * @method static \Illuminate\Database\Eloquent\Builder|Simple newQuery() * @method static \Illuminate\Database\Eloquent\Builder|Simple query() @@ -20,6 +28,7 @@ */ class Simple extends Model { + // With a backed property protected function name(): Attribute { return new Attribute( @@ -27,11 +36,84 @@ function (?string $name): ?string { return $name; }, function (?string $name): ?string { - return $name === null ? null : ucfirst($name); + return $name; + } + ); + } + + // Without backed properties + + protected function typeHintedGetAndSet(): Attribute + { + return new Attribute( + function (): ?string { + return $this->name; + }, + function (?string $name) { + $this->name = $name; + } + ); + } + + protected function divergingTypeHintedGetAndSet(): Attribute + { + return new Attribute( + function (): int { + return strlen($this->name); + }, + function (?string $name) { + $this->name = $name; + } + ); + } + + protected function typeHintedGet(): Attribute + { + return Attribute::get(function (): ?string { + return $this->name; + }); + } + + protected function typeHintedSet(): Attribute + { + return Attribute::set(function (?string $name) { + $this->name = $name; + }); + } + + protected function nonTypeHintedGetAndSet(): Attribute + { + return new Attribute( + function () { + return $this->name; + }, + function ($name) { + $this->name = $name; } ); } + protected function nonTypeHintedGet(): Attribute + { + return Attribute::get(function () { + return $this->name; + }); + } + + protected function nonTypeHintedSet(): Attribute + { + return Attribute::set(function ($name) { + $this->name = $name; + }); + } + + protected function parameterlessSet(): Attribute + { + return Attribute::set(function () { + $this->name = null; + }); + } + /** * ide-helper does not recognize this method being an Attribute * because the method has no actual return type;