初めに
FlowのRepositoryには、DBへのシンプルな問い合わせを定義するマジックメソッドが用意されています。これにより、簡単な問い合わせでメソッドを作成することなくDBアクセスができます。しかし、PHPStanではデフォルトでマジックメソッドを認識してくれません。
今回はFlowのRepositoryクラスのマジックメソッドをPHPStanに認識させる方法を紹介します。
Flowのマジックメソッド
FlowのRepositoryクラスにはDBからデータを取得するためのマジックメソッドがいくつか存在します。
- findBy~()
- findOneBy~ ()
- countBy~()
※~の部分にはドメインモデルのメンバ変数が入ります
findBy~
条件に合うエンティティを配列で取得するマジックメソッドです。
例えば、$name
がtest
という値のエンティティを取得したい場合、findByName('test')
のように書きます。
/**
* 例:メンバ変数$nameを持つHogeクラスがあるとする
*/
class Hoge
{
/**
* @ORM\Column(type="string")
* @var string
*/
protected $name;
}
// 取得したいドメインモデルに対応するRepositoryを用いる
$hogeRepository = new HogeRepository()
// Hogeのメンバ変数$nameが引数と等しいエンティティを全て取得する
$resultArray = $hogeRepository->findByName('test');
findOneBy~
findBy~の取得結果が1つだけバージョンです。条件に当てはまるエンティティが複数ある場合、一番最初のエンティティを取得します。
/**
* 例:メンバ変数$nameを持つHogeクラスがあるとする
*/
class Hoge
{
/**
* @ORM\Column(type="string")
* @var string
*/
protected $name;
}
// 取得したいドメインモデルに対応するRepositoryを用いる
$hogeRepository = new HogeRepository()
// Hogeのメンバ変数$nameが引数と等しいエンティティを一つだけ取得する
$result = $hogeRepository->findOneByName('test');
countBy~
前二つと同様の使い方で、条件に合う数を取得するマジックメソッドです。
/**
* 例:メンバ変数$nameを持つHogeクラスがあるとする
*/
class Hoge
{
/**
* @ORM\Column(type="string")
* @var string
*/
protected $name;
}
// 取得したいドメインモデルに対応するRepositoryを用いる
$hogeRepository = new HogeRepository()
// Hogeのメンバ変数$nameが引数と等しいエンティティの数を取得する
$resultCount = $hogeRepository->countBy('test');
PHPStanとマジックメソッド
これらのマジックメソッドは非常に便利です。
しかし、PHPStanはマジックメソッドを認識してくれません。Call to an undefined method
というエラーになります。
$result = $this->functionalRepository->findOneById('test');
> ./bin/phpstan analyse .\Packages\Application\Neos.Welcome\Classes\Controller\FunctionalTestController.php
Note: Using configuration file C:\Path\To\Project\phpstan.neon.
1/1 [============================] 100%
------ -------------------------------------------------------------------------------------------------
Line Packages/Application/Neos.Welcome/Classes/Controller/FunctionalTestController.php
------ ---------------------------------------------------------------------------------------
:62 Call to an undefined method Neos\Welcome\Domain\Repository\FunctionalRepository::findOneById().
------ ---------------------------------------------------------------------------------------
PHPStanにマジックメソッドを認識させる
ということでマジックメソッドを認識させましょう。
今回はMethodsClassReflectionExtension
という機能を用いてみます。
また、FlowはDoctrineを内包しているということもあるので、「PHPStanのdoctrineExtensionも一部利用します。
MethodsClassReflectionExtension
MethodsClassReflectionExtension
はメソッドを認識させるための拡張機能です。
hasMethod
にはPHPStanに認識させたいメソッドを定義します。
getMethod
にはhasMethod
で認識させたメソッドの定義(引数や戻り値など)を定義します。返却値のMethodReflection
がメソッドの定義を実装するためのInterfaceになります。
namespace PHPStan\Reflection;
interface MethodsClassReflectionExtension
{
public function hasMethod(ClassReflection $classReflection, string $methodName): bool;
public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection;
}
namespace PHPStan\Reflection;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Type;
interface MethodReflection
{
public function getDeclaringClass(): ClassReflection;
public function isStatic(): bool;
public function isPrivate(): bool;
public function isPublic(): bool;
public function getDocComment(): ?string;
public function getName(): string;
public function getPrototype(): ClassMemberReflection;
/**
* @return \PHPStan\Reflection\ParametersAcceptor[]
*/
public function getVariants(): array;
public function isDeprecated(): TrinaryLogic;
public function getDeprecatedDescription(): ?string;
public function isFinal(): TrinaryLogic;
public function isInternal(): TrinaryLogic;
public function getThrowType(): ?Type;
public function hasSideEffects(): TrinaryLogic;
}
Repositoryのマジックメソッドを読み込ませる
というわけで、拡張機能を作成してみました。
<?php declare(strict_types = 1);
namespace Neos\Welcome\PHPStan\Reflection;
use Neos\Flow\Persistence\Repository;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\MethodsClassReflectionExtension;
use PHPStan\Reflection\Doctrine\MagicRepositoryMethodReflection;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\ArrayType;
use PHPStan\Type\IntegerType;
use PHPStan\Type\NullType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\TypeCombinator;
use function lcfirst;
use function str_replace;
use function strlen;
use function strpos;
use function substr;
use function ucwords;
class RepositoriesMagicMethodClassReflectionExtension implements MethodsClassReflectionExtension
{
public function hasMethod(ClassReflection $classReflection, string $methodName): bool
{
// メソッド名をマジックメソッドに限定
if (
strpos($methodName, 'findBy') === 0
&& strlen($methodName) > strlen('findBy')
) {
$methodFieldName = substr($methodName, strlen('findBy'));
} elseif (
strpos($methodName, 'findOneBy') === 0
&& strlen($methodName) > strlen('findOneBy')
) {
$methodFieldName = substr($methodName, strlen('findOneBy'));
} elseif (
strpos($methodName, 'countBy') === 0
&& strlen($methodName) > strlen('countBy')
) {
$methodFieldName = substr($methodName, strlen('countBy'));
} else {
return false;
}
// Repositoryクラスを継承したクラスに限定
$repositoryAncesor = $classReflection->getAncestorWithClassName(Repository::class);
if ($repositoryAncesor === null) {
return false;
}
// Repository配下のクラスに限定
$repositoryClassName = $classReflection->getName();
if (!str_starts_with($repositoryClassName, 'Neos\\Welcome\\Domain\\Repository')) {
return false;
}
$fieldName = $this->classify($methodFieldName);
$repositoryClassName = $classReflection->getName();
// メソッド名のサフィックスがメンバ変数かどうか判定
$withoutRepository = preg_replace('/Repository$/', '', $repositoryClassName);
$domainModelClassName = str_replace('Repository', 'Model', $withoutRepository);
$reflection = new \ReflectionClass($domainModelClassName);
$properties = $reflection->getProperties();
foreach ($properties as $property) {
if ($property->getName() === $fieldName) {
return true;
};
}
return false;
}
private function classify(string $word): string
{
return lcfirst(str_replace([' ', '_', '-'], '', ucwords($word, ' _-')));
}
public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection
{
$repositoryAncesor = $classReflection->getAncestorWithClassName(Repository::class);
if ($repositoryAncesor === null) {
$repositoryAncesor = $classReflection->getAncestorWithClassName(Repository::class);
if ($repositoryAncesor === null) {
throw new ShouldNotHappenException();
}
}
$repositoryClassName = $classReflection->getName();
if ($repositoryClassName !== \Neos\Flow\Persistence\Repository::class) {
$withoutRepository = preg_replace('/Repository$/', '', $repositoryClassName);
$domainModelClassName = str_replace('Repository', 'Model', $withoutRepository);
}
$domainModelClassType = new ObjectType($domainModelClassName ?? $repositoryClassName);
if ($domainModelClassType === null) {
throw new ShouldNotHappenException();
}
if (
strpos($methodName, 'findBy') === 0
) {
$returnType = new ArrayType(new IntegerType(), $domainModelClassType);
} elseif (
strpos($methodName, 'findOneBy') === 0
) {
$returnType = TypeCombinator::union($domainModelClassType, new NullType());
} elseif (
strpos($methodName, 'countBy') === 0
) {
$returnType = new IntegerType();
} else {
throw new ShouldNotHappenException();
}
return new MagicRepositoryMethodReflection($classReflection, $methodName, $returnType);
}
}
MethodReflectionについてはPHPStanのdoctrine拡張内にあるMagicRepositoryMethodReflection
クラスを利用しました。
それでは実際に試してみましょう。
以下のコードに対して解析をかけてみます。
$resultArray = $this->functionalRepository->findById('test');
\PHPStan\dumpType($resultArray);
$result = $this->functionalRepository->findOneById('test');
\PHPStan\dumpType($result);
$resultCount = $this->functionalRepository->countById('test');
\PHPStan\dumpType($resultCount);
以下が解析結果です。
無事に解析ができました!
> ./bin/phpstan analyse .\Packages\Application\Neos.Welcome\Classes\Controller\FunctionalTestController.php
Note: Using configuration file C:\Path\To\Project\phpstan.neon.
1/1 [============================] 100%
------ --------------------------------------------------------------------------------------
Line Packages/Application/Neos.Welcome/Classes/Controller/FunctionalTestController.php
------ --------------------------------------------------------------------------------------
:63 Dumped type: array<int, Neos\Welcome\Domain\Model\Functional>
✏️ Packages/Application/Neos.Welcome/Classes/Controller/FunctionalTestController.php
:65 Dumped type: Neos\Welcome\Domain\Model\Functional|null
✏️ Packages/Application/Neos.Welcome/Classes/Controller/FunctionalTestController.php
:67 Dumped type: int
✏️ Packages/Application/Neos.Welcome/Classes/Controller/FunctionalTestController.php
------ --------------------------------------------------------------------------------------
参考
FlowはDoctrineを利用しているということもあり、このマジックメソッドについてもdoctrineと似たつくりになっています。今回実装した拡張機能も、PHPStanのDoctrine拡張に含まれる以下のクラスを参考に作成しました。
終わりに
今回はPHPStanでDoctrineExtensionを利用する方法を紹介しました。FlowはDoctrineを利用しているものの、PHPStanのDoctrine拡張をそのまま利用できるわけではなく、このような独自の拡張機能を作成する必要があります。今後も何か分かったことがあれば書こうと思います。
ここまでご覧いただきありがとうございました!