Skip to content
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: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file.

[Next release](https://github.com/barryvdh/laravel-ide-helper/compare/v2.10.0...master)
--------------
### Added
- Add support for Laravel 8.77 Attributes [\#1289 / SimonJnsson](https://github.com/barryvdh/laravel-ide-helper/pull/1289)

### Added
- 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)

Expand Down
54 changes: 49 additions & 5 deletions src/Console/ModelsCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use Doctrine\DBAL\Types\Type;
use Illuminate\Console\Command;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
Expand All @@ -35,6 +36,7 @@
use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use phpDocumentor\Reflection\Types\ContextFactory;
use ReflectionClass;
Expand Down Expand Up @@ -559,6 +561,9 @@ public function getPropertiesFromMethods($model)
if ($methods) {
sort($methods);
foreach ($methods as $method) {
$reflection = new \ReflectionMethod($model, $method);
$type = $this->getReturnType($reflection);
$isAttribute = is_a($type, '\Illuminate\Database\Eloquent\Casts\Attribute', true);
if (
Str::startsWith($method, 'get') && Str::endsWith(
$method,
Expand All @@ -568,12 +573,25 @@ public function getPropertiesFromMethods($model)
//Magic get<name>Attribute
$name = Str::snake(substr($method, 3, -9));
if (!empty($name)) {
$reflection = new \ReflectionMethod($model, $method);
$type = $this->getReturnType($reflection);
$type = $this->getTypeInModel($model, $type);
$comment = $this->getCommentFromDocBlock($reflection);
$this->setProperty($name, $type, true, null, $comment);
}
} elseif ($isAttribute) {
$name = Str::snake($method);
$types = $this->getAttributeReturnType($model, $method);

if ($types->has('get')) {
$type = $this->getTypeInModel($model, $types['get']);
$comment = $this->getCommentFromDocBlock($reflection);
$this->setProperty($name, $type, true, null, $comment);
}

if ($types->has('set')) {
$comment = $this->getCommentFromDocBlock($reflection);
$this->setProperty($name, null, null, true, $comment);
}
} elseif (
Str::startsWith($method, 'set') && Str::endsWith(
$method,
Expand All @@ -583,15 +601,13 @@ public function getPropertiesFromMethods($model)
//Magic set<name>Attribute
$name = Str::snake(substr($method, 3, -9));
if (!empty($name)) {
$reflection = new \ReflectionMethod($model, $method);
$comment = $this->getCommentFromDocBlock($reflection);
$this->setProperty($name, null, null, true, $comment);
}
} elseif (Str::startsWith($method, 'scope') && $method !== 'scopeQuery') {
//Magic set<name>Attribute
$name = Str::camel(substr($method, 5));
if (!empty($name)) {
$reflection = new \ReflectionMethod($model, $method);
$comment = $this->getCommentFromDocBlock($reflection);
$args = $this->getParameters($reflection);
//Remove the first ($query) argument
Expand Down Expand Up @@ -622,8 +638,6 @@ public function getPropertiesFromMethods($model)
&& !Str::startsWith($method, 'get')
) {
//Use reflection to inspect the code, based on Illuminate/Support/SerializableClosure.php
$reflection = new \ReflectionMethod($model, $method);

if ($returnType = $reflection->getReturnType()) {
$type = $returnType instanceof ReflectionNamedType
? $returnType->getName()
Expand Down Expand Up @@ -1056,6 +1070,36 @@ protected function hasCamelCaseModelProperties()
return $this->laravel['config']->get('ide-helper.model_camel_case_properties', false);
}

protected function getAttributeReturnType(Model $model, string $method): Collection
{
/** @var Attribute $attribute */
$attribute = $model->{$method}();

return collect([
'get' => $attribute->get ? optional(new \ReflectionFunction($attribute->get))->getReturnType() : null,
'set' => $attribute->set ? optional(new \ReflectionFunction($attribute->set))->getReturnType() : null,
])
->filter()
->map(function ($type) {
if ($type instanceof \ReflectionUnionType) {
$types =collect($type->getTypes())
/** @var ReflectionType $reflectionType */
->map(function ($reflectionType) {
return collect($this->extractReflectionTypes($reflectionType));
})
->flatten();
} else {
$types = collect($this->extractReflectionTypes($type));
}

if ($type->allowsNull()) {
$types->push('null');
}

return $types->join('|');
});
}

protected function getReturnType(\ReflectionMethod $reflection): ?string
{
$type = $this->getReturnTypeFromDocBlock($reflection);
Expand Down
23 changes: 23 additions & 0 deletions tests/Console/ModelsCommand/Attributes/Models/Simple.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\Attributes\Models;

use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;

class Simple extends Model
{
public function name(): Attribute
{
return new Attribute(
function (?string $name): ?string {
return $name;
},
function (?string $name): ?string {
return $name === null ? null : ucfirst($name);
}
);
}
}
33 changes: 33 additions & 0 deletions tests/Console/ModelsCommand/Attributes/Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\Attributes;

use Barryvdh\LaravelIdeHelper\Console\ModelsCommand;
use Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\AbstractModelsCommand;

class Test extends AbstractModelsCommand
{
protected function setUp(): void
{
parent::setUp();

if (!class_exists('\Illuminate\Database\Eloquent\Casts\Attribute')) {
$this->markTestSkipped('This test requires Laravel 8.77 or newer');
}
}

public function test(): void
{
$command = $this->app->make(ModelsCommand::class);

$tester = $this->runCommand($command, [
'--write' => true,
]);

$this->assertSame(0, $tester->getStatusCode());
$this->assertStringContainsString('Written new phpDocBlock to', $tester->getDisplay());
$this->assertMatchesMockedSnapshot();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\Attributes\Models;

use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;

/**
* Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\Attributes\Models\Simple
*
* @property integer $id
* @property string|null $name
* @method static \Illuminate\Database\Eloquent\Builder|Simple newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|Simple newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|Simple query()
* @method static \Illuminate\Database\Eloquent\Builder|Simple whereId($value)
* @mixin \Eloquent
*/
class Simple extends Model
{
public function name(): Attribute
{
return new Attribute(
function (?string $name): ?string {
return $name;
},
function (?string $name): ?string {
return $name === null ? null : ucfirst($name);
}
);
}
}