Skip to content

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

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

Merged
merged 4 commits into from
Feb 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
------------------
Expand All @@ -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
------------------
Expand Down
60 changes: 34 additions & 26 deletions src/Console/ModelsCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'
)
Expand All @@ -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,
Expand Down Expand Up @@ -1192,21 +1186,36 @@ 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);

/** @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) {
Expand All @@ -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');
}

Expand Down Expand Up @@ -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;
Expand Down
76 changes: 75 additions & 1 deletion tests/Console/ModelsCommand/Attributes/Models/Simple.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,92 @@

class Simple extends Model
{
// With a backed property
protected function name(): Attribute
{
return new Attribute(
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -20,18 +28,92 @@
*/
class Simple extends Model
{
// With a backed property
protected function name(): Attribute
{
return new Attribute(
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;
Expand Down