Add support for non type-hinted attribute accessors with no backed property (#1411)

* Read Attribute type from parameter

* Update CHANGELOG.md

* Update ModelsCommand.php

---------

Co-authored-by: Barry vd. Heuvel <[email protected]>
Co-authored-by: Barry vd. Heuvel <[email protected]>
This commit is contained in:
Hans van Luttikhuizen-Ross
2024-02-08 09:39:10 +01:00
committed by GitHub
co-authored by Barry vd. Heuvel Barry vd. Heuvel
parent 4751420c9e
commit f12d933ab1
4 changed files with 194 additions and 29 deletions
+2 -1
View File
@@ -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 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 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 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 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) - Refactor resolving of null information for custom casted attribute types [#1330 / wimski](https://github.com/barryvdh/laravel-ide-helper/pull/1330)
### Fixed ### 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) - Catch exceptions when loading aliases [#1465 / dongm2ez](https://github.com/barryvdh/laravel-ide-helper/pull/1465)
### Added ### 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 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 `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 2023-02-04, 2.13.0
------------------ ------------------
+34 -26
View File
@@ -620,10 +620,7 @@ class ModelsCommand extends Command
// methods that resemble mutators but aren't. // methods that resemble mutators but aren't.
$reflections = array_filter($reflections, function (\ReflectionMethod $methodReflection) { $reflections = array_filter($reflections, function (\ReflectionMethod $methodReflection) {
return !$methodReflection->isPrivate() && !( return !$methodReflection->isPrivate() && !(
in_array( $methodReflection->getDeclaringClass()->getName() === \Illuminate\Database\Eloquent\Model::class && (
\Illuminate\Database\Eloquent\Concerns\HasAttributes::class,
$methodReflection->getDeclaringClass()->getTraitNames()
) && (
$methodReflection->getName() === 'setClassCastableAttribute' || $methodReflection->getName() === 'setClassCastableAttribute' ||
$methodReflection->getName() === 'setEnumCastableAttribute' $methodReflection->getName() === 'setEnumCastableAttribute'
) )
@@ -649,18 +646,15 @@ class ModelsCommand extends Command
$this->setProperty($name, $type, true, null, $comment); $this->setProperty($name, $type, true, null, $comment);
} }
} elseif ($isAttribute) { } elseif ($isAttribute) {
$name = Str::snake($method); $types = $this->getAttributeTypes($model, $reflection);
$types = $this->getAttributeReturnType($model, $reflection); $type = $this->getTypeInModel($model, $types->get('get') ?: $types->get('set')) ?: null;
$comment = $this->getCommentFromDocBlock($reflection); $this->setProperty(
Str::snake($method),
if ($types->has('get')) { $type,
$type = $this->getTypeInModel($model, $types['get']); $types->has('get'),
$this->setProperty($name, $type, true, null, $comment); $types->has('set'),
} $this->getCommentFromDocBlock($reflection)
);
if ($types->has('set')) {
$this->setProperty($name, null, null, true, $comment);
}
} elseif ( } elseif (
Str::startsWith($method, 'set') && Str::endsWith( Str::startsWith($method, 'set') && Str::endsWith(
$method, $method,
@@ -1192,7 +1186,10 @@ class ModelsCommand extends Command
return $this->laravel['config']->get('ide-helper.model_camel_case_properties', false); 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 // Private/protected ReflectionMethods require setAccessible prior to PHP 8.1
$reflectionMethod->setAccessible(true); $reflectionMethod->setAccessible(true);
@@ -1200,13 +1197,25 @@ class ModelsCommand extends Command
/** @var Attribute $attribute */ /** @var Attribute $attribute */
$attribute = $reflectionMethod->invoke($model); $attribute = $reflectionMethod->invoke($model);
return collect([ $methods = new Collection();
'get' => $attribute->get ? optional(new \ReflectionFunction($attribute->get))->getReturnType() : null,
'set' => $attribute->set ? optional(new \ReflectionFunction($attribute->set))->getReturnType() : null, if ($attribute->get) {
]) $methods['get'] = optional(new \ReflectionFunction($attribute->get))->getReturnType();
->filter() }
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) { ->map(function ($type) {
if ($type instanceof \ReflectionUnionType) { if ($type === null) {
$types = collect([]);
} elseif ($type instanceof \ReflectionUnionType) {
$types = collect($type->getTypes()) $types = collect($type->getTypes())
/** @var ReflectionType $reflectionType */ /** @var ReflectionType $reflectionType */
->map(function ($reflectionType) { ->map(function ($reflectionType) {
@@ -1217,7 +1226,7 @@ class ModelsCommand extends Command
$types = collect($this->extractReflectionTypes($type)); $types = collect($this->extractReflectionTypes($type));
} }
if ($type->allowsNull()) { if ($type && $type->allowsNull()) {
$types->push('null'); $types->push('null');
} }
@@ -1467,8 +1476,7 @@ class ModelsCommand extends Command
{ {
$reflection = $model instanceof ReflectionClass $reflection = $model instanceof ReflectionClass
? $model ? $model
: new ReflectionObject($model) : new ReflectionObject($model);
;
$className = trim($className, '\\'); $className = trim($className, '\\');
$writingToExternalFile = !$this->write || $this->write_mixin; $writingToExternalFile = !$this->write || $this->write_mixin;
@@ -9,6 +9,7 @@ use Illuminate\Database\Eloquent\Model;
class Simple extends Model class Simple extends Model
{ {
// With a backed property
protected function name(): Attribute protected function name(): Attribute
{ {
return new Attribute( return new Attribute(
@@ -16,11 +17,84 @@ class Simple extends Model
return $name; return $name;
}, },
function (?string $name): ?string { 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 * ide-helper does not recognize this method being an Attribute
* because the method has no actual return type; * because the method has no actual return type;
@@ -11,7 +11,15 @@ use Illuminate\Database\Eloquent\Model;
* Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\Attributes\Models\Simple * Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\Attributes\Models\Simple
* *
* @property integer $id * @property integer $id
* @property int $diverging_type_hinted_get_and_set
* @property string|null $name * @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 newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|Simple newQuery() * @method static \Illuminate\Database\Eloquent\Builder|Simple newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|Simple query() * @method static \Illuminate\Database\Eloquent\Builder|Simple query()
@@ -20,6 +28,7 @@ use Illuminate\Database\Eloquent\Model;
*/ */
class Simple extends Model class Simple extends Model
{ {
// With a backed property
protected function name(): Attribute protected function name(): Attribute
{ {
return new Attribute( return new Attribute(
@@ -27,11 +36,84 @@ class Simple extends Model
return $name; return $name;
}, },
function (?string $name): ?string { 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 * ide-helper does not recognize this method being an Attribute
* because the method has no actual return type; * because the method has no actual return type;