Skip to content

Commit 29dd57e

Browse files
authored
Support Laravel 8.77 Attributes (#1289)
* ✨ 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
1 parent 6244a93 commit 29dd57e

File tree

5 files changed

+142
-5
lines changed

5 files changed

+142
-5
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file.
44

55
[Next release](https://github.com/barryvdh/laravel-ide-helper/compare/v2.10.0...master)
66
--------------
7+
### Added
8+
- Add support for Laravel 8.77 Attributes [\#1289 / SimonJnsson](https://github.com/barryvdh/laravel-ide-helper/pull/1289)
9+
710
### Added
811
- 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)
912

src/Console/ModelsCommand.php

+49-5
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use Doctrine\DBAL\Types\Type;
2222
use Illuminate\Console\Command;
2323
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
24+
use Illuminate\Database\Eloquent\Casts\Attribute;
2425
use Illuminate\Database\Eloquent\Factories\Factory;
2526
use Illuminate\Database\Eloquent\Model;
2627
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -35,6 +36,7 @@
3536
use Illuminate\Database\Eloquent\Relations\MorphToMany;
3637
use Illuminate\Database\Eloquent\Relations\Relation;
3738
use Illuminate\Filesystem\Filesystem;
39+
use Illuminate\Support\Collection;
3840
use Illuminate\Support\Str;
3941
use phpDocumentor\Reflection\Types\ContextFactory;
4042
use ReflectionClass;
@@ -559,6 +561,9 @@ public function getPropertiesFromMethods($model)
559561
if ($methods) {
560562
sort($methods);
561563
foreach ($methods as $method) {
564+
$reflection = new \ReflectionMethod($model, $method);
565+
$type = $this->getReturnType($reflection);
566+
$isAttribute = is_a($type, '\Illuminate\Database\Eloquent\Casts\Attribute', true);
562567
if (
563568
Str::startsWith($method, 'get') && Str::endsWith(
564569
$method,
@@ -568,12 +573,25 @@ public function getPropertiesFromMethods($model)
568573
//Magic get<name>Attribute
569574
$name = Str::snake(substr($method, 3, -9));
570575
if (!empty($name)) {
571-
$reflection = new \ReflectionMethod($model, $method);
572576
$type = $this->getReturnType($reflection);
573577
$type = $this->getTypeInModel($model, $type);
574578
$comment = $this->getCommentFromDocBlock($reflection);
575579
$this->setProperty($name, $type, true, null, $comment);
576580
}
581+
} elseif ($isAttribute) {
582+
$name = Str::snake($method);
583+
$types = $this->getAttributeReturnType($model, $method);
584+
585+
if ($types->has('get')) {
586+
$type = $this->getTypeInModel($model, $types['get']);
587+
$comment = $this->getCommentFromDocBlock($reflection);
588+
$this->setProperty($name, $type, true, null, $comment);
589+
}
590+
591+
if ($types->has('set')) {
592+
$comment = $this->getCommentFromDocBlock($reflection);
593+
$this->setProperty($name, null, null, true, $comment);
594+
}
577595
} elseif (
578596
Str::startsWith($method, 'set') && Str::endsWith(
579597
$method,
@@ -583,15 +601,13 @@ public function getPropertiesFromMethods($model)
583601
//Magic set<name>Attribute
584602
$name = Str::snake(substr($method, 3, -9));
585603
if (!empty($name)) {
586-
$reflection = new \ReflectionMethod($model, $method);
587604
$comment = $this->getCommentFromDocBlock($reflection);
588605
$this->setProperty($name, null, null, true, $comment);
589606
}
590607
} elseif (Str::startsWith($method, 'scope') && $method !== 'scopeQuery') {
591608
//Magic set<name>Attribute
592609
$name = Str::camel(substr($method, 5));
593610
if (!empty($name)) {
594-
$reflection = new \ReflectionMethod($model, $method);
595611
$comment = $this->getCommentFromDocBlock($reflection);
596612
$args = $this->getParameters($reflection);
597613
//Remove the first ($query) argument
@@ -622,8 +638,6 @@ public function getPropertiesFromMethods($model)
622638
&& !Str::startsWith($method, 'get')
623639
) {
624640
//Use reflection to inspect the code, based on Illuminate/Support/SerializableClosure.php
625-
$reflection = new \ReflectionMethod($model, $method);
626-
627641
if ($returnType = $reflection->getReturnType()) {
628642
$type = $returnType instanceof ReflectionNamedType
629643
? $returnType->getName()
@@ -1056,6 +1070,36 @@ protected function hasCamelCaseModelProperties()
10561070
return $this->laravel['config']->get('ide-helper.model_camel_case_properties', false);
10571071
}
10581072

1073+
protected function getAttributeReturnType(Model $model, string $method): Collection
1074+
{
1075+
/** @var Attribute $attribute */
1076+
$attribute = $model->{$method}();
1077+
1078+
return collect([
1079+
'get' => $attribute->get ? optional(new \ReflectionFunction($attribute->get))->getReturnType() : null,
1080+
'set' => $attribute->set ? optional(new \ReflectionFunction($attribute->set))->getReturnType() : null,
1081+
])
1082+
->filter()
1083+
->map(function ($type) {
1084+
if ($type instanceof \ReflectionUnionType) {
1085+
$types =collect($type->getTypes())
1086+
/** @var ReflectionType $reflectionType */
1087+
->map(function ($reflectionType) {
1088+
return collect($this->extractReflectionTypes($reflectionType));
1089+
})
1090+
->flatten();
1091+
} else {
1092+
$types = collect($this->extractReflectionTypes($type));
1093+
}
1094+
1095+
if ($type->allowsNull()) {
1096+
$types->push('null');
1097+
}
1098+
1099+
return $types->join('|');
1100+
});
1101+
}
1102+
10591103
protected function getReturnType(\ReflectionMethod $reflection): ?string
10601104
{
10611105
$type = $this->getReturnTypeFromDocBlock($reflection);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\Attributes\Models;
6+
7+
use Illuminate\Database\Eloquent\Casts\Attribute;
8+
use Illuminate\Database\Eloquent\Model;
9+
10+
class Simple extends Model
11+
{
12+
public function name(): Attribute
13+
{
14+
return new Attribute(
15+
function (?string $name): ?string {
16+
return $name;
17+
},
18+
function (?string $name): ?string {
19+
return $name === null ? null : ucfirst($name);
20+
}
21+
);
22+
}
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\Attributes;
6+
7+
use Barryvdh\LaravelIdeHelper\Console\ModelsCommand;
8+
use Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\AbstractModelsCommand;
9+
10+
class Test extends AbstractModelsCommand
11+
{
12+
protected function setUp(): void
13+
{
14+
parent::setUp();
15+
16+
if (!class_exists('\Illuminate\Database\Eloquent\Casts\Attribute')) {
17+
$this->markTestSkipped('This test requires Laravel 8.77 or newer');
18+
}
19+
}
20+
21+
public function test(): void
22+
{
23+
$command = $this->app->make(ModelsCommand::class);
24+
25+
$tester = $this->runCommand($command, [
26+
'--write' => true,
27+
]);
28+
29+
$this->assertSame(0, $tester->getStatusCode());
30+
$this->assertStringContainsString('Written new phpDocBlock to', $tester->getDisplay());
31+
$this->assertMatchesMockedSnapshot();
32+
}
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\Attributes\Models;
6+
7+
use Illuminate\Database\Eloquent\Casts\Attribute;
8+
use Illuminate\Database\Eloquent\Model;
9+
10+
/**
11+
* Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\Attributes\Models\Simple
12+
*
13+
* @property integer $id
14+
* @property string|null $name
15+
* @method static \Illuminate\Database\Eloquent\Builder|Simple newModelQuery()
16+
* @method static \Illuminate\Database\Eloquent\Builder|Simple newQuery()
17+
* @method static \Illuminate\Database\Eloquent\Builder|Simple query()
18+
* @method static \Illuminate\Database\Eloquent\Builder|Simple whereId($value)
19+
* @mixin \Eloquent
20+
*/
21+
class Simple extends Model
22+
{
23+
public function name(): Attribute
24+
{
25+
return new Attribute(
26+
function (?string $name): ?string {
27+
return $name;
28+
},
29+
function (?string $name): ?string {
30+
return $name === null ? null : ucfirst($name);
31+
}
32+
);
33+
}
34+
}

0 commit comments

Comments
 (0)