Skip to content

Commit e38f4e8

Browse files
pindab0terbarryvdh
authored andcommitted
Add support for non type-hinted attribute accessors with no backed property (barryvdh#1411)
* Read Attribute type from parameter * Update CHANGELOG.md * Update ModelsCommand.php --------- Co-authored-by: Barry vd. Heuvel <barry@fruitcake.nl> Co-authored-by: Barry vd. Heuvel <barryvdh@gmail.com>
1 parent 17751ab commit e38f4e8

File tree

4 files changed

+194
-29
lines changed

4 files changed

+194
-29
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ All notable changes to this project will be documented in this file.
1515
- Add support for enum default arguments using enum cases. [#1464 / d8vjork](https://github.com/barryvdh/laravel-ide-helper/pull/1464)
1616
- Add support for real-time facades in the helper file. [#1455 / filipac](https://github.com/barryvdh/laravel-ide-helper/pull/1455)
1717
- Add support for relations with composite keys. [#1479 / calebdw](https://github.com/barryvdh/laravel-ide-helper/pull/1479)
18+
- Add support for attribute accessors with no backing field or type hinting [#1411 / pindab0ter](https://github.com/barryvdh/laravel-ide-helper/pull/1411).
1819

1920
2024-02-05, 2.14.0
2021
------------------
@@ -24,12 +25,12 @@ All notable changes to this project will be documented in this file.
2425
- Refactor resolving of null information for custom casted attribute types [#1330 / wimski](https://github.com/barryvdh/laravel-ide-helper/pull/1330)
2526

2627
### Fixed
27-
- Add support for attribute accessors marked as protected. [#1339 / pindab0ter](https://github.com/barryvdh/laravel-ide-helper/pull/1339)
2828
- Catch exceptions when loading aliases [#1465 / dongm2ez](https://github.com/barryvdh/laravel-ide-helper/pull/1465)
2929

3030
### Added
3131
- Add support for nikic/php-parser 5 (next to 4) [#1502 / mfn](https://github.com/barryvdh/laravel-ide-helper/pull/1502)
3232
- Add support for `immutable_date:*` and `immutable_datetime:*` casts. [#1380 / thekonz](https://github.com/barryvdh/laravel-ide-helper/pull/1380)
33+
- Add support for attribute accessors marked as protected. [#1339 / pindab0ter](https://github.com/barryvdh/laravel-ide-helper/pull/1339)
3334

3435
2023-02-04, 2.13.0
3536
------------------

src/Console/ModelsCommand.php

Lines changed: 34 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -620,10 +620,7 @@ public function getPropertiesFromMethods($model)
620620
// methods that resemble mutators but aren't.
621621
$reflections = array_filter($reflections, function (\ReflectionMethod $methodReflection) {
622622
return !$methodReflection->isPrivate() && !(
623-
in_array(
624-
\Illuminate\Database\Eloquent\Concerns\HasAttributes::class,
625-
$methodReflection->getDeclaringClass()->getTraitNames()
626-
) && (
623+
$methodReflection->getDeclaringClass()->getName() === \Illuminate\Database\Eloquent\Model::class && (
627624
$methodReflection->getName() === 'setClassCastableAttribute' ||
628625
$methodReflection->getName() === 'setEnumCastableAttribute'
629626
)
@@ -649,18 +646,15 @@ public function getPropertiesFromMethods($model)
649646
$this->setProperty($name, $type, true, null, $comment);
650647
}
651648
} elseif ($isAttribute) {
652-
$name = Str::snake($method);
653-
$types = $this->getAttributeReturnType($model, $reflection);
654-
$comment = $this->getCommentFromDocBlock($reflection);
655-
656-
if ($types->has('get')) {
657-
$type = $this->getTypeInModel($model, $types['get']);
658-
$this->setProperty($name, $type, true, null, $comment);
659-
}
660-
661-
if ($types->has('set')) {
662-
$this->setProperty($name, null, null, true, $comment);
663-
}
649+
$types = $this->getAttributeTypes($model, $reflection);
650+
$type = $this->getTypeInModel($model, $types->get('get') ?: $types->get('set')) ?: null;
651+
$this->setProperty(
652+
Str::snake($method),
653+
$type,
654+
$types->has('get'),
655+
$types->has('set'),
656+
$this->getCommentFromDocBlock($reflection)
657+
);
664658
} elseif (
665659
Str::startsWith($method, 'set') && Str::endsWith(
666660
$method,
@@ -1203,21 +1197,36 @@ protected function hasCamelCaseModelProperties()
12031197
return $this->laravel['config']->get('ide-helper.model_camel_case_properties', false);
12041198
}
12051199

1206-
protected function getAttributeReturnType(Model $model, \ReflectionMethod $reflectionMethod): Collection
1200+
/**
1201+
* @psalm-suppress NoValue
1202+
*/
1203+
protected function getAttributeTypes(Model $model, \ReflectionMethod $reflectionMethod): Collection
12071204
{
12081205
// Private/protected ReflectionMethods require setAccessible prior to PHP 8.1
12091206
$reflectionMethod->setAccessible(true);
12101207

12111208
/** @var Attribute $attribute */
12121209
$attribute = $reflectionMethod->invoke($model);
12131210

1214-
return collect([
1215-
'get' => $attribute->get ? optional(new \ReflectionFunction($attribute->get))->getReturnType() : null,
1216-
'set' => $attribute->set ? optional(new \ReflectionFunction($attribute->set))->getReturnType() : null,
1217-
])
1218-
->filter()
1211+
$methods = new Collection();
1212+
1213+
if ($attribute->get) {
1214+
$methods['get'] = optional(new \ReflectionFunction($attribute->get))->getReturnType();
1215+
}
1216+
if ($attribute->set) {
1217+
$function = optional(new \ReflectionFunction($attribute->set));
1218+
if ($function->getNumberOfParameters() === 0) {
1219+
$methods['set'] = null;
1220+
} else {
1221+
$methods['set'] = $function->getParameters()[0]->getType();
1222+
}
1223+
}
1224+
1225+
return $methods
12191226
->map(function ($type) {
1220-
if ($type instanceof \ReflectionUnionType) {
1227+
if ($type === null) {
1228+
$types = collect([]);
1229+
} elseif ($type instanceof \ReflectionUnionType) {
12211230
$types = collect($type->getTypes())
12221231
/** @var ReflectionType $reflectionType */
12231232
->map(function ($reflectionType) {
@@ -1228,7 +1237,7 @@ protected function getAttributeReturnType(Model $model, \ReflectionMethod $refle
12281237
$types = collect($this->extractReflectionTypes($type));
12291238
}
12301239

1231-
if ($type->allowsNull()) {
1240+
if ($type && $type->allowsNull()) {
12321241
$types->push('null');
12331242
}
12341243

@@ -1478,8 +1487,7 @@ protected function getClassNameInDestinationFile(object $model, string $classNam
14781487
{
14791488
$reflection = $model instanceof ReflectionClass
14801489
? $model
1481-
: new ReflectionObject($model)
1482-
;
1490+
: new ReflectionObject($model);
14831491

14841492
$className = trim($className, '\\');
14851493
$writingToExternalFile = !$this->write || $this->write_mixin;

tests/Console/ModelsCommand/Attributes/Models/Simple.php

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,92 @@
99

1010
class Simple extends Model
1111
{
12+
// With a backed property
1213
protected function name(): Attribute
1314
{
1415
return new Attribute(
1516
function (?string $name): ?string {
1617
return $name;
1718
},
1819
function (?string $name): ?string {
19-
return $name === null ? null : ucfirst($name);
20+
return $name;
21+
}
22+
);
23+
}
24+
25+
// Without backed properties
26+
27+
protected function typeHintedGetAndSet(): Attribute
28+
{
29+
return new Attribute(
30+
function (): ?string {
31+
return $this->name;
32+
},
33+
function (?string $name) {
34+
$this->name = $name;
35+
}
36+
);
37+
}
38+
39+
protected function divergingTypeHintedGetAndSet(): Attribute
40+
{
41+
return new Attribute(
42+
function (): int {
43+
return strlen($this->name);
44+
},
45+
function (?string $name) {
46+
$this->name = $name;
47+
}
48+
);
49+
}
50+
51+
protected function typeHintedGet(): Attribute
52+
{
53+
return Attribute::get(function (): ?string {
54+
return $this->name;
55+
});
56+
}
57+
58+
protected function typeHintedSet(): Attribute
59+
{
60+
return Attribute::set(function (?string $name) {
61+
$this->name = $name;
62+
});
63+
}
64+
65+
protected function nonTypeHintedGetAndSet(): Attribute
66+
{
67+
return new Attribute(
68+
function () {
69+
return $this->name;
70+
},
71+
function ($name) {
72+
$this->name = $name;
2073
}
2174
);
2275
}
2376

77+
protected function nonTypeHintedGet(): Attribute
78+
{
79+
return Attribute::get(function () {
80+
return $this->name;
81+
});
82+
}
83+
84+
protected function nonTypeHintedSet(): Attribute
85+
{
86+
return Attribute::set(function ($name) {
87+
$this->name = $name;
88+
});
89+
}
90+
91+
protected function parameterlessSet(): Attribute
92+
{
93+
return Attribute::set(function () {
94+
$this->name = null;
95+
});
96+
}
97+
2498
/**
2599
* ide-helper does not recognize this method being an Attribute
26100
* because the method has no actual return type;

tests/Console/ModelsCommand/Attributes/__snapshots__/Test__test__1.php

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,15 @@
1111
* Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\Attributes\Models\Simple
1212
*
1313
* @property integer $id
14+
* @property int $diverging_type_hinted_get_and_set
1415
* @property string|null $name
16+
* @property-read mixed $non_type_hinted_get
17+
* @property mixed $non_type_hinted_get_and_set
18+
* @property-write mixed $non_type_hinted_set
19+
* @property-write mixed $parameterless_set
20+
* @property-read string|null $type_hinted_get
21+
* @property string|null $type_hinted_get_and_set
22+
* @property-write string|null $type_hinted_set
1523
* @method static \Illuminate\Database\Eloquent\Builder|Simple newModelQuery()
1624
* @method static \Illuminate\Database\Eloquent\Builder|Simple newQuery()
1725
* @method static \Illuminate\Database\Eloquent\Builder|Simple query()
@@ -20,18 +28,92 @@
2028
*/
2129
class Simple extends Model
2230
{
31+
// With a backed property
2332
protected function name(): Attribute
2433
{
2534
return new Attribute(
2635
function (?string $name): ?string {
2736
return $name;
2837
},
2938
function (?string $name): ?string {
30-
return $name === null ? null : ucfirst($name);
39+
return $name;
40+
}
41+
);
42+
}
43+
44+
// Without backed properties
45+
46+
protected function typeHintedGetAndSet(): Attribute
47+
{
48+
return new Attribute(
49+
function (): ?string {
50+
return $this->name;
51+
},
52+
function (?string $name) {
53+
$this->name = $name;
54+
}
55+
);
56+
}
57+
58+
protected function divergingTypeHintedGetAndSet(): Attribute
59+
{
60+
return new Attribute(
61+
function (): int {
62+
return strlen($this->name);
63+
},
64+
function (?string $name) {
65+
$this->name = $name;
66+
}
67+
);
68+
}
69+
70+
protected function typeHintedGet(): Attribute
71+
{
72+
return Attribute::get(function (): ?string {
73+
return $this->name;
74+
});
75+
}
76+
77+
protected function typeHintedSet(): Attribute
78+
{
79+
return Attribute::set(function (?string $name) {
80+
$this->name = $name;
81+
});
82+
}
83+
84+
protected function nonTypeHintedGetAndSet(): Attribute
85+
{
86+
return new Attribute(
87+
function () {
88+
return $this->name;
89+
},
90+
function ($name) {
91+
$this->name = $name;
3192
}
3293
);
3394
}
3495

96+
protected function nonTypeHintedGet(): Attribute
97+
{
98+
return Attribute::get(function () {
99+
return $this->name;
100+
});
101+
}
102+
103+
protected function nonTypeHintedSet(): Attribute
104+
{
105+
return Attribute::set(function ($name) {
106+
$this->name = $name;
107+
});
108+
}
109+
110+
protected function parameterlessSet(): Attribute
111+
{
112+
return Attribute::set(function () {
113+
$this->name = null;
114+
});
115+
}
116+
35117
/**
36118
* ide-helper does not recognize this method being an Attribute
37119
* because the method has no actual return type;

0 commit comments

Comments
 (0)