0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【PHPフレームワークFlow】findByIdentifierの戻り値をPHPStanに認識させる

Posted at

初めに

PHPフレームワークFlowのfindByIdentifier()メソッドは動的に戻り値を変えるメソッドです。そのため、PHPStanの解析時に型の定義ができません。今回は、PHPStanの動的戻り値拡張を用いてこのメソッドに型を定義していきたいと思います。

前提

FlowはDoctrineORMを採用&拡張しており、エンティティからテーブルを自動生成できます。生成したテーブルにはpersistence_object_identifierというカラムが自動で作成され、これがテーブルの主キーになっています。

例えば、以下のようなクラスがあったとします。

<?php
namespace Neos\Welcome\Domain\Model;

use Neos\Flow\Annotations as Flow;
use Doctrine\ORM\Mapping as ORM;

/**
 * @Flow\Entity
 * @ORM\Table(name="item")
 */
class Item
{

    /**
     * @ORM\Column(type="string", name="item_name", length=20, nullable=false, unique=true)
     * @var string
     */
    protected $name;

    /**
     * @ORM\Column(type="string", length=100)
     * @var string
     */
    protected $description;

    /**
     * @ORM\Column(type="integer", nullable=false)
     * @var int
     */
    protected $price;

    public function __construct(string $name, string $description, int $price)
    {
        $this->name = $name;
        $this->description = $description;
        $this->price = $price;
    }

}

このクラスから以下のようなテーブルが自動生成されます。

mysql> show create table item\G
*************************** 1. row ***************************
       Table: item
Create Table: CREATE TABLE `item` (
  `persistence_object_identifier` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL,
  `item_name` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL,
  `description` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
  `price` int(11) NOT NULL,
  PRIMARY KEY (`persistence_object_identifier`),
  UNIQUE KEY `UNIQ_1F1B251E96133AFD` (`item_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
1 row in set (0.00 sec)

findByIdentifier()とは、UUIDからエンティティを取得するメソッド

findByIdentifier()はUUIDからエンティティを取得できるメソッドです。
このメソッドはRepositoryの親クラスにあるため、どのRepositoryからでも利用できます。

findByIdentifierの例
$entity = $this->itemlRepository->findByIdentifier('dummy');

findByIdentifier()は実行されるまで戻り値の型が分からない

前述の通り、findByIdentifier()は親Repositoryのメソッドのため、呼び出されるまでreturnの型を定義することができません。
以下のように、PHPDocは@return object|nullとなっています。

    /**
     * Finds an object matching the given identifier.
     *
     * @param string $identifier The identifier of the object to find
     * @return object|null The matching object if found, otherwise NULL
     * @api
     */
    public function findByIdentifier($identifier)
    {
        return $this->persistenceManager->getObjectByIdentifier($identifier, $this->entityClassName);
    }

動的戻り値拡張を使う

ということで、動的戻り値拡張を使って解決していきましょう。
動的戻り値拡張についてはこちらの記事で記載をしたので是非ご覧ください。

Flowでは、Repositoryとエンティティは同じ名前にしなければいけないというルールがあります。そして、RepositoryのfindByIdentifier()で取得する型は対応したエンティティになります。

そのため、呼び出しているRepositoryさえわかればfindByIdentifier()で取得する型の定義が可能ということです。

※エンティティとRepositoryの関係は以下をご参照ください

Classes/
   └ Domain/
        ├ Model
        |    └ Item.php(★)
        └ Repository
             └ ItemRepository.php(★)← ItemRepositoryではItem型のエンティティを取得する

ということで拡張機能を作ってみました。
Repository名からModelの型を定義しています。nullもあり得るので忘れずに定義。

<?php
namespace Neos\Welcome\PHPStan\Extension;

use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\NullType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;

class FindByIdentifierDynamicMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension
{
    public function getClass(): string
	{
		return \Neos\Flow\Persistence\Repository::class;
        // return \Neos\Welcome\Domain\Repository\FunctionalRepository::class;
	}
	public function isMethodSupported(MethodReflection $methodReflection): bool
	{
		return $methodReflection->getName() === 'findByIdentifier';
	}
	public function getTypeFromMethodCall(
		MethodReflection $methodReflection,
		MethodCall $methodCall,
		Scope $scope
	): ?Type
	{
        $repositoryClassName = $scope->getType($methodCall->var)->getClassName();

        if (!str_ends_with($repositoryClassName, 'Repository')) {
            return null;
        }

        $withoutRepository = preg_replace('/Repository$/', '', $repositoryClassName);
        $domainModelClassName = str_replace('Repository', 'Model', $withoutRepository);

		$types[] = new ObjectType($domainModelClassName);
		$types[] = new NullType();

		return TypeCombinator::union(...$types);
	}
}

動作確認のために以下のクラスを用意しました。
itemRepositoryを用いているので、結果がNeos\Welcome\Domain\Model\ItemクラスになっていればOKです。

解析対象(一部抜粋)
class Test
{
    public function findDetailAction(): void
    {
        $result = $this->itemlRepository->findByIdentifier('dummy');
        \PHPStan\dumpType($result);
    } 
}

解析結果が以下です。ちゃんと型が取得できました!

解析結果(一部抜粋)
$ ./bin/phpstan analyse
 1/1 [============================] 100%

 ------ ----------------------------------------------------------------------------
  Line   Packages/Application/Neos.Welcome/Classes/Util/Test.php
 ------ ----------------------------------------------------------------------------
  :43    Dumped type: Neos\Welcome\Domain\Model\Item|null
 ------ ----------------------------------------------------------------------------

終わりに

解析自体では扱うことができましたが、エディタ上での型補完は利きません。ひとまず一歩前進しましたが、型補完できる方法を探したいですね。
また何か学んだことがあれば記載します。
ここまで読んでいただきありがとうございました!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?