From 0b5d051b084cb20adeb9850823837ec80585278a Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Fri, 9 Jun 2023 10:07:37 +0200 Subject: [PATCH] chore: psalm plugin --- Makefile | 4 +- composer.json | 9 ++ phpstan-baseline.neon | 30 ----- phpstan.neon | 4 +- psalm.xml | 9 +- src/AnonymousFactory.php | 2 +- src/BaseFactory.php | 27 +++-- src/Bundle/Maker/Factory/FactoryGenerator.php | 15 --- src/Bundle/Maker/Factory/MakeFactoryData.php | 16 +-- .../Maker/Factory/MakeFactoryPHPDocMethod.php | 19 +--- src/Bundle/Resources/config/maker.xml | 1 - src/Bundle/Resources/skeleton/Factory.tpl.php | 8 -- src/FactoryCollection.php | 4 +- src/FactoryManager.php | 2 +- src/Instantiator.php | 2 +- src/Object/ObjectFactory.php | 11 +- src/Persistence/PersistentObjectFactory.php | 50 +++------ src/Psalm/FixAnonymousFunctions.php | 103 ++++++++++++++++++ src/Psalm/FixFactoryMethodsReturnType.php | 48 ++++++++ src/Psalm/FoundryPlugin.php | 24 ++++ src/Psalm/PsalmTypeHelper.php | 64 +++++++++++ src/Psalm/RemoveFactoryPhpDocMethods.php | 35 ++++++ src/functions.php | 4 +- tests/Fixtures/Factories/ContactFactory.php | 1 + tests/Fixtures/Factories/PostFactory.php | 1 + ..._with_repository_with_data_set_phpstan.php | 80 -------------- ...ty_with_repository_with_data_set_psalm.php | 80 -------------- ...ysis_annotations_with_data_set_phpstan.php | 77 ------------- ...alysis_annotations_with_data_set_psalm.php | 77 ------------- .../Migrations/Version20230513160346.php | 1 + tests/Fixtures/Object/SomeObjectFactory.php | 6 - .../Object/SomeOtherObjectFactory.php | 6 - tests/Fixtures/PhpStan/test-types.php | 8 ++ .../Psalm/test-types-with-persistence.php | 81 ++++++++++++++ .../Psalm/test-types-without-persistence.php | 62 +++++++++++ .../Bundle/Maker/MakeFactoryTest.php | 64 ----------- tests/Unit/ModelFactoryTest.php | 15 --- 37 files changed, 500 insertions(+), 550 deletions(-) create mode 100644 src/Psalm/FixAnonymousFunctions.php create mode 100644 src/Psalm/FixFactoryMethodsReturnType.php create mode 100644 src/Psalm/FoundryPlugin.php create mode 100644 src/Psalm/PsalmTypeHelper.php create mode 100644 src/Psalm/RemoveFactoryPhpDocMethods.php delete mode 100644 tests/Fixtures/Maker/expected/can_create_factory_for_entity_with_repository_with_data_set_phpstan.php delete mode 100644 tests/Fixtures/Maker/expected/can_create_factory_for_entity_with_repository_with_data_set_psalm.php delete mode 100644 tests/Fixtures/Maker/expected/can_create_factory_with_static_analysis_annotations_with_data_set_phpstan.php delete mode 100644 tests/Fixtures/Maker/expected/can_create_factory_with_static_analysis_annotations_with_data_set_psalm.php create mode 100644 tests/Fixtures/Psalm/test-types-with-persistence.php create mode 100644 tests/Fixtures/Psalm/test-types-without-persistence.php diff --git a/Makefile b/Makefile index d18961300..3899c9403 100644 --- a/Makefile +++ b/Makefile @@ -111,7 +111,7 @@ database-generate-migration: database-drop-schema ### Generate new migration bas .PHONY: database-validate-mapping database-validate-mapping: database-drop-schema ### Validate mapping in Zenstruck\Foundry\Tests\Fixtures\Entity @${DOCKER_PHP} vendor/bin/doctrine-migrations migrations:migrate --no-interaction --allow-no-migration - @${DOCKER_PHP} bin/doctrine orm:validate-schema + @${DOCKER_PHP} bin/doctrine orm:validate-schema -v .PHONY: database-drop-schema database-drop-schema: vendor ### Drop database schema @@ -150,7 +150,7 @@ docker-start: ### Start containers .PHONY: docker-stop docker-stop: ### Stop containers - @rm $(DOCKER_PHP_CONTAINER_FLAG) + @rm $(DOCKER_PHP_CONTAINER_FLAG) || true @$(DOCKER_COMPOSE) stop .PHONY: docker-purge diff --git a/composer.json b/composer.json index 182038551..c3be68182 100644 --- a/composer.json +++ b/composer.json @@ -22,6 +22,7 @@ "zenstruck/callback": "^1.1" }, "require-dev": { + "ext-simplexml": "*", "bamarni/composer-bin-plugin": "^1.4", "dama/doctrine-test-bundle": "^7.0", "doctrine/doctrine-bundle": "^2.5", @@ -63,6 +64,14 @@ "target-directory": "bin/tools", "bin-links": true, "forward-command": false + }, + "phpstan": { + "includes": [ + "phpstan-foundry.neon" + ] + }, + "psalm": { + "pluginClass": "Zenstruck\\Foundry\\Psalm\\FoundryPlugin" } }, "minimum-stability": "dev", diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index dc59f97bf..16afc70eb 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -5,16 +5,6 @@ parameters: count: 1 path: src/AnonymousFactory.php - - - message: "#^Method Zenstruck\\\\Foundry\\\\AnonymousFactory\\:\\:many\\(\\) should return Zenstruck\\\\Foundry\\\\FactoryCollection\\ but returns Zenstruck\\\\Foundry\\\\FactoryCollection\\\\.$#" - count: 1 - path: src/AnonymousFactory.php - - - - message: "#^Method Zenstruck\\\\Foundry\\\\AnonymousFactory\\:\\:sequence\\(\\) should return Zenstruck\\\\Foundry\\\\FactoryCollection\\ but returns Zenstruck\\\\Foundry\\\\FactoryCollection\\\\.$#" - count: 1 - path: src/AnonymousFactory.php - - message: "#^Parameter \\#1 \\$objectOrClass of class ReflectionClass constructor expects class\\-string\\\\|T of object, string\\|null given\\.$#" count: 1 @@ -89,23 +79,3 @@ parameters: message: "#^Parameter \\#2 \\$configuration of static method Zenstruck\\\\Foundry\\\\BaseFactory\\\\:\\:boot\\(\\) expects Zenstruck\\\\Foundry\\\\Configuration, object\\|null given\\.$#" count: 1 path: src/ZenstruckFoundryBundle.php - - - - message: "#^Function Zenstruck\\\\Foundry\\\\create\\(\\) should return \\(T of object\\)\\|Zenstruck\\\\Foundry\\\\Proxy\\ but returns object\\.$#" - count: 1 - path: src/functions.php - - - - message: "#^Function Zenstruck\\\\Foundry\\\\create_many\\(\\) should return array\\\\> but returns array\\\\.$#" - count: 1 - path: src/functions.php - - - - message: "#^Function Zenstruck\\\\Foundry\\\\instantiate\\(\\) should return \\(T of object\\)\\|Zenstruck\\\\Foundry\\\\Proxy\\ but returns object\\.$#" - count: 1 - path: src/functions.php - - - - message: "#^Function Zenstruck\\\\Foundry\\\\instantiate_many\\(\\) should return array\\\\> but returns array\\\\.$#" - count: 1 - path: src/functions.php diff --git a/phpstan.neon b/phpstan.neon index 2bbbdd2a3..dca0f65c6 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -11,8 +11,7 @@ parameters: - ./src # let's analyse factories generated with maker - - ./tests/Fixtures/Maker/expected/can_create_factory_for_entity_with_repository_with_data_set_phpstan.php - - ./tests/Fixtures/Maker/expected/can_create_factory_with_static_analysis_annotations_with_data_set_phpstan.php + - ./tests/Fixtures/Maker/expected/can_create_factory.php level: 8 bootstrapFiles: - ./vendor/autoload.php @@ -35,3 +34,4 @@ parameters: excludePaths: - ./src/Bundle/Resources - ./src/PhpStan + - ./src/Psalm diff --git a/psalm.xml b/psalm.xml index fe40b7965..daa24c6ea 100644 --- a/psalm.xml +++ b/psalm.xml @@ -7,9 +7,14 @@ xsi:schemaLocation="https://getpsalm.org/schema/config bin/tools/psalm/vendor/vimeo/psalm/config.xsd" findUnusedBaselineEntry="true" findUnusedCode="false" + autoloader="vendor/autoload.php" > - - + + + + + + diff --git a/src/AnonymousFactory.php b/src/AnonymousFactory.php index daad45d4e..4bf081c82 100644 --- a/src/AnonymousFactory.php +++ b/src/AnonymousFactory.php @@ -20,7 +20,7 @@ * * @deprecated * - * @phpstan-import-type SequenceAttributes from BaseFactory + * @psalm-import-type SequenceAttributes from BaseFactory */ final class AnonymousFactory implements \Countable, \IteratorAggregate { diff --git a/src/BaseFactory.php b/src/BaseFactory.php index 8d679727a..2ac6c71f0 100644 --- a/src/BaseFactory.php +++ b/src/BaseFactory.php @@ -21,11 +21,9 @@ * * @template T * - * @method static list createMany(int $number, Attributes $attributes = []) - * - * @phpstan-type Parameters array - * @phpstan-type Attributes Parameters|(callable():Parameters) - * @phpstan-type SequenceAttributes iterable|(callable(): iterable) + * @psalm-type Parameters = array + * @psalm-type Attributes = Parameters|(callable():Parameters) + * @psalm-type SequenceAttributes = iterable|callable(): iterable */ abstract class BaseFactory { @@ -51,15 +49,6 @@ public function __call(string $name, array $arguments): array return $this->many($arguments[0])->create($arguments[1] ?? []); } - public static function __callStatic(string $name, array $arguments): array - { - if ('createMany' !== $name) { - throw new \BadMethodCallException(\sprintf('Call to undefined static method "%s::%s".', static::class, $name)); - } - - return static::new()->many($arguments[0])->create($arguments[1] ?? []); - } - /** * @param Attributes|string $attributes */ @@ -117,6 +106,16 @@ final public static function createOne(array|callable $attributes = []): mixed */ abstract public function create(array|callable $attributes = []): mixed; + /** + * @param Attributes $attributes + * + * @return list + */ + public static function createMany(int $min, array|callable $attributes = []): array + { + return static::new()->many($min)->create($attributes); + } + /** * @param int|null $max If set, when created, the collection will be a random size between $min and $max * diff --git a/src/Bundle/Maker/Factory/FactoryGenerator.php b/src/Bundle/Maker/Factory/FactoryGenerator.php index c7c5f89f9..f51c1428f 100644 --- a/src/Bundle/Maker/Factory/FactoryGenerator.php +++ b/src/Bundle/Maker/Factory/FactoryGenerator.php @@ -19,7 +19,6 @@ use Symfony\Bundle\MakerBundle\Str; use Symfony\Component\Console\Question\Question; use Symfony\Component\Console\Style\SymfonyStyle; -use Symfony\Component\HttpKernel\KernelInterface; use Zenstruck\Foundry\Bundle\Maker\Factory\Exception\FactoryClassAlreadyExistException; /** @@ -27,13 +26,9 @@ */ final class FactoryGenerator { - public const PHPSTAN_PATH = '/vendor/phpstan/phpstan/phpstan'; - public const PSALM_PATH = '/vendor/vimeo/psalm/psalm'; - /** @param \Traversable $defaultPropertiesGuessers */ public function __construct( private ManagerRegistry $managerRegistry, - private KernelInterface $kernel, private \Traversable $defaultPropertiesGuessers, private FactoryClassMap $factoryClassMap, private NamespaceGuesser $namespaceGuesser, @@ -136,17 +131,7 @@ private function createMakeFactoryData(Generator $generator, string $class, Make $object, $factory, $repository ?? null, - $this->staticAnalysisTool(), $persisted ); } - - private function staticAnalysisTool(): string - { - return match (true) { - \file_exists($this->kernel->getProjectDir().self::PHPSTAN_PATH) => MakeFactoryData::STATIC_ANALYSIS_TOOL_PHPSTAN, - \file_exists($this->kernel->getProjectDir().self::PSALM_PATH) => MakeFactoryData::STATIC_ANALYSIS_TOOL_PSALM, - default => MakeFactoryData::STATIC_ANALYSIS_TOOL_NONE, - }; - } } diff --git a/src/Bundle/Maker/Factory/MakeFactoryData.php b/src/Bundle/Maker/Factory/MakeFactoryData.php index 3c29f6e56..98ac34040 100644 --- a/src/Bundle/Maker/Factory/MakeFactoryData.php +++ b/src/Bundle/Maker/Factory/MakeFactoryData.php @@ -22,10 +22,6 @@ */ final class MakeFactoryData { - public const STATIC_ANALYSIS_TOOL_NONE = 'none'; - public const STATIC_ANALYSIS_TOOL_PHPSTAN = 'phpstan'; - public const STATIC_ANALYSIS_TOOL_PSALM = 'psalm'; - /** @var list */ private array $uses; /** @var array */ @@ -33,7 +29,7 @@ final class MakeFactoryData /** @var non-empty-list */ private array $methodsInPHPDoc; - public function __construct(private \ReflectionClass $object, private ClassNameDetails $factoryClassNameDetails, private ?\ReflectionClass $repository, private string $staticAnalysisTool, private bool $persisted) + public function __construct(private \ReflectionClass $object, private ClassNameDetails $factoryClassNameDetails, private ?\ReflectionClass $repository, private bool $persisted) { $this->uses = [ PersistentObjectFactory::class, @@ -80,16 +76,6 @@ public function isPersisted(): bool return $this->persisted; } - public function hasStaticAnalysisTool(): bool - { - return self::STATIC_ANALYSIS_TOOL_NONE !== $this->staticAnalysisTool; - } - - public function staticAnalysisTool(): string - { - return $this->staticAnalysisTool; - } - /** @param class-string $use */ public function addUse(string $use): void { diff --git a/src/Bundle/Maker/Factory/MakeFactoryPHPDocMethod.php b/src/Bundle/Maker/Factory/MakeFactoryPHPDocMethod.php index 995155aeb..96cc2a906 100644 --- a/src/Bundle/Maker/Factory/MakeFactoryPHPDocMethod.php +++ b/src/Bundle/Maker/Factory/MakeFactoryPHPDocMethod.php @@ -52,27 +52,20 @@ public static function createAll(MakeFactoryData $makeFactoryData): array return $methods; } - public function toString(string|null $staticAnalysisTool = null): string + public function toString(): string { - $annotation = $staticAnalysisTool ? "{$staticAnalysisTool}-method" : 'method'; $static = $this->isStatic ? 'static' : ' '; if ($this->repository) { - $returnType = match ((bool) $staticAnalysisTool) { - false => "{$this->repository}|RepositoryProxy", - true => "RepositoryProxy<{$this->objectName}>", - }; + $returnType = "{$this->repository}|RepositoryProxy"; } else { - /** @phpstan-ignore-next-line */ - $returnType = match ([$this->returnsCollection, (bool) $staticAnalysisTool]) { - [true, true] => "listobjectName}>>", - [true, false] => "{$this->objectName}[]|Proxy[]", - [false, true] => "Proxy<{$this->objectName}>", - [false, false] => "{$this->objectName}|Proxy", + $returnType = match ($this->returnsCollection) { + true => "{$this->objectName}[]|Proxy[]", + false => "{$this->objectName}|Proxy", }; } - return " * @{$annotation} {$static} {$returnType} {$this->prototype}"; + return " * @method {$static} {$returnType} {$this->prototype}"; } public function sortValue(): string diff --git a/src/Bundle/Resources/config/maker.xml b/src/Bundle/Resources/config/maker.xml index 6e2bd98c5..4be822eeb 100644 --- a/src/Bundle/Resources/config/maker.xml +++ b/src/Bundle/Resources/config/maker.xml @@ -50,7 +50,6 @@ - diff --git a/src/Bundle/Resources/skeleton/Factory.tpl.php b/src/Bundle/Resources/skeleton/Factory.tpl.php index 778047285..a4080445f 100644 --- a/src/Bundle/Resources/skeleton/Factory.tpl.php +++ b/src/Bundle/Resources/skeleton/Factory.tpl.php @@ -15,14 +15,6 @@ foreach ($makeFactoryData->getMethodsPHPDoc() as $methodPHPDoc) { echo "{$methodPHPDoc->toString()}\n"; } - -//if ($makeFactoryData->hasStaticAnalysisTool()) { -// echo " *\n"; -// -// foreach ($makeFactoryData->getMethodsPHPDoc() as $methodPHPDoc) { -// echo "{$methodPHPDoc->toString($makeFactoryData->staticAnalysisTool())}\n"; -// } -//} ?> */ final class extends PersistentObjectFactory diff --git a/src/FactoryCollection.php b/src/FactoryCollection.php index 13777d710..f9cc26ee9 100644 --- a/src/FactoryCollection.php +++ b/src/FactoryCollection.php @@ -16,8 +16,8 @@ * * @author Kevin Bond * - * @phpstan-import-type Parameters from BaseFactory - * @phpstan-import-type Attributes from BaseFactory + * @psalm-import-type Parameters from BaseFactory + * @psalm-import-type Attributes from BaseFactory */ final class FactoryCollection implements \IteratorAggregate { diff --git a/src/FactoryManager.php b/src/FactoryManager.php index 680fd2d0b..b54ded9a3 100644 --- a/src/FactoryManager.php +++ b/src/FactoryManager.php @@ -19,7 +19,7 @@ * * @internal * - * @phpstan-import-type CallableInstantiator from ObjectFactory + * @psalm-import-type CallableInstantiator from ObjectFactory */ final class FactoryManager { diff --git a/src/Instantiator.php b/src/Instantiator.php index a868e206c..a06e16a03 100644 --- a/src/Instantiator.php +++ b/src/Instantiator.php @@ -21,7 +21,7 @@ * @author Kevin Bond * @template T of object * - * @phpstan-import-type Parameters from BaseFactory + * @psalm-import-type Parameters from BaseFactory */ final class Instantiator { diff --git a/src/Object/ObjectFactory.php b/src/Object/ObjectFactory.php index db917bf27..ad2cae3bf 100644 --- a/src/Object/ObjectFactory.php +++ b/src/Object/ObjectFactory.php @@ -21,10 +21,10 @@ * @template T of object * @extends BaseFactory * - * @phpstan-type CallableInstantiator Instantiator|\Closure(Parameters,class-string):T + * @psalm-type CallableInstantiator = Instantiator|\Closure(Parameters,class-string):T * - * @phpstan-import-type Parameters from BaseFactory - * @phpstan-import-type Attributes from BaseFactory + * @psalm-import-type Parameters from BaseFactory + * @psalm-import-type Attributes from BaseFactory */ abstract class ObjectFactory extends BaseFactory { @@ -42,6 +42,11 @@ abstract class ObjectFactory extends BaseFactory */ abstract public static function class(): string; + /** + * @param Attributes $attributes + * + * @return T + */ public function create(array|callable $attributes = []): object { return $this->normalizeAndInstantiate($attributes)[0]; diff --git a/src/Persistence/PersistentObjectFactory.php b/src/Persistence/PersistentObjectFactory.php index a19a4f359..b5df8f234 100644 --- a/src/Persistence/PersistentObjectFactory.php +++ b/src/Persistence/PersistentObjectFactory.php @@ -28,17 +28,10 @@ * @template T of object * @extends ObjectFactory * - * @phpstan-type Criteria Proxy|array|mixed + * @psalm-type Criteria = Proxy|array|mixed * - * @method static list createSequence(SequenceAttributes $sequence) - * @method static list createMany(int $number, Attributes $attributes = []) - * - * @phpstan-method static list> createSequence(SequenceAttributes $sequence) - * @phpstan-method static list> createMany(int $number, Attributes $attributes = []) - * - * @phpstan-import-type Parameters from BaseFactory - * @phpstan-import-type Attributes from BaseFactory - * @phpstan-import-type SequenceAttributes from BaseFactory + * @psalm-import-type Parameters from BaseFactory + * @psalm-import-type Attributes from BaseFactory */ abstract class PersistentObjectFactory extends ObjectFactory { @@ -96,8 +89,7 @@ final public static function persistenceManager(): PersistenceManager * * @phpstan-param Criteria $criteria * - * @return T&Proxy - * @phpstan-return Proxy + * @return Proxy * * @throws \RuntimeException If no entity found */ @@ -118,8 +110,7 @@ final public static function assert(): RepositoryAssertions /** * @param Parameters $attributes * - * @return T&Proxy - * @phpstan-return Proxy + * @return Proxy */ final public static function findOrCreate(array $attributes): object { @@ -134,8 +125,7 @@ final public static function findOrCreate(array $attributes): object } /** - * @return T&Proxy - * @phpstan-return Proxy + * @return Proxy */ final public static function first(string $sortedField = 'id'): object { @@ -147,8 +137,7 @@ final public static function first(string $sortedField = 'id'): object } /** - * @return T&Proxy - * @phpstan-return Proxy + * @return Proxy */ final public static function last(string $sortedField = 'id'): object { @@ -160,8 +149,7 @@ final public static function last(string $sortedField = 'id'): object } /** - * @return list - * @phpstan-return list> + * @return list> */ final public static function all(): array { @@ -171,8 +159,7 @@ final public static function all(): array /** * @param Parameters $attributes * - * @return list - * @phpstan-return list> + * @return list> */ final public static function findBy(array $attributes): array { @@ -182,8 +169,7 @@ final public static function findBy(array $attributes): array /** * @param Parameters $attributes * - * @return T&Proxy - * @phpstan-return Proxy + * @return Proxy */ final public static function random(array $attributes = []): object { @@ -193,8 +179,7 @@ final public static function random(array $attributes = []): object /** * @param Parameters $attributes * - * @return T&Proxy - * @phpstan-return Proxy + * @return Proxy */ final public static function randomOrCreate(array $attributes = []): object { @@ -208,8 +193,7 @@ final public static function randomOrCreate(array $attributes = []): object /** * @param Parameters $attributes * - * @return list - * @phpstan-return list> + * @return list> */ final public static function randomSet(int $number, array $attributes = []): array { @@ -219,8 +203,7 @@ final public static function randomSet(int $number, array $attributes = []): arr /** * @param Parameters $attributes * - * @return list - * @phpstan-return list> + * @return list> */ final public static function randomRange(int $min, int $max, array $attributes = []): array { @@ -241,7 +224,7 @@ final public static function truncate(): void } /** - * @phpstan-return RepositoryProxy + * @return RepositoryProxy */ final public static function repository(): RepositoryProxy { @@ -249,8 +232,9 @@ final public static function repository(): RepositoryProxy } /** - * @return T&Proxy - * @phpstan-return Proxy + * @param Attributes $attributes + * + * @return Proxy */ final public function create(array|callable $attributes = []): object { diff --git a/src/Psalm/FixAnonymousFunctions.php b/src/Psalm/FixAnonymousFunctions.php new file mode 100644 index 000000000..256c6d7b8 --- /dev/null +++ b/src/Psalm/FixAnonymousFunctions.php @@ -0,0 +1,103 @@ +getFunctionId()) { + 'zenstruck\\foundry\\anonymous' => self::getAnonymousReturnType($event), + 'zenstruck\\foundry\\create' => self::getCreateReturnType($event), + 'zenstruck\\foundry\\create_many' => self::getCreateManyReturnType($event), + default => null, + }; + } + + // anonymous(Entity::class) returns a FactoryCollection> + // anonymous(Object::class) returns a FactoryCollection + private static function getAnonymousReturnType(FunctionReturnTypeProviderEvent $event): ?Union + { + $targetClass = PsalmTypeHelper::resolveFactoryTargetClass($event->getCallArgs()[0] ?? null); + + $factoryClass = match (PsalmTypeHelper::isFactoryTargetClassPersisted($targetClass)) { + true => PersistentObjectFactory::class, + false => ObjectFactory::class, + null => null + }; + + if (!$factoryClass) { + return null; + } + + return PsalmTypeHelper::genericType($factoryClass, $targetClass); + } + + // create(Entity::class) returns a Proxy + // create(Object::class) returns a Object + private static function getCreateReturnType(FunctionReturnTypeProviderEvent $event): ?Union + { + $targetClass = PsalmTypeHelper::resolveFactoryTargetClass($event->getCallArgs()[0] ?? null); + + return match (PsalmTypeHelper::isFactoryTargetClassPersisted($targetClass)) { + true => PsalmTypeHelper::genericType(Proxy::class, $targetClass), + false => PsalmTypeHelper::classType($targetClass), + null => null + }; + } + + // create(Entity::class) returns a list> + // create(Object::class) returns a list + private static function getCreateManyReturnType(FunctionReturnTypeProviderEvent $event): ?Union + { + $targetClass = PsalmTypeHelper::resolveFactoryTargetClass($event->getCallArgs()[1] ?? null); + + $createdObjectType = match (PsalmTypeHelper::isFactoryTargetClassPersisted($targetClass)) { + true => PsalmTypeHelper::genericType(Proxy::class, $targetClass), + false => PsalmTypeHelper::classType($targetClass), + null => null + }; + + if (!$createdObjectType) { + return null; + } + + return new \Psalm\Type\Union([new \Psalm\Type\Atomic\TKeyedArray([$createdObjectType], is_list: true)]); + } + + public static function getClassLikeNames(): array + { + return [PersistentObjectFactory::class]; + } + + // anonymous(Entity::class)->many() returns a FactoryCollection> + // anonymous(Entity::class)->sequence() returns a FactoryCollection> + public static function getMethodReturnType(MethodReturnTypeProviderEvent $event): ?Union + { + if (!\in_array($event->getMethodNameLowercase(), ['many', 'sequence'], true)) { + return null; + } + + return PsalmTypeHelper::factoryCollection($event->getTemplateTypeParameters()[0]); + } +} diff --git a/src/Psalm/FixFactoryMethodsReturnType.php b/src/Psalm/FixFactoryMethodsReturnType.php new file mode 100644 index 000000000..a4ef3a153 --- /dev/null +++ b/src/Psalm/FixFactoryMethodsReturnType.php @@ -0,0 +1,48 @@ +getMethodId()); + + // PersistentObjectFactory::createOne() returns a list> + // ObjectFactory::createOne() returns a T + if (is_subclass_of($class, ObjectFactory::class) && 'createone' === $method) { + $templateType = $event->getCodebase()->classlikes->getStorageFor($class)->template_extended_params[ObjectFactory::class]['T'] ?? null; + + if (!$templateType) { + return; + } + + $event->setReturnTypeCandidate( + match(is_subclass_of($class, PersistentObjectFactory::class)){ + true => PsalmTypeHelper::genericTypeFromUnionType(Proxy::class, $templateType), + false => $templateType + } + ); + } + + // PersistentObjectFactory->many() returns a FactoryCollection> + if (is_subclass_of($class, PersistentObjectFactory::class) && (\in_array($method, ['many', 'sequence'], true))) { + $templateType = $event->getCodebase()->classlikes->getStorageFor($class)->template_extended_params[PersistentObjectFactory::class]['T'] ?? null; + + if (!$templateType) { + return; + } + + $event->setReturnTypeCandidate(PsalmTypeHelper::factoryCollection($templateType)); + } + } +} diff --git a/src/Psalm/FoundryPlugin.php b/src/Psalm/FoundryPlugin.php new file mode 100644 index 000000000..c66ad0ddc --- /dev/null +++ b/src/Psalm/FoundryPlugin.php @@ -0,0 +1,24 @@ +registerHooksFromClass(RemoveFactoryPhpDocMethods::class); + + class_exists(FixFactoryMethodsReturnType::class, true); + $psalm->registerHooksFromClass(FixFactoryMethodsReturnType::class); + + class_exists(FixAnonymousFunctions::class, true); + $psalm->registerHooksFromClass(FixAnonymousFunctions::class); + } +} diff --git a/src/Psalm/PsalmTypeHelper.php b/src/Psalm/PsalmTypeHelper.php new file mode 100644 index 000000000..430b64596 --- /dev/null +++ b/src/Psalm/PsalmTypeHelper.php @@ -0,0 +1,64 @@ +value instanceof ClassConstFetch) { + return null; + } + + return $arg->value->class->getAttributes()['resolvedName']; + } + + public static function isFactoryTargetClassPersisted(string|null $factoryTargetClass): ?bool + { + if (!$factoryTargetClass) { + return null; + } + + $reflectionClass = new \ReflectionClass($factoryTargetClass); + + return $reflectionClass->getAttributes(Entity::class) || $reflectionClass->getAttributes(Document::class); + } +} diff --git a/src/Psalm/RemoveFactoryPhpDocMethods.php b/src/Psalm/RemoveFactoryPhpDocMethods.php new file mode 100644 index 000000000..063721e4e --- /dev/null +++ b/src/Psalm/RemoveFactoryPhpDocMethods.php @@ -0,0 +1,35 @@ +getStorage(); + + if ($classLikeStorage->parent_class === PersistentObjectFactory::class + || is_subclass_of($classLikeStorage->name, PersistentObjectFactory::class)) { + foreach (array_keys($classLikeStorage->pseudo_methods) as $name) { + if (method_exists(PersistentObjectFactory::class, $name)) { + unset($classLikeStorage->pseudo_methods[$name]); + } + } + + foreach (array_keys($classLikeStorage->pseudo_static_methods) as $name) { + if (method_exists(PersistentObjectFactory::class, $name)) { + unset($classLikeStorage->pseudo_static_methods[$name]); + } + } + } + } +} diff --git a/src/functions.php b/src/functions.php index b86cc9d99..81ca86cff 100644 --- a/src/functions.php +++ b/src/functions.php @@ -40,7 +40,7 @@ function factory(string $class, array|\Closure $defaultAttributes = []): Anonymo * * @param class-string $class * - * @return (ObjectFactory|PersistentObjectFactory) + * @return ObjectFactory|PersistentObjectFactory */ function anonymous(string $class, array|callable $defaultAttributes = []): ObjectFactory|PersistentObjectFactory { @@ -83,7 +83,7 @@ protected function getDefaults(): array /** * @see BaseFactory::create() * - * @return (Proxy&T)|T + * @return (Proxy)|T * * @template T of object * @phpstan-param class-string $class diff --git a/tests/Fixtures/Factories/ContactFactory.php b/tests/Fixtures/Factories/ContactFactory.php index 16ba05234..035a61d49 100644 --- a/tests/Fixtures/Factories/ContactFactory.php +++ b/tests/Fixtures/Factories/ContactFactory.php @@ -7,6 +7,7 @@ /** * @author Kevin Bond + * @extends PersistentObjectFactory */ final class ContactFactory extends PersistentObjectFactory { diff --git a/tests/Fixtures/Factories/PostFactory.php b/tests/Fixtures/Factories/PostFactory.php index 16cfca76a..594ca13ec 100644 --- a/tests/Fixtures/Factories/PostFactory.php +++ b/tests/Fixtures/Factories/PostFactory.php @@ -8,6 +8,7 @@ /** * @author Kevin Bond + * @extends PersistentObjectFactory */ class PostFactory extends PersistentObjectFactory { diff --git a/tests/Fixtures/Maker/expected/can_create_factory_for_entity_with_repository_with_data_set_phpstan.php b/tests/Fixtures/Maker/expected/can_create_factory_for_entity_with_repository_with_data_set_phpstan.php deleted file mode 100644 index 0928c60ad..000000000 --- a/tests/Fixtures/Maker/expected/can_create_factory_for_entity_with_repository_with_data_set_phpstan.php +++ /dev/null @@ -1,80 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace App\Factory; - -use Zenstruck\Foundry\Persistence\PersistentObjectFactory; -use Zenstruck\Foundry\Proxy; -use Zenstruck\Foundry\RepositoryProxy; -use Zenstruck\Foundry\Tests\Fixtures\Entity\Post; -use Zenstruck\Foundry\Tests\Fixtures\Repository\PostRepository; - -/** - * @extends PersistentObjectFactory - * - * @method Post|Proxy create(array|callable $attributes = []) - * @method static Post|Proxy createOne(array $attributes = []) - * @method static Post|Proxy find(object|array|mixed $criteria) - * @method static Post|Proxy findOrCreate(array $attributes) - * @method static Post|Proxy first(string $sortedField = 'id') - * @method static Post|Proxy last(string $sortedField = 'id') - * @method static Post|Proxy random(array $attributes = []) - * @method static Post|Proxy randomOrCreate(array $attributes = []) - * @method static PostRepository|RepositoryProxy repository() - * @method static Post[]|Proxy[] all() - * @method static Post[]|Proxy[] createMany(int $number, array|callable $attributes = []) - * @method static Post[]|Proxy[] createSequence(iterable|callable $sequence) - * @method static Post[]|Proxy[] findBy(array $attributes) - * @method static Post[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) - * @method static Post[]|Proxy[] randomSet(int $number, array $attributes = []) - */ -final class PostFactory extends PersistentObjectFactory -{ - /** - * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services - * - * @todo inject services if required - */ - public function __construct() - { - parent::__construct(); - } - - public static function class(): string - { - return Post::class; - } - - /** - * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#model-factories - * - * @todo add your default values here - */ - protected function getDefaults(): array - { - return [ - 'body' => self::faker()->text(), - 'createdAt' => self::faker()->dateTime(), - 'title' => self::faker()->text(255), - 'viewCount' => self::faker()->randomNumber(), - ]; - } - - /** - * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization - */ - protected function initialize(): self - { - return $this - // ->afterInstantiate(function(Post $post): void {}) - ; - } -} diff --git a/tests/Fixtures/Maker/expected/can_create_factory_for_entity_with_repository_with_data_set_psalm.php b/tests/Fixtures/Maker/expected/can_create_factory_for_entity_with_repository_with_data_set_psalm.php deleted file mode 100644 index 0928c60ad..000000000 --- a/tests/Fixtures/Maker/expected/can_create_factory_for_entity_with_repository_with_data_set_psalm.php +++ /dev/null @@ -1,80 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace App\Factory; - -use Zenstruck\Foundry\Persistence\PersistentObjectFactory; -use Zenstruck\Foundry\Proxy; -use Zenstruck\Foundry\RepositoryProxy; -use Zenstruck\Foundry\Tests\Fixtures\Entity\Post; -use Zenstruck\Foundry\Tests\Fixtures\Repository\PostRepository; - -/** - * @extends PersistentObjectFactory - * - * @method Post|Proxy create(array|callable $attributes = []) - * @method static Post|Proxy createOne(array $attributes = []) - * @method static Post|Proxy find(object|array|mixed $criteria) - * @method static Post|Proxy findOrCreate(array $attributes) - * @method static Post|Proxy first(string $sortedField = 'id') - * @method static Post|Proxy last(string $sortedField = 'id') - * @method static Post|Proxy random(array $attributes = []) - * @method static Post|Proxy randomOrCreate(array $attributes = []) - * @method static PostRepository|RepositoryProxy repository() - * @method static Post[]|Proxy[] all() - * @method static Post[]|Proxy[] createMany(int $number, array|callable $attributes = []) - * @method static Post[]|Proxy[] createSequence(iterable|callable $sequence) - * @method static Post[]|Proxy[] findBy(array $attributes) - * @method static Post[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) - * @method static Post[]|Proxy[] randomSet(int $number, array $attributes = []) - */ -final class PostFactory extends PersistentObjectFactory -{ - /** - * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services - * - * @todo inject services if required - */ - public function __construct() - { - parent::__construct(); - } - - public static function class(): string - { - return Post::class; - } - - /** - * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#model-factories - * - * @todo add your default values here - */ - protected function getDefaults(): array - { - return [ - 'body' => self::faker()->text(), - 'createdAt' => self::faker()->dateTime(), - 'title' => self::faker()->text(255), - 'viewCount' => self::faker()->randomNumber(), - ]; - } - - /** - * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization - */ - protected function initialize(): self - { - return $this - // ->afterInstantiate(function(Post $post): void {}) - ; - } -} diff --git a/tests/Fixtures/Maker/expected/can_create_factory_with_static_analysis_annotations_with_data_set_phpstan.php b/tests/Fixtures/Maker/expected/can_create_factory_with_static_analysis_annotations_with_data_set_phpstan.php deleted file mode 100644 index e9578aea3..000000000 --- a/tests/Fixtures/Maker/expected/can_create_factory_with_static_analysis_annotations_with_data_set_phpstan.php +++ /dev/null @@ -1,77 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace App\Factory; - -use Doctrine\ORM\EntityRepository; -use Zenstruck\Foundry\Persistence\PersistentObjectFactory; -use Zenstruck\Foundry\Proxy; -use Zenstruck\Foundry\RepositoryProxy; -use Zenstruck\Foundry\Tests\Fixtures\Entity\Category; - -/** - * @extends PersistentObjectFactory - * - * @method Category|Proxy create(array|callable $attributes = []) - * @method static Category|Proxy createOne(array $attributes = []) - * @method static Category|Proxy find(object|array|mixed $criteria) - * @method static Category|Proxy findOrCreate(array $attributes) - * @method static Category|Proxy first(string $sortedField = 'id') - * @method static Category|Proxy last(string $sortedField = 'id') - * @method static Category|Proxy random(array $attributes = []) - * @method static Category|Proxy randomOrCreate(array $attributes = []) - * @method static EntityRepository|RepositoryProxy repository() - * @method static Category[]|Proxy[] all() - * @method static Category[]|Proxy[] createMany(int $number, array|callable $attributes = []) - * @method static Category[]|Proxy[] createSequence(iterable|callable $sequence) - * @method static Category[]|Proxy[] findBy(array $attributes) - * @method static Category[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) - * @method static Category[]|Proxy[] randomSet(int $number, array $attributes = []) - */ -final class CategoryFactory extends PersistentObjectFactory -{ - /** - * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services - * - * @todo inject services if required - */ - public function __construct() - { - parent::__construct(); - } - - public static function class(): string - { - return Category::class; - } - - /** - * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#model-factories - * - * @todo add your default values here - */ - protected function getDefaults(): array - { - return [ - 'name' => self::faker()->text(255), - ]; - } - - /** - * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization - */ - protected function initialize(): self - { - return $this - // ->afterInstantiate(function(Category $category): void {}) - ; - } -} diff --git a/tests/Fixtures/Maker/expected/can_create_factory_with_static_analysis_annotations_with_data_set_psalm.php b/tests/Fixtures/Maker/expected/can_create_factory_with_static_analysis_annotations_with_data_set_psalm.php deleted file mode 100644 index e9578aea3..000000000 --- a/tests/Fixtures/Maker/expected/can_create_factory_with_static_analysis_annotations_with_data_set_psalm.php +++ /dev/null @@ -1,77 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace App\Factory; - -use Doctrine\ORM\EntityRepository; -use Zenstruck\Foundry\Persistence\PersistentObjectFactory; -use Zenstruck\Foundry\Proxy; -use Zenstruck\Foundry\RepositoryProxy; -use Zenstruck\Foundry\Tests\Fixtures\Entity\Category; - -/** - * @extends PersistentObjectFactory - * - * @method Category|Proxy create(array|callable $attributes = []) - * @method static Category|Proxy createOne(array $attributes = []) - * @method static Category|Proxy find(object|array|mixed $criteria) - * @method static Category|Proxy findOrCreate(array $attributes) - * @method static Category|Proxy first(string $sortedField = 'id') - * @method static Category|Proxy last(string $sortedField = 'id') - * @method static Category|Proxy random(array $attributes = []) - * @method static Category|Proxy randomOrCreate(array $attributes = []) - * @method static EntityRepository|RepositoryProxy repository() - * @method static Category[]|Proxy[] all() - * @method static Category[]|Proxy[] createMany(int $number, array|callable $attributes = []) - * @method static Category[]|Proxy[] createSequence(iterable|callable $sequence) - * @method static Category[]|Proxy[] findBy(array $attributes) - * @method static Category[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) - * @method static Category[]|Proxy[] randomSet(int $number, array $attributes = []) - */ -final class CategoryFactory extends PersistentObjectFactory -{ - /** - * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services - * - * @todo inject services if required - */ - public function __construct() - { - parent::__construct(); - } - - public static function class(): string - { - return Category::class; - } - - /** - * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#model-factories - * - * @todo add your default values here - */ - protected function getDefaults(): array - { - return [ - 'name' => self::faker()->text(255), - ]; - } - - /** - * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization - */ - protected function initialize(): self - { - return $this - // ->afterInstantiate(function(Category $category): void {}) - ; - } -} diff --git a/tests/Fixtures/Migrations/Version20230513160346.php b/tests/Fixtures/Migrations/Version20230513160346.php index 5aca419cd..e9554828f 100644 --- a/tests/Fixtures/Migrations/Version20230513160346.php +++ b/tests/Fixtures/Migrations/Version20230513160346.php @@ -48,6 +48,7 @@ public function up(Schema $schema): void $this->addSql('CREATE TABLE entity_with_property_name_different_from_construct (id INT NOT NULL, entity_id INT DEFAULT NULL, someField VARCHAR(255) NOT NULL, address_value VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id))'); $this->addSql('CREATE INDEX IDX_AA016C6381257D5D ON entity_with_property_name_different_from_construct (entity_id)'); $this->addSql('ALTER TABLE entity_with_property_name_different_from_construct ADD CONSTRAINT FK_AA016C6381257D5D FOREIGN KEY (entity_id) REFERENCES entity_for_relations (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE users ADD data JSON NOT NULL;'); if (PHP_VERSION_ID < 80100) { return; diff --git a/tests/Fixtures/Object/SomeObjectFactory.php b/tests/Fixtures/Object/SomeObjectFactory.php index ecb2ac3d2..8030c3be9 100644 --- a/tests/Fixtures/Object/SomeObjectFactory.php +++ b/tests/Fixtures/Object/SomeObjectFactory.php @@ -3,16 +3,10 @@ namespace Zenstruck\Foundry\Tests\Fixtures\Object; use Zenstruck\Foundry\Object\ObjectFactory; -use Zenstruck\Foundry\Proxy; use Zenstruck\Foundry\Tests\Fixtures\Factories\UserFactory; /** * @extends ObjectFactory - * - * @method SomeObject|Proxy create(array|callable $attributes = []) - * @method static SomeObject|Proxy createOne(array $attributes = []) - * @method static SomeObject[]|Proxy[] createMany(int $number, array|callable $attributes = []) - * @method static SomeObject[]|Proxy[] createSequence(iterable|callable $sequence) */ final class SomeObjectFactory extends ObjectFactory { diff --git a/tests/Fixtures/Object/SomeOtherObjectFactory.php b/tests/Fixtures/Object/SomeOtherObjectFactory.php index 32765081e..c1f4208c7 100644 --- a/tests/Fixtures/Object/SomeOtherObjectFactory.php +++ b/tests/Fixtures/Object/SomeOtherObjectFactory.php @@ -3,15 +3,9 @@ namespace Zenstruck\Foundry\Tests\Fixtures\Object; use Zenstruck\Foundry\Persistence\PersistentObjectFactory; -use Zenstruck\Foundry\Proxy; /** * @extends PersistentObjectFactory - * - * @method SomeOtherObject|Proxy create(array|callable $attributes = []) - * @method static SomeOtherObject|Proxy createOne(array $attributes = []) - * @method static SomeOtherObject[]|Proxy[] createMany(int $number, array|callable $attributes = []) - * @method static SomeOtherObject[]|Proxy[] createSequence(iterable|callable $sequence) */ final class SomeOtherObjectFactory extends PersistentObjectFactory { diff --git a/tests/Fixtures/PhpStan/test-types.php b/tests/Fixtures/PhpStan/test-types.php index 9b56be948..e691c261c 100644 --- a/tests/Fixtures/PhpStan/test-types.php +++ b/tests/Fixtures/PhpStan/test-types.php @@ -10,27 +10,35 @@ use function Zenstruck\Foundry\create_many; assertType('Zenstruck\Foundry\Proxy', PostFactory::new()->create()); +assertType('Zenstruck\Foundry\Proxy', PostFactory::new()->create(['title' => 'foo'])); +assertType('Zenstruck\Foundry\Proxy', PostFactory::new()->create(fn() => ['title' => 'foo'])); assertType('Zenstruck\Foundry\Proxy', PostFactory::createOne()); assertType('Zenstruck\Foundry\RepositoryProxy', PostFactory::repository()); assertType('Zenstruck\Foundry\FactoryCollection>', PostFactory::new()->many(2)); +assertType('Zenstruck\Foundry\FactoryCollection>', PostFactory::new()->sequence([[]])); assertType('array>', PostFactory::new()->many(2)->create()); assertType('array>', PostFactory::createMany(2)); assertType('Zenstruck\Foundry\Tests\Fixtures\Object\SomeObject', SomeObjectFactory::new()->create()); +assertType('Zenstruck\Foundry\Tests\Fixtures\Object\SomeObject', SomeObjectFactory::new()->create(['title' => 'foo'])); +assertType('Zenstruck\Foundry\Tests\Fixtures\Object\SomeObject', SomeObjectFactory::new()->create(fn() => ['title' => 'foo'])); assertType('Zenstruck\Foundry\Tests\Fixtures\Object\SomeObject', SomeObjectFactory::createOne()); assertType('Zenstruck\Foundry\FactoryCollection', SomeObjectFactory::new()->many(2)); +assertType('Zenstruck\Foundry\FactoryCollection', SomeObjectFactory::new()->sequence([[]])); assertType('array', SomeObjectFactory::new()->many(2)->create()); assertType('array', SomeObjectFactory::createMany(2)); assertType('Zenstruck\Foundry\Persistence\PersistentObjectFactory', anonymous(Post::class)); assertType('Zenstruck\Foundry\Proxy', anonymous(Post::class)->create()); assertType('Zenstruck\Foundry\FactoryCollection>', anonymous(Post::class)->many(2)); +assertType('Zenstruck\Foundry\FactoryCollection>', anonymous(Post::class)->sequence([[]])); assertType('array>', anonymous(Post::class)->many(2)->create()); assertType('Zenstruck\Foundry\Proxy', create(Post::class)); assertType('Zenstruck\Foundry\FactoryCollection>', create_many(2, Post::class)); assertType('Zenstruck\Foundry\Tests\Fixtures\Object\SomeObject', anonymous(SomeObject::class)->create()); assertType('Zenstruck\Foundry\FactoryCollection', anonymous(SomeObject::class)->many(2)); +assertType('Zenstruck\Foundry\FactoryCollection', anonymous(SomeObject::class)->sequence([[]])); assertType('array', anonymous(SomeObject::class)->many(2)->create()); assertType('Zenstruck\Foundry\Tests\Fixtures\Object\SomeObject', create(SomeObject::class)); assertType('Zenstruck\Foundry\FactoryCollection', create_many(2, SomeObject::class)); diff --git a/tests/Fixtures/Psalm/test-types-with-persistence.php b/tests/Fixtures/Psalm/test-types-with-persistence.php new file mode 100644 index 000000000..aad402f07 --- /dev/null +++ b/tests/Fixtures/Psalm/test-types-with-persistence.php @@ -0,0 +1,81 @@ + $factory + * @return PersistentObjectFactory + */ +function factory(PersistentObjectFactory $factory): PersistentObjectFactory +{ + return $factory; +} + +/** + * @param Proxy $proxy + */ +function proxy(Proxy $proxy): Contact +{ + return $proxy->object(); +} + +/** + * @param list> $proxies + * @return list + */ +function proxies(array $proxies): array +{ + return array_map( + fn($proxy) => $proxy->object(), + $proxies + ); +} + +/** + * @param RepositoryProxy $proxy + */ +function repository(RepositoryProxy $proxy): Contact|null +{ + return $proxy->findOneBy([])?->object(); +} + +/** + * @param FactoryCollection> $factoryCollection + * @return list> + */ +function factory_collection(FactoryCollection $factoryCollection): array +{ + return $factoryCollection->create(); +} + +factory(ContactFactory::new()); +factory(anonymous(Contact::class)); + +proxy(ContactFactory::new()->create()); +proxy(ContactFactory::new()->create(['title' => 'foo'])); +proxy(ContactFactory::new()->create(fn() => ['title' => 'foo'])); +proxy(ContactFactory::createOne()); +proxy(anonymous(Contact::class)->create()); +proxy(create(Contact::class)); + +proxies(ContactFactory::new()->many(2)->create()); +proxies(ContactFactory::createMany(2)); +proxies(anonymous(Contact::class)->many(2)->create()); +proxies(create_many(2, Contact::class)); + +repository(ContactFactory::repository()); + +factory_collection(ContactFactory::new()->many(2)); +factory_collection(ContactFactory::new()->sequence([['title' => 'foo']])); +factory_collection(anonymous(Contact::class)->many(2)); +factory_collection(anonymous(Contact::class)->sequence([['title' => 'foo']])); + + diff --git a/tests/Fixtures/Psalm/test-types-without-persistence.php b/tests/Fixtures/Psalm/test-types-without-persistence.php new file mode 100644 index 000000000..3af3dabe9 --- /dev/null +++ b/tests/Fixtures/Psalm/test-types-without-persistence.php @@ -0,0 +1,62 @@ + $factory + * @return ObjectFactory + */ +function factory(ObjectFactory $factory): ObjectFactory +{ + return $factory; +} + +function object(SomeObject $object): SomeObject +{ + return $object; +} + +/** + * @param list $objects + * @return list + */ +function objects(array $objects): array +{ + return $objects; +} + +/** + * @param FactoryCollection $factoryCollection + * @return FactoryCollection + */ +function factory_collection(FactoryCollection $factoryCollection): FactoryCollection +{ + return $factoryCollection; +} + +factory(SomeObjectFactory::new()); +factory(anonymous(SomeObject::class)); + +object(SomeObjectFactory::new()->create()); +object(SomeObjectFactory::new()->create(['title' => 'foo'])); +object(SomeObjectFactory::new()->create(fn() => ['title' => 'foo'])); +object(SomeObjectFactory::createOne()); +object(anonymous(SomeObject::class)->create()); +object(create(SomeObject::class)); + +objects(SomeObjectFactory::new()->many(2)->create()); +objects(SomeObjectFactory::createMany(2)); +objects(anonymous(SomeObject::class)->many(2)->create()); +objects(create_many(2, SomeObject::class)); + +factory_collection(SomeObjectFactory::new()->many(2)); +factory_collection(SomeObjectFactory::new()->sequence([['title' => 'foo']])); +factory_collection(SomeObjectFactory::new()->sequence(fn() => [['title' => 'foo']])); +factory_collection(anonymous(SomeObject::class)->many(2)); +factory_collection(anonymous(SomeObject::class)->sequence([['title' => 'foo']])); diff --git a/tests/Functional/Bundle/Maker/MakeFactoryTest.php b/tests/Functional/Bundle/Maker/MakeFactoryTest.php index 909b75b6a..e767caf1e 100644 --- a/tests/Functional/Bundle/Maker/MakeFactoryTest.php +++ b/tests/Functional/Bundle/Maker/MakeFactoryTest.php @@ -16,7 +16,6 @@ use Symfony\Bundle\MakerBundle\Str; use Symfony\Component\Console\Tester\CommandTester; use Symfony\Component\HttpKernel\KernelInterface; -use Zenstruck\Foundry\Bundle\Maker\Factory\FactoryGenerator; use Zenstruck\Foundry\Tests\Fixtures\Document\ODMComment; use Zenstruck\Foundry\Tests\Fixtures\Document\ODMPost; use Zenstruck\Foundry\Tests\Fixtures\Document\Tag as AnotherTagClass; @@ -43,9 +42,6 @@ */ final class MakeFactoryTest extends MakerTestCase { - private const PHPSTAN_PATH = __DIR__.'/../../../..'.FactoryGenerator::PHPSTAN_PATH; - private const PSALM_PATH = __DIR__.'/../../../..'.FactoryGenerator::PSALM_PATH; - protected function setUp(): void { self::assertDirectoryDoesNotExist(self::tempDir()); @@ -56,16 +52,6 @@ protected function setUp(): void protected function tearDown(): void { parent::tearDown(); - - $removeSCAMock = static function(string $file): void { - if (\file_exists($file)) { - \unlink($file); - \rmdir(\dirname($file)); - \rmdir(\dirname($file, 2)); - } - }; - $removeSCAMock(self::PHPSTAN_PATH); - $removeSCAMock(self::PSALM_PATH); } /** @@ -169,50 +155,6 @@ public function can_create_factory_in_test_dir_interactively(): void $this->assertFileFromMakerSameAsExpectedFile(self::tempFile('tests/Factory/TagFactory.php')); } - /** - * @test - * @dataProvider scaToolProvider - */ - public function can_create_factory_with_static_analysis_annotations(string $scaTool): void - { - if (!\getenv('USE_ORM')) { - self::markTestSkipped('doctrine/orm not enabled.'); - } - - $this->emulateSCAToolEnabled($scaTool); - - $tester = $this->makeFactoryCommandTester(); - - $tester->execute(['class' => Category::class]); - - $this->assertFileFromMakerSameAsExpectedFile(self::tempFile('src/Factory/CategoryFactory.php')); - } - - /** - * @test - * @dataProvider scaToolProvider - */ - public function can_create_factory_for_entity_with_repository(string $scaTool): void - { - if (!\getenv('USE_ORM')) { - self::markTestSkipped('doctrine/orm not enabled.'); - } - - $this->emulateSCAToolEnabled($scaTool); - - $tester = $this->makeFactoryCommandTester(); - - $tester->execute(['class' => ORMPost::class]); - - $this->assertFileFromMakerSameAsExpectedFile(self::tempFile('src/Factory/PostFactory.php')); - } - - public function scaToolProvider(): iterable - { - yield 'phpstan' => [self::PHPSTAN_PATH]; - yield 'psalm' => [self::PSALM_PATH]; - } - /** * @test */ @@ -574,12 +516,6 @@ protected static function createKernel(array $options = []): KernelInterface ); } - private function emulateSCAToolEnabled(string $scaToolFilePath): void - { - \mkdir(\dirname($scaToolFilePath), 0777, true); - \touch($scaToolFilePath); - } - private function makeFactoryCommandTester(array $factoriesRegistered = []): CommandTester { return new CommandTester( diff --git a/tests/Unit/ModelFactoryTest.php b/tests/Unit/ModelFactoryTest.php index 7561a36e3..f37dce3c2 100644 --- a/tests/Unit/ModelFactoryTest.php +++ b/tests/Unit/ModelFactoryTest.php @@ -54,21 +54,6 @@ public function can_instantiate(): void $this->assertSame('title', PostFactory::createOne(['title' => 'title'])->getTitle()); } - /** - * @test - * @group legacy - */ - public function can_instantiate_many_legacy(): void - { - $this->expectDeprecation(\sprintf('Since zenstruck/foundry 1.7: Calling instance method "%1$s::createMany()" is deprecated and will be removed in 2.0, use e.g. "%1$s::new()->many(2)->create()" instead.', PostFactory::class)); - - $objects = PostFactory::new(['body' => 'body'])->createMany(2, ['title' => 'title']); - - $this->assertCount(2, $objects); - $this->assertSame('title', $objects[0]->getTitle()); - $this->assertSame('body', $objects[1]->getBody()); - } - /** * @test */