Merge pull request #81 from psalm/polymorphic-support

feature: support for polymorphic relations
This commit is contained in:
feek 2020-06-09 10:42:57 -07:00 committed by GitHub
commit 6d863cda1f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 131 additions and 2 deletions

View File

@ -2,11 +2,17 @@
namespace Psalm\LaravelPlugin\PropertyProvider;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use PhpParser;
use Psalm\Context;
use Psalm\CodeLocation;
use Psalm\Type;
use Psalm\StatementsSource;
use function in_array;
use function str_replace;
class ModelPropertyProvider implements
@ -110,12 +116,20 @@ class ModelPropertyProvider implements
if (!$methodReturnType) {
return Type::getMixed();
}
/** @var \Psalm\Type\Union|null $modelType */
$modelType = null;
/** @var \Psalm\Type\Atomic\TGenericObject|null $relationType */
$relationType = null;
// In order to get the property value, we need to decipher the generic relation object
foreach ($methodReturnType->getAtomicTypes() as $atomicType) {
if (!$atomicType instanceof Type\Atomic\TGenericObject) {
continue;
}
$relationType = $atomicType;
foreach ($atomicType->getChildNodes() as $childNode) {
if (!$childNode instanceof Type\Union) {
continue;
@ -124,12 +138,25 @@ class ModelPropertyProvider implements
if (!$atomicType instanceof Type\Atomic\TNamedObject) {
continue;
}
return $childNode;
$modelType = $childNode;
break 3;
}
}
}
return Type::getMixed();
$returnType = $modelType;
// these methods return collection instances
if ($modelType && $relationType && in_array($relationType->value, [BelongsToMany::class, HasMany::class, HasManyThrough::class, MorphMany::class])) {
$returnType = new Type\Union([
new Type\Atomic\TGenericObject(Collection::class, [
$modelType
]),
]);
}
return $returnType ?: Type::getMixed();
}
if (self::accessorExists($codebase, $fq_classlike_name, $property_name)) {

View File

@ -0,0 +1,16 @@
<?php
namespace Illuminate\Database\Eloquent\Relations;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
/**
* @template TRelatedModel of Model
* @template-extends MorphOneOrMany<TRelatedModel>
* @mixin \Illuminate\Database\Eloquent\Builder<TRelatedModel>
*/
class MorphMany extends MorphOneOrMany
{
}

View File

@ -0,0 +1,16 @@
<?php
namespace Illuminate\Database\Eloquent\Relations;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
/**
* @template TRelatedModel of Model
* @template-extends HasOneOrMany<TRelatedModel>
* @mixin \Illuminate\Database\Eloquent\Builder<TRelatedModel>
*/
class MorphOneOrMany extends HasOneOrMany
{
}

View File

@ -4,6 +4,7 @@ namespace Tests\Psalm\LaravelPlugin\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;
final class Comment extends Model
{
@ -14,4 +15,12 @@ final class Comment extends Model
{
return $this->belongsTo(Post::class);
}
/**
* Get the owning commentable model.
*/
public function commentable(): MorphTo
{
return $this->morphTo();
}
}

18
tests/Models/Video.php Normal file
View File

@ -0,0 +1,18 @@
<?php declare(strict_types=1);
namespace Tests\Psalm\LaravelPlugin\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphMany;
final class Video extends Model
{
/**
* Get all of the video's comments.
* @psalm-return MorphMany<Comment>
*/
public function comments(): MorphMany
{
return $this->morphMany(Comment::class, 'commentable');
}
}

View File

@ -22,12 +22,14 @@ Feature: Eloquent Relation Types
use \Illuminate\Database\Eloquent\Builder;
use \Illuminate\Database\Eloquent\Model;
use \Illuminate\Database\Eloquent\Collection;
use \Illuminate\Database\Eloquent\Relations\HasOne;
use \Illuminate\Database\Eloquent\Relations\BelongsTo;
use \Illuminate\Database\Eloquent\Relations\BelongsToMany;
use \Illuminate\Database\Eloquent\Relations\HasMany;
use \Illuminate\Database\Eloquent\Relations\HasManyThrough;
use \Illuminate\Database\Eloquent\Relations\HasOneThrough;
use \Illuminate\Database\Eloquent\Relations\MorphMany;
use \Illuminate\Database\Eloquent\Relations\MorphTo;
use Tests\Psalm\LaravelPlugin\Models\Comment;
@ -37,6 +39,7 @@ Feature: Eloquent Relation Types
use Tests\Psalm\LaravelPlugin\Models\Post;
use Tests\Psalm\LaravelPlugin\Models\Role;
use Tests\Psalm\LaravelPlugin\Models\User;
use Tests\Psalm\LaravelPlugin\Models\Video;
"""
Scenario: Models can declare one to one relationships
@ -160,6 +163,46 @@ Feature: Eloquent Relation Types
When I run Psalm
Then I see no errors
Scenario: Models can declare many to many polymorphic relationships
Given I have the following code
"""
final class Repository
{
/**
* @psalm-return MorphMany<Comment>
*/
public function getCommentsRelation(Video $video): MorphMany {
return $video->comments();
}
/**
* @psalm-return Collection<Comment>
*/
public function getComments(Video $video): Collection {
return $video->comments;
}
}
"""
When I run Psalm
Then I see no errors
Scenario: Polymorphic models can retrieve their inverse relation
Given I have the following code
"""
final class Repository
{
/**
* todo: this should be a union of possible types...
* @psalm-return mixed
*/
public function getCommentable(Comment $comment) {
return $comment->commentable;
}
}
"""
When I run Psalm
Then I see no errors
Scenario: Relationships can be accessed via a property
Given I have the following code
"""