├── CHANGELOG.md
├── LICENSE
├── README.md
├── UPGRADE-2.0.md
├── composer.json
├── config
├── command_stubs.php
├── in_memory.php
├── makers.php
├── mongo.php
├── orm.php
├── persistence.php
└── services.php
├── docs
├── .doctor-rst.yaml
└── index.rst
├── phpbench.json
├── phpunit-deprecation-baseline.xml
├── skeleton
├── Factory.tpl.php
└── Story.tpl.php
├── src
├── AnonymousFactoryGenerator.php
├── ArrayFactory.php
├── Attribute
│ ├── AsFixture.php
│ └── WithStory.php
├── Command
│ ├── LoadStoryCommand.php
│ └── StubCommand.php
├── Configuration.php
├── DependencyInjection
│ └── AsFixtureStoryCompilerPass.php
├── Exception
│ ├── CannotCreateFactory.php
│ ├── FactoriesTraitNotUsed.php
│ ├── FoundryNotBooted.php
│ ├── PersistenceDisabled.php
│ └── PersistenceNotAvailable.php
├── Factory.php
├── FactoryCollection.php
├── FactoryRegistry.php
├── FactoryRegistryInterface.php
├── ForceValue.php
├── InMemory
│ ├── AsInMemoryTest.php
│ ├── CannotEnableInMemory.php
│ ├── DependencyInjection
│ │ └── InMemoryCompilerPass.php
│ ├── GenericInMemoryRepository.php
│ ├── InMemoryDoctrineObjectRepositoryAdapter.php
│ ├── InMemoryFactoryRegistry.php
│ ├── InMemoryRepository.php
│ ├── InMemoryRepositoryRegistry.php
│ └── InMemoryRepositoryTrait.php
├── LazyValue.php
├── Maker
│ ├── Factory
│ │ ├── AbstractDefaultPropertyGuesser.php
│ │ ├── AbstractDoctrineDefaultPropertiesGuesser.php
│ │ ├── DefaultPropertiesGuesser.php
│ │ ├── DoctrineScalarFieldsDefaultPropertiesGuesser.php
│ │ ├── Exception
│ │ │ └── FactoryClassAlreadyExistException.php
│ │ ├── FactoryCandidatesClassesExtractor.php
│ │ ├── FactoryClassMap.php
│ │ ├── FactoryGenerator.php
│ │ ├── LegacyORMDefaultPropertiesGuesser.php
│ │ ├── MakeFactoryData.php
│ │ ├── MakeFactoryPHPDocMethod.php
│ │ ├── MakeFactoryQuery.php
│ │ ├── NoPersistenceObjectsAutoCompleter.php
│ │ ├── ODMDefaultPropertiesGuesser.php
│ │ ├── ORMDefaultPropertiesGuesser.php
│ │ └── ObjectDefaultPropertiesGuesser.php
│ ├── MakeFactory.php
│ ├── MakeStory.php
│ └── NamespaceGuesser.php
├── Mongo
│ ├── MongoPersistenceStrategy.php
│ ├── MongoResetter.php
│ └── MongoSchemaResetter.php
├── ORM
│ ├── AbstractORMPersistenceStrategy.php
│ ├── DoctrineOrmVersionGuesser.php
│ ├── OrmV2PersistenceStrategy.php
│ ├── OrmV3PersistenceStrategy.php
│ └── ResetDatabase
│ │ ├── BaseOrmResetter.php
│ │ ├── DamaDatabaseResetter.php
│ │ ├── MigrateDatabaseResetter.php
│ │ ├── OrmResetter.php
│ │ ├── ResetDatabaseMode.php
│ │ └── SchemaDatabaseResetter.php
├── Object
│ ├── Hydrator.php
│ └── Instantiator.php
├── ObjectFactory.php
├── PHPUnit
│ ├── BootFoundryOnDataProviderMethodCalled.php
│ ├── BuildStoryOnTestPrepared.php
│ ├── DisplayFakerSeedOnTestSuiteFinished.php
│ ├── EnableInMemoryBeforeTest.php
│ ├── FoundryExtension.php
│ └── ShutdownFoundryOnDataProviderMethodFinished.php
├── Persistence
│ ├── Exception
│ │ ├── NoPersistenceStrategy.php
│ │ ├── NotEnoughObjects.php
│ │ └── RefreshObjectFailed.php
│ ├── IsProxy.php
│ ├── PersistMode.php
│ ├── PersistenceManager.php
│ ├── PersistenceStrategy.php
│ ├── PersistentObjectFactory.php
│ ├── PersistentProxyObjectFactory.php
│ ├── Proxy.php
│ ├── ProxyGenerator.php
│ ├── ProxyRepositoryDecorator.php
│ ├── Relationship
│ │ ├── ManyToOneRelationship.php
│ │ ├── OneToManyRelationship.php
│ │ ├── OneToOneRelationship.php
│ │ └── RelationshipMetadata.php
│ ├── RepositoryAssertions.php
│ ├── RepositoryDecorator.php
│ ├── ResetDatabase
│ │ ├── BeforeEachTestResetter.php
│ │ ├── BeforeFirstTestResetter.php
│ │ └── ResetDatabaseManager.php
│ └── functions.php
├── Story.php
├── StoryRegistry.php
├── Test
│ ├── Factories.php
│ ├── ResetDatabase.php
│ └── UnitTestConfig.php
├── ZenstruckFoundryBundle.php
├── functions.php
└── symfony_console.php
└── utils
└── psalm
├── FixProxyFactoryMethodsReturnType.php
└── FoundryPlugin.php
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Kevin Bond
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "zenstruck/foundry",
3 | "description": "A model factory library for creating expressive, auto-completable, on-demand dev/test fixtures with Symfony and Doctrine.",
4 | "homepage": "https://github.com/zenstruck/foundry",
5 | "type": "library",
6 | "license": "MIT",
7 | "keywords": ["fixture", "factory", "test", "symfony", "faker", "doctrine", "dev"],
8 | "authors": [
9 | {
10 | "name": "Kevin Bond",
11 | "email": "kevinbond@gmail.com"
12 | },
13 | {
14 | "name": "Nicolas PHILIPPE",
15 | "email": "nikophil@gmail.com"
16 | }
17 | ],
18 | "require": {
19 | "php": ">=8.1",
20 | "fakerphp/faker": "^1.23",
21 | "symfony/deprecation-contracts": "^2.2|^3.0",
22 | "symfony/property-access": "^6.4|^7.0",
23 | "symfony/property-info": "^6.4|^7.0",
24 | "symfony/var-exporter": "^6.4.9|~7.0.9|^7.1.2",
25 | "zenstruck/assert": "^1.4"
26 | },
27 | "require-dev": {
28 | "bamarni/composer-bin-plugin": "^1.8",
29 | "brianium/paratest": "^6|^7",
30 | "dama/doctrine-test-bundle": "^8.0",
31 | "doctrine/collections": "^1.7|^2.0",
32 | "doctrine/common": "^3.2.2",
33 | "doctrine/doctrine-bundle": "^2.10",
34 | "doctrine/doctrine-migrations-bundle": "^2.2|^3.0",
35 | "doctrine/mongodb-odm": "^2.4",
36 | "doctrine/mongodb-odm-bundle": "^4.6|^5.0",
37 | "doctrine/persistence": "^2.0|^3.0|^4.0",
38 | "doctrine/orm": "^2.16|^3.0",
39 | "phpunit/phpunit": "^9.5.0 || ^10.0 || ^11.0 || ^12.0",
40 | "symfony/console": "^6.4|^7.0",
41 | "symfony/dotenv": "^6.4|^7.0",
42 | "symfony/framework-bundle": "^6.4|^7.0",
43 | "symfony/maker-bundle": "^1.55",
44 | "symfony/phpunit-bridge": "^6.4|^7.0",
45 | "symfony/runtime": "^6.4|^7.0",
46 | "symfony/translation-contracts": "^3.4",
47 | "symfony/uid": "^6.4|^7.0",
48 | "symfony/var-dumper": "^6.4|^7.0",
49 | "symfony/yaml": "^6.4|^7.0"
50 | },
51 | "autoload": {
52 | "psr-4": {
53 | "Zenstruck\\Foundry\\": "src/",
54 | "Zenstruck\\Foundry\\Psalm\\": "utils/psalm"
55 | },
56 | "files": [
57 | "src/functions.php",
58 | "src/Persistence/functions.php",
59 | "src/symfony_console.php"
60 | ]
61 | },
62 | "autoload-dev": {
63 | "psr-4": {
64 | "Zenstruck\\Foundry\\Tests\\": ["tests/"],
65 | "App\\": "tests/Fixture/Maker/tmp/src",
66 | "App\\Tests\\": "tests/Fixture/Maker/tmp/tests"
67 | },
68 | "exclude-from-classmap": ["tests/Fixture/Maker/expected"]
69 | },
70 | "config": {
71 | "preferred-install": "dist",
72 | "sort-packages": true,
73 | "allow-plugins": {
74 | "bamarni/composer-bin-plugin": true,
75 | "symfony/flex": true,
76 | "symfony/runtime": false
77 | }
78 | },
79 | "conflict": {
80 | "doctrine/persistence": "<2.0",
81 | "symfony/framework-bundle": "<6.4"
82 | },
83 | "extra": {
84 | "bamarni-bin": {
85 | "target-directory": "bin/tools",
86 | "bin-links": true,
87 | "forward-command": false
88 | },
89 | "psalm": {
90 | "pluginClass": "Zenstruck\\Foundry\\Psalm\\FoundryPlugin"
91 | }
92 | },
93 | "scripts": {
94 | "post-install-cmd": ["@composer bin phpstan install", "@composer bin phpbench install"]
95 | },
96 | "minimum-stability": "dev",
97 | "prefer-stable": true
98 | }
99 |
--------------------------------------------------------------------------------
/config/command_stubs.php:
--------------------------------------------------------------------------------
1 | services()
9 | ->set('.zenstruck_foundry.make_factory_command', StubCommand::class)
10 | ->args([
11 | "To run \"make:factory\" you need the \"MakerBundle\" which is currently not installed.\n\nTry running \"composer require symfony/maker-bundle --dev\"."
12 | ])
13 | ->tag('console.command', [
14 | 'command' => '|make:factory',
15 | 'description' => 'Creates a Foundry object factory',
16 | ])
17 | ->set('.zenstruck_foundry.make_story_command', StubCommand::class)
18 | ->args([
19 | "To run \"make:story\" you need the \"MakerBundle\" which is currently not installed.\n\nTry running \"composer require symfony/maker-bundle --dev\"."
20 | ])
21 | ->tag('console.command', [
22 | 'command' => '|make:story',
23 | 'description' => 'Creates a Foundry story',
24 | ])
25 | ;
26 | };
27 |
--------------------------------------------------------------------------------
/config/in_memory.php:
--------------------------------------------------------------------------------
1 | services()
10 | ->set('.zenstruck_foundry.in_memory.factory_registry', InMemoryFactoryRegistry::class)
11 | ->decorate('.zenstruck_foundry.factory_registry')
12 | ->arg('$decorated', service('.inner'));
13 |
14 | $container->services()
15 | ->set('.zenstruck_foundry.in_memory.repository_registry', InMemoryRepositoryRegistry::class)
16 | ->arg('$inMemoryRepositories', abstract_arg('inMemoryRepositories'))
17 | ;
18 | };
19 |
--------------------------------------------------------------------------------
/config/makers.php:
--------------------------------------------------------------------------------
1 | services()
21 | ->set('.zenstruck_foundry.maker.story', MakeStory::class)
22 | ->args([
23 | service('.zenstruck_foundry.maker.namespace_guesser')
24 | ])
25 | ->tag('maker.command')
26 |
27 | ->set('.zenstruck_foundry.maker.factory', MakeFactory::class)
28 | ->args([
29 | service('kernel'),
30 | service('.zenstruck_foundry.maker.factory.generator'),
31 | service('.zenstruck_foundry.maker.factory.autoCompleter'),
32 | service('.zenstruck_foundry.maker.factory.candidate_classes_extractor'),
33 | ])
34 | ->tag('maker.command')
35 |
36 | ->set('.zenstruck_foundry.maker.factory.orm_default_properties_guesser', DoctrineOrmVersionGuesser::isOrmV3() ? ORMDefaultPropertiesGuesser::class : LegacyORMDefaultPropertiesGuesser::class)
37 | ->args([
38 | service('.zenstruck_foundry.persistence_manager')->nullOnInvalid(),
39 | service('.zenstruck_foundry.maker.factory.factory_class_map'),
40 | service('.zenstruck_foundry.maker.factory.generator'),
41 | ])
42 | ->tag('foundry.make_factory.default_properties_guesser')
43 |
44 | ->set('.zenstruck_foundry.maker.factory.odm_default_properties_guesser', ODMDefaultPropertiesGuesser::class)
45 | ->args([
46 | service('.zenstruck_foundry.persistence_manager')->nullOnInvalid(),
47 | service('.zenstruck_foundry.maker.factory.factory_class_map'),
48 | service('.zenstruck_foundry.maker.factory.generator'),
49 | ])
50 | ->tag('foundry.make_factory.default_properties_guesser')
51 |
52 | ->set('.zenstruck_foundry.maker.factory.doctrine_scalar_fields_default_properties_guesser', DoctrineScalarFieldsDefaultPropertiesGuesser::class)
53 | ->args([
54 | service('.zenstruck_foundry.persistence_manager')->nullOnInvalid(),
55 | service('.zenstruck_foundry.maker.factory.factory_class_map'),
56 | service('.zenstruck_foundry.maker.factory.generator'),
57 | ])
58 | ->tag('foundry.make_factory.default_properties_guesser')
59 |
60 | ->set('.zenstruck_foundry.maker.factory.object_default_properties_guesser', ObjectDefaultPropertiesGuesser::class)
61 | ->args([
62 | service('.zenstruck_foundry.maker.factory.factory_class_map'),
63 | service('.zenstruck_foundry.maker.factory.generator'),
64 | ])
65 | ->tag('foundry.make_factory.default_properties_guesser')
66 |
67 | ->set('.zenstruck_foundry.maker.factory.factory_class_map', FactoryClassMap::class)
68 | ->args([
69 | tagged_iterator('foundry.factory'),
70 | ])
71 |
72 | ->set('.zenstruck_foundry.maker.factory.generator', FactoryGenerator::class)
73 | ->args([
74 | service('.zenstruck_foundry.persistence_manager')->nullOnInvalid(),
75 | service('kernel'),
76 | tagged_iterator('foundry.make_factory.default_properties_guesser'),
77 | service('.zenstruck_foundry.maker.factory.factory_class_map'),
78 | service('.zenstruck_foundry.maker.namespace_guesser'),
79 | ])
80 |
81 | ->set('.zenstruck_foundry.maker.factory.autoCompleter', NoPersistenceObjectsAutoCompleter::class)
82 | ->args([
83 | param('kernel.project_dir')
84 | ])
85 |
86 | ->set('.zenstruck_foundry.maker.factory.candidate_classes_extractor', FactoryCandidatesClassesExtractor::class)
87 | ->args([
88 | service('.zenstruck_foundry.persistence_manager')->nullOnInvalid(),
89 | service('.zenstruck_foundry.maker.factory.factory_class_map'),
90 | ])
91 |
92 | ->set('.zenstruck_foundry.maker.namespace_guesser', NamespaceGuesser::class)
93 | ->args([
94 | service('.zenstruck_foundry.persistence_manager')->nullOnInvalid(),
95 | ])
96 | ;
97 | };
98 |
--------------------------------------------------------------------------------
/config/mongo.php:
--------------------------------------------------------------------------------
1 | services()
11 | ->set('.zenstruck_foundry.persistence_strategy.mongo', MongoPersistenceStrategy::class)
12 | ->args([
13 | service('doctrine_mongodb'),
14 | ])
15 | ->tag('.foundry.persistence_strategy')
16 |
17 | ->set(MongoResetter::class, MongoSchemaResetter::class)
18 | ->args([
19 | abstract_arg('managers'),
20 | ])
21 | ->tag('.foundry.persistence.schema_resetter')
22 | ;
23 | };
24 |
--------------------------------------------------------------------------------
/config/orm.php:
--------------------------------------------------------------------------------
1 | services()
15 | ->set('.zenstruck_foundry.persistence_strategy.orm', DoctrineOrmVersionGuesser::isOrmV3() ? OrmV3PersistenceStrategy::class : OrmV2PersistenceStrategy::class)
16 | ->args([
17 | service('doctrine'),
18 | ])
19 | ->tag('.foundry.persistence_strategy')
20 |
21 | ->set('.zenstruck_foundry.persistence.database_resetter.orm.abstract', BaseOrmResetter::class)
22 | ->arg('$registry', service('doctrine'))
23 | ->arg('$managers', service('managers'))
24 | ->arg('$connections', service('connections'))
25 | ->abstract()
26 |
27 | ->set(OrmResetter::class, /* class to be defined thanks to the configuration */)
28 | ->parent('.zenstruck_foundry.persistence.database_resetter.orm.abstract')
29 | ->tag('.foundry.persistence.database_resetter')
30 | ->tag('.foundry.persistence.schema_resetter')
31 | ;
32 |
33 | if (\class_exists(StaticDriver::class)) {
34 | $container->services()
35 | ->set('.zenstruck_foundry.persistence.database_resetter.orm.dama', DamaDatabaseResetter::class)
36 | ->decorate(OrmResetter::class, priority: 10)
37 | ->args([
38 | service('.inner'),
39 | ])
40 | ;
41 | }
42 | };
43 |
--------------------------------------------------------------------------------
/config/persistence.php:
--------------------------------------------------------------------------------
1 | services()
11 | ->set('.zenstruck_foundry.persistence_manager', PersistenceManager::class)
12 | ->args([
13 | tagged_iterator('.foundry.persistence_strategy'),
14 | service('.zenstruck_foundry.persistence.reset_database_manager'),
15 | ])
16 | ->set('.zenstruck_foundry.persistence.reset_database_manager', ResetDatabaseManager::class)
17 | ->args([
18 | tagged_iterator('.foundry.persistence.database_resetter'),
19 | tagged_iterator('.foundry.persistence.schema_resetter'),
20 | ])
21 |
22 | ->set('.zenstruck_foundry.story.load_story-command', LoadStoryCommand::class)
23 | ->arg('$databaseResetters', tagged_iterator('.foundry.persistence.database_resetter'))
24 | ->arg('$kernel', service('kernel'))
25 | ->tag('console.command', [
26 | 'command' => 'foundry:load-stories',
27 | 'aliases' => ['foundry:load-fixtures', 'foundry:load-fixture', 'foundry:load-story'],
28 | 'description' => 'Load stories which are marked with #[AsFixture] attribute.',
29 | ])
30 | ;
31 | };
32 |
--------------------------------------------------------------------------------
/config/services.php:
--------------------------------------------------------------------------------
1 | services()
13 | ->set('.zenstruck_foundry.faker', Faker\Generator::class)
14 | ->factory([Faker\Factory::class, 'create'])
15 |
16 | ->set('.zenstruck_foundry.factory_registry', FactoryRegistry::class)
17 | ->args([tagged_iterator('foundry.factory')])
18 |
19 | ->set('.zenstruck_foundry.story_registry', StoryRegistry::class)
20 | ->args([
21 | tagged_iterator('foundry.story'),
22 | abstract_arg('global_stories'),
23 | ])
24 |
25 | ->set('.zenstruck_foundry.instantiator', Instantiator::class)
26 | ->factory([Instantiator::class, 'withConstructor'])
27 |
28 | ->set('.zenstruck_foundry.configuration', Configuration::class)
29 | ->args([
30 | service('.zenstruck_foundry.factory_registry'),
31 | service('.zenstruck_foundry.faker'),
32 | service('.zenstruck_foundry.instantiator'),
33 | service('.zenstruck_foundry.story_registry'),
34 | service('.zenstruck_foundry.persistence_manager')->nullOnInvalid(),
35 | param('zenstruck_foundry.persistence.flush_once'),
36 | '%env(default:zenstruck_foundry.faker.seed:int:FOUNDRY_FAKER_SEED)%',
37 | service('.zenstruck_foundry.in_memory.repository_registry'),
38 | ])
39 | ->public()
40 | ;
41 | };
42 |
--------------------------------------------------------------------------------
/docs/.doctor-rst.yaml:
--------------------------------------------------------------------------------
1 | rules:
2 | american_english: ~
3 | avoid_repetetive_words: ~
4 | blank_line_after_anchor: ~
5 | blank_line_after_directive: ~
6 | blank_line_before_directive: ~
7 | composer_dev_option_not_at_the_end: ~
8 | correct_code_block_directive_based_on_the_content: ~
9 | deprecated_directive_should_have_version: ~
10 | ensure_bash_prompt_before_composer_command: ~
11 | ensure_correct_format_for_phpfunction: ~
12 | ensure_exactly_one_space_before_directive_type: ~
13 | ensure_exactly_one_space_between_link_definition_and_link: ~
14 | ensure_explicit_nullable_types: ~
15 | ensure_github_directive_start_with_prefix:
16 | prefix: 'Foundry'
17 | ensure_link_bottom: ~
18 | ensure_link_definition_contains_valid_url: ~
19 | ensure_order_of_code_blocks_in_configuration_block: ~
20 | ensure_php_reference_syntax: ~
21 | extend_abstract_controller: ~
22 | # extension_xlf_instead_of_xliff: ~
23 | forbidden_directives:
24 | directives:
25 | - '.. index::'
26 | indention: ~
27 | lowercase_as_in_use_statements: ~
28 | max_blank_lines:
29 | max: 2
30 | max_colons: ~
31 | no_app_console: ~
32 | no_attribute_redundant_parenthesis: ~
33 | no_blank_line_after_filepath_in_php_code_block: ~
34 | no_blank_line_after_filepath_in_twig_code_block: ~
35 | no_blank_line_after_filepath_in_xml_code_block: ~
36 | no_blank_line_after_filepath_in_yaml_code_block: ~
37 | no_brackets_in_method_directive: ~
38 | no_composer_req: ~
39 | no_directive_after_shorthand: ~
40 | no_duplicate_use_statements: ~
41 | no_explicit_use_of_code_block_php: ~
42 | no_footnotes: ~
43 | no_inheritdoc: ~
44 | no_merge_conflict: ~
45 | no_namespace_after_use_statements: ~
46 | no_php_open_tag_in_code_block_php_directive: ~
47 | no_space_before_self_xml_closing_tag: ~
48 | only_backslashes_in_namespace_in_php_code_block: ~
49 | only_backslashes_in_use_statements_in_php_code_block: ~
50 | ordered_use_statements: ~
51 | php_prefix_before_bin_console: ~
52 | remove_trailing_whitespace: ~
53 | replace_code_block_types: ~
54 | replacement: ~
55 | short_array_syntax: ~
56 | space_between_label_and_link_in_doc: ~
57 | space_between_label_and_link_in_ref: ~
58 | string_replacement: ~
59 | title_underline_length_must_match_title_length: ~
60 | typo: ~
61 | unused_links: ~
62 | use_deprecated_directive_instead_of_versionadded: ~
63 | use_named_constructor_without_new_keyword_rule: ~
64 | use_https_xsd_urls: ~
65 | valid_inline_highlighted_namespaces: ~
66 | valid_use_statements: ~
67 | versionadded_directive_should_have_version: ~
68 | yaml_instead_of_yml_suffix: ~
69 |
--------------------------------------------------------------------------------
/phpbench.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./bin/tools/phpbench/vendor/phpbench/phpbench/phpbench.schema.json",
3 | "runner.bootstrap": "./vendor/autoload.php",
4 | "runner.php_env": {
5 | "KERNEL_CLASS": "Zenstruck\\Foundry\\Tests\\Fixture\\TestKernel",
6 | "APP_ENV": "dev"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/phpunit-deprecation-baseline.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/skeleton/Factory.tpl.php:
--------------------------------------------------------------------------------
1 |
2 |
3 | namespace ;
4 |
5 | getUses() as $use) {
7 | echo "use {$use};\n";
8 | }
9 | ?>
10 |
11 | /**
12 | * @extends getFactoryClassShortName(); ?><getObjectShortName(); ?>>
13 | getMethodsPHPDoc())) {
15 | echo " *\n";
16 | foreach ($makeFactoryData->getMethodsPHPDoc() as $methodPHPDoc) {
17 | echo "{$methodPHPDoc->toString()}\n";
18 | }
19 |
20 | echo " *\n";
21 |
22 | foreach ($makeFactoryData->getMethodsPHPDoc() as $methodPHPDoc) {
23 | echo "{$methodPHPDoc->toString($makeFactoryData->staticAnalysisTool())}\n";
24 | }
25 | }
26 | ?>
27 | */
28 | final class extends getFactoryClassShortName(); ?>
29 | {
30 | shouldAddHints()): ?> /**
31 | * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services
32 | *
33 | * @todo inject services if required
34 | */
35 | public function __construct()
36 | {
37 | }
38 |
39 | public static function class(): string
40 | {
41 | return getObjectShortName(); ?>::class;
42 | }
43 |
44 | shouldAddHints()): ?> /**
45 | * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#model-factories
46 | *
47 | * @todo add your default values here
48 | */
49 | protected function defaults(): arrayshouldAddHints()): ?>|callable
50 | {
51 | return [
52 | getDefaultProperties() as $propertyName => $value) {
54 | echo " '{$propertyName}' => {$value}\n";
55 | }
56 | ?>
57 | ];
58 | }
59 |
60 | shouldAddHints()): ?> /**
61 | * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization
62 | */
63 | protected function initialize(): static
64 | {
65 | return $this
66 | // ->afterInstantiate(function(getObjectShortName(); ?> $getObjectShortName()); ?>): void {})
67 | ;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/skeleton/Story.tpl.php:
--------------------------------------------------------------------------------
1 | = "
2 |
3 | namespace = $namespace; ?>;
4 |
5 | use Zenstruck\Foundry\Story;
6 |
7 | final class = $class_name ?> extends Story
8 | {
9 | public function build(): void
10 | {
11 | // TODO build your story here (https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#stories)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/AnonymousFactoryGenerator.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Foundry;
13 |
14 | /**
15 | * @author Kevin Bond
16 | *
17 | * @internal
18 | */
19 | final class AnonymousFactoryGenerator
20 | {
21 | /**
22 | * @template T of object
23 | * @template F of Factory
24 | *
25 | * @param class-string $class
26 | * @param class-string $factoryClass
27 | *
28 | * @return class-string
29 | */
30 | public static function create(string $class, string $factoryClass): string
31 | {
32 | $anonymousClassName = \sprintf('FoundryAnonymous%s_', (new \ReflectionClass($factoryClass))->getShortName());
33 | $anonymousClassName .= \str_replace('\\', '', $class);
34 | $anonymousClassName = \preg_replace('/\W/', '', $anonymousClassName); // sanitize for anonymous classes
35 |
36 | /** @var class-string $anonymousClassName */
37 | if (!\class_exists($anonymousClassName)) {
38 | $anonymousClassCode = <<
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Foundry;
13 |
14 | /**
15 | * @author Kevin Bond
16 | *
17 | * @phpstan-import-type Parameters from Factory
18 | * @extends Factory
19 | */
20 | abstract class ArrayFactory extends Factory
21 | {
22 | final public function create(callable|array $attributes = []): array
23 | {
24 | return $this->normalizeParameters($this->normalizeAttributes($attributes));
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Attribute/AsFixture.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Zenstruck\Foundry\Attribute;
15 |
16 | /**
17 | * @author Nicolas PHILIPPE
18 | */
19 | #[\Attribute(\Attribute::TARGET_CLASS)]
20 | final class AsFixture
21 | {
22 | public function __construct(
23 | public readonly string $name,
24 | /** @var list */
25 | public readonly array $groups = [],
26 | ) {
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Attribute/WithStory.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Zenstruck\Foundry\Attribute;
15 |
16 | use Zenstruck\Foundry\Story;
17 |
18 | #[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)]
19 | final class WithStory
20 | {
21 | public function __construct(
22 | /** @var class-string $story */
23 | public readonly string $story,
24 | ) {
25 | if (!\is_subclass_of($story, Story::class)) {
26 | throw new \InvalidArgumentException(\sprintf('"%s" is not a valid story class.', $story));
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Command/LoadStoryCommand.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Foundry\Command;
13 |
14 | use DAMA\DoctrineTestBundle\Doctrine\DBAL\StaticDriver;
15 | use Symfony\Component\Console\Command\Command;
16 | use Symfony\Component\Console\Exception\InvalidArgumentException;
17 | use Symfony\Component\Console\Exception\LogicException;
18 | use Symfony\Component\Console\Input\InputArgument;
19 | use Symfony\Component\Console\Input\InputInterface;
20 | use Symfony\Component\Console\Input\InputOption;
21 | use Symfony\Component\Console\Output\OutputInterface;
22 | use Symfony\Component\Console\Style\SymfonyStyle;
23 | use Symfony\Component\HttpKernel\KernelInterface;
24 | use Zenstruck\Foundry\Persistence\ResetDatabase\BeforeFirstTestResetter;
25 | use Zenstruck\Foundry\Story;
26 |
27 | /**
28 | * @author Nicolas PHILIPPE
29 | */
30 | final class LoadStoryCommand extends Command
31 | {
32 | public function __construct(
33 | /** @var array> */
34 | private readonly array $stories,
35 | /** @var array>> */
36 | private readonly array $groupedStories,
37 | /** @var iterable */
38 | private iterable $databaseResetters,
39 | private KernelInterface $kernel,
40 | ) {
41 | parent::__construct();
42 | }
43 |
44 | protected function configure(): void
45 | {
46 | $this
47 | ->addArgument('name', InputArgument::OPTIONAL, 'The name of the story to load.')
48 | ->addOption('append', 'a', InputOption::VALUE_NONE, 'Skip resetting database and append data to the existing database.')
49 | ;
50 | }
51 |
52 | protected function execute(InputInterface $input, OutputInterface $output): int
53 | {
54 | if (0 === \count($this->stories)) {
55 | throw new LogicException('No story as fixture available: add attribute #[AsFixture] to your story classes before running this command.');
56 | }
57 |
58 | $io = new SymfonyStyle($input, $output);
59 |
60 | if (!$input->getOption('append')) {
61 | $this->resetDatabase();
62 | }
63 |
64 | $stories = [];
65 |
66 | if (null === ($name = $input->getArgument('name'))) {
67 | if (1 === \count($this->stories)) {
68 | $name = \array_keys($this->stories)[0];
69 | } else {
70 | $storyNames = \array_keys($this->stories);
71 | if (\count($this->groupedStories) > 0) {
72 | $storyNames[] = '(choose a group of stories...)';
73 | }
74 | $name = $io->choice('Choose a story to load:', $storyNames);
75 | }
76 |
77 | if (!isset($this->stories[$name])) {
78 | $groupsNames = \array_keys($this->groupedStories);
79 | $name = $io->choice('Choose a group of stories:', $groupsNames);
80 | }
81 | }
82 |
83 | if (isset($this->stories[$name])) {
84 | $io->comment("Loading story with name \"{$name}\"...");
85 | $stories = [$name => $this->stories[$name]];
86 | }
87 |
88 | if (isset($this->groupedStories[$name])) {
89 | $io->comment("Loading stories group \"{$name}\"...");
90 | $stories = $this->groupedStories[$name];
91 | }
92 |
93 | if (!$stories) {
94 | throw new InvalidArgumentException("Story with name \"{$name}\" does not exist.");
95 | }
96 |
97 | foreach ($stories as $name => $storyClass) {
98 | $storyClass::load();
99 |
100 | if ($io->isVerbose()) {
101 | $io->info("Story \"{$storyClass}\" loaded (name: {$name}).");
102 | }
103 | }
104 |
105 | $io->success('Stories successfully loaded!');
106 |
107 | return self::SUCCESS;
108 | }
109 |
110 | private function resetDatabase(): void
111 | {
112 | // it is very not likely that we need dama when running this command
113 | if (\class_exists(StaticDriver::class) && StaticDriver::isKeepStaticConnections()) {
114 | StaticDriver::setKeepStaticConnections(false);
115 | }
116 |
117 | foreach ($this->databaseResetters as $databaseResetter) {
118 | $databaseResetter->resetBeforeFirstTest($this->kernel);
119 | }
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/src/Command/StubCommand.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Foundry\Command;
13 |
14 | use Symfony\Component\Console\Command\Command;
15 | use Symfony\Component\Console\Input\InputInterface;
16 | use Symfony\Component\Console\Output\OutputInterface;
17 |
18 | /**
19 | * @author Kevin Bond
20 | *
21 | * @internal
22 | */
23 | final class StubCommand extends Command
24 | {
25 | public function __construct(private string $message)
26 | {
27 | parent::__construct();
28 | }
29 |
30 | protected function configure(): void
31 | {
32 | $this->ignoreValidationErrors();
33 | }
34 |
35 | protected function execute(InputInterface $input, OutputInterface $output): int
36 | {
37 | throw new \RuntimeException($this->message);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/Configuration.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Foundry;
13 |
14 | use Faker;
15 | use Zenstruck\Foundry\Exception\FactoriesTraitNotUsed;
16 | use Zenstruck\Foundry\Exception\FoundryNotBooted;
17 | use Zenstruck\Foundry\Exception\PersistenceDisabled;
18 | use Zenstruck\Foundry\Exception\PersistenceNotAvailable;
19 | use Zenstruck\Foundry\InMemory\CannotEnableInMemory;
20 | use Zenstruck\Foundry\InMemory\InMemoryRepositoryRegistry;
21 | use Zenstruck\Foundry\Persistence\PersistenceManager;
22 |
23 | /**
24 | * @author Kevin Bond
25 | *
26 | * @internal
27 | *
28 | * @phpstan-import-type InstantiatorCallable from ObjectFactory
29 | */
30 | final class Configuration
31 | {
32 | /**
33 | * @readonly
34 | *
35 | * @phpstan-var InstantiatorCallable
36 | */
37 | public $instantiator;
38 |
39 | /**
40 | * This property is only filled if the PHPUnit extension is used!
41 | */
42 | private bool $bootedForDataProvider = false;
43 |
44 | /** @var \Closure():self|self|null */
45 | private static \Closure|self|null $instance = null;
46 |
47 | private static ?int $fakerSeed = null;
48 |
49 | private bool $inMemory = false;
50 |
51 | /**
52 | * @phpstan-param InstantiatorCallable $instantiator
53 | */
54 | public function __construct(
55 | public readonly FactoryRegistryInterface $factories,
56 | public readonly Faker\Generator $faker,
57 | callable $instantiator,
58 | public readonly StoryRegistry $stories,
59 | private readonly ?PersistenceManager $persistence = null,
60 | public readonly bool $flushOnce = false,
61 | ?int $forcedFakerSeed = null,
62 | public readonly ?InMemoryRepositoryRegistry $inMemoryRepositoryRegistry = null,
63 | ) {
64 | if (null === self::$instance) {
65 | $this->faker->seed(self::fakerSeed($forcedFakerSeed));
66 | }
67 |
68 | $this->instantiator = $instantiator;
69 | }
70 |
71 | public static function fakerSeed(?int $forcedFakerSeed = null): int
72 | {
73 | return self::$fakerSeed ??= ($forcedFakerSeed ?? \random_int(0, 1000000));
74 | }
75 |
76 | public static function resetFakerSeed(): void
77 | {
78 | self::$fakerSeed = null;
79 | }
80 |
81 | /**
82 | * @throws PersistenceNotAvailable
83 | */
84 | public function persistence(): PersistenceManager
85 | {
86 | return $this->persistence ?? throw new PersistenceNotAvailable('No persistence managers configured. Note: persistence cannot be used in unit tests.');
87 | }
88 |
89 | public function isPersistenceAvailable(): bool
90 | {
91 | return (bool) $this->persistence;
92 | }
93 |
94 | public function isPersistenceEnabled(): bool
95 | {
96 | return $this->isPersistenceAvailable() && $this->persistence()->isEnabled();
97 | }
98 |
99 | public function assertPersistenceEnabled(): void
100 | {
101 | if (!$this->isPersistenceEnabled()) {
102 | throw new PersistenceDisabled('Cannot get repository when persist is disabled (if in a unit test, you probably should not try to get the repository).');
103 | }
104 | }
105 |
106 | public function inADataProvider(): bool
107 | {
108 | return $this->bootedForDataProvider;
109 | }
110 |
111 | public static function instance(): self
112 | {
113 | if (!self::$instance) {
114 | throw new FoundryNotBooted();
115 | }
116 |
117 | FactoriesTraitNotUsed::throwIfComingFromKernelTestCaseWithoutFactoriesTrait();
118 |
119 | return \is_callable(self::$instance) ? (self::$instance)() : self::$instance;
120 | }
121 |
122 | public static function isBooted(): bool
123 | {
124 | return null !== self::$instance;
125 | }
126 |
127 | /** @param \Closure():self|self $configuration */
128 | public static function boot(\Closure|self $configuration): void
129 | {
130 | self::$instance = $configuration;
131 | }
132 |
133 | /** @param \Closure():self|self $configuration */
134 | public static function bootForDataProvider(\Closure|self $configuration): void
135 | {
136 | self::$instance = \is_callable($configuration) ? ($configuration)() : $configuration;
137 | self::$instance->bootedForDataProvider = true;
138 | }
139 |
140 | public static function shutdown(): void
141 | {
142 | StoryRegistry::reset();
143 | self::$instance = null;
144 | }
145 |
146 | /**
147 | * @throws CannotEnableInMemory
148 | */
149 | public function enableInMemory(): void
150 | {
151 | if (null === $this->inMemoryRepositoryRegistry) {
152 | throw CannotEnableInMemory::noInMemoryRepositoryRegistry();
153 | }
154 |
155 | $this->inMemory = true;
156 | }
157 |
158 | /**
159 | * @phpstan-assert-if-true InMemoryRepositoryRegistry $this->inMemoryRepositoryRegistry
160 | */
161 | public function isInMemoryEnabled(): bool
162 | {
163 | return $this->inMemory;
164 | }
165 | }
166 |
--------------------------------------------------------------------------------
/src/DependencyInjection/AsFixtureStoryCompilerPass.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Foundry\DependencyInjection;
13 |
14 | use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
15 | use Symfony\Component\DependencyInjection\ContainerBuilder;
16 | use Symfony\Component\DependencyInjection\Exception\LogicException;
17 | use Symfony\Component\DependencyInjection\Reference;
18 |
19 | final class AsFixtureStoryCompilerPass implements CompilerPassInterface
20 | {
21 | public function process(ContainerBuilder $container): void
22 | {
23 | if (!$container->has('.zenstruck_foundry.story.load_story-command')) {
24 | return;
25 | }
26 |
27 | /** @var array $fixtureStories */
28 | $fixtureStories = [];
29 | $groupedFixtureStories = [];
30 | foreach ($container->findTaggedServiceIds('foundry.story.fixture') as $id => $tags) {
31 | if (1 !== \count($tags)) {
32 | throw new LogicException('Tag "foundry.story.fixture" must be used only once per service.');
33 | }
34 |
35 | $name = $tags[0]['name'];
36 |
37 | if (isset($fixtureStories[$name])) {
38 | throw new LogicException("Cannot use #[AsFixture] name \"{$name}\" for service \"{$id}\". This name is already used by service \"{$fixtureStories[$name]}\".");
39 | }
40 |
41 | $storyClass = $container->findDefinition($id)->getClass();
42 |
43 | $fixtureStories[$name] = $storyClass;
44 |
45 | $groups = $tags[0]['groups'];
46 | if (!$groups) {
47 | continue;
48 | }
49 |
50 | foreach ($groups as $group) {
51 | $groupedFixtureStories[$group] ??= [];
52 | $groupedFixtureStories[$group][$name] = $storyClass;
53 | }
54 | }
55 |
56 | if ($collisionNames = \array_intersect(\array_keys($fixtureStories), \array_keys($groupedFixtureStories))) {
57 | $collisionNames = \implode('", "', $collisionNames);
58 | throw new LogicException("Cannot use #[AsFixture] group(s) \"{$collisionNames}\", they collide with fixture names.");
59 | }
60 |
61 | $container->findDefinition('.zenstruck_foundry.story.load_story-command')
62 | ->setArgument('$stories', $fixtureStories)
63 | ->setArgument('$groupedStories', $groupedFixtureStories);
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/Exception/CannotCreateFactory.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Zenstruck\Foundry\Exception;
15 |
16 | /**
17 | * @author Nicolas PHILIPPE
18 | * @internal
19 | */
20 | final class CannotCreateFactory extends \LogicException
21 | {
22 | public static function argumentCountError(\ArgumentCountError $e): static
23 | {
24 | return new self('Factories with dependencies (services) cannot be created before foundry is booted.', previous: $e);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Exception/FactoriesTraitNotUsed.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Zenstruck\Foundry\Exception;
15 |
16 | use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
17 | use Zenstruck\Foundry\Test\Factories;
18 |
19 | /**
20 | * @author Nicolas PHILIPPE
21 | */
22 | final class FactoriesTraitNotUsed extends \LogicException
23 | {
24 | /**
25 | * @param class-string $class
26 | */
27 | private function __construct(string $class)
28 | {
29 | parent::__construct(
30 | \sprintf('You must use the trait "%s" in "%s" in order to use Foundry.', Factories::class, $class)
31 | );
32 | }
33 |
34 | public static function throwIfComingFromKernelTestCaseWithoutFactoriesTrait(): void
35 | {
36 | $backTrace = \debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS); // @phpstan-ignore ekinoBannedCode.function
37 |
38 | foreach ($backTrace as $trace) {
39 | if (
40 | '->' === ($trace['type'] ?? null)
41 | && isset($trace['class'])
42 | && KernelTestCase::class !== $trace['class']
43 | && \is_a($trace['class'], KernelTestCase::class, allow_string: true)
44 | ) {
45 | self::throwIfClassDoesNotHaveFactoriesTrait($trace['class']);
46 | }
47 | }
48 | }
49 |
50 | /**
51 | * @param class-string $class
52 | */
53 | public static function throwIfClassDoesNotHaveFactoriesTrait(string $class): void
54 | {
55 | if (!(new \ReflectionClass($class))->hasMethod('_bootFoundry')) {
56 | // throw new self($class);
57 | trigger_deprecation(
58 | 'zenstruck/foundry',
59 | '2.4',
60 | 'In order to use Foundry correctly, you must use the trait "%s" in your "%s" tests. This will throw an exception in 3.0.',
61 | Factories::class,
62 | $class
63 | );
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/Exception/FoundryNotBooted.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Foundry\Exception;
13 |
14 | /**
15 | * @author Kevin Bond
16 | */
17 | final class FoundryNotBooted extends \LogicException
18 | {
19 | public function __construct()
20 | {
21 | parent::__construct('Foundry is not yet booted. Ensure ZenstruckFoundryBundle is enabled. If in a test, ensure your TestCase has the Factories trait.');
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/Exception/PersistenceDisabled.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Foundry\Exception;
13 |
14 | final class PersistenceDisabled extends \LogicException
15 | {
16 | }
17 |
--------------------------------------------------------------------------------
/src/Exception/PersistenceNotAvailable.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Foundry\Exception;
13 |
14 | /**
15 | * @author Kevin Bond
16 | */
17 | final class PersistenceNotAvailable extends \LogicException
18 | {
19 | }
20 |
--------------------------------------------------------------------------------
/src/FactoryRegistry.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Foundry;
13 |
14 | use Zenstruck\Foundry\Exception\CannotCreateFactory;
15 |
16 | /**
17 | * @author Kevin Bond
18 | *
19 | * @internal
20 | */
21 | final class FactoryRegistry implements FactoryRegistryInterface
22 | {
23 | /**
24 | * @param Factory[] $factories
25 | */
26 | public function __construct(private iterable $factories)
27 | {
28 | }
29 |
30 | public function get(string $class): Factory
31 | {
32 | foreach ($this->factories as $factory) {
33 | if ($class === $factory::class) {
34 | return $factory; // @phpstan-ignore return.type
35 | }
36 | }
37 |
38 | try {
39 | return new $class();
40 | } catch (\ArgumentCountError $e) {
41 | throw CannotCreateFactory::argumentCountError($e);
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/FactoryRegistryInterface.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Foundry;
13 |
14 | /**
15 | * @author Nicolas PHILIPPE
16 | *
17 | * @internal
18 | */
19 | interface FactoryRegistryInterface
20 | {
21 | /**
22 | * @template T of Factory
23 | *
24 | * @param class-string $class
25 | *
26 | * @return T
27 | */
28 | public function get(string $class): Factory;
29 | }
30 |
--------------------------------------------------------------------------------
/src/ForceValue.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Foundry;
13 |
14 | /**
15 | * @author NIcolas PHILIPPE
16 | *
17 | * @internal
18 | */
19 | final class ForceValue
20 | {
21 | public function __construct(public readonly mixed $value)
22 | {
23 | }
24 |
25 | /**
26 | * @param array $what
27 | * @return array
28 | */
29 | public static function unwrap(mixed $what): mixed
30 | {
31 | if (\is_array($what)) {
32 | return \array_map(
33 | self::unwrap(...),
34 | $what
35 | );
36 | }
37 |
38 | return $what instanceof self ? $what->value : $what;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/InMemory/AsInMemoryTest.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Zenstruck\Foundry\InMemory;
15 |
16 | /**
17 | * @author Nicolas PHILIPPE
18 | * @experimental
19 | */
20 | #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)]
21 | final class AsInMemoryTest
22 | {
23 | /**
24 | * @param class-string $class
25 | * @internal
26 | */
27 | public static function shouldEnableInMemory(string $class, string $method): bool
28 | {
29 | $classReflection = new \ReflectionClass($class);
30 |
31 | if ($classReflection->getAttributes(static::class)) {
32 | return true;
33 | }
34 |
35 | return (bool) $classReflection->getMethod($method)->getAttributes(static::class);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/InMemory/CannotEnableInMemory.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Zenstruck\Foundry\InMemory;
15 |
16 | final class CannotEnableInMemory extends \LogicException
17 | {
18 | public static function testIsNotAKernelTestCase(string $testName): self
19 | {
20 | return new self("{$testName}: Cannot use the #[AsInMemoryTest] attribute without extending KernelTestCase.");
21 | }
22 |
23 | public static function noInMemoryRepositoryRegistry(): self
24 | {
25 | return new self('Cannot enable "in memory": maybe not in a KernelTestCase?');
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/InMemory/DependencyInjection/InMemoryCompilerPass.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Zenstruck\Foundry\InMemory\DependencyInjection;
15 |
16 | use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
17 | use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
18 | use Symfony\Component\DependencyInjection\ContainerBuilder;
19 | use Symfony\Component\DependencyInjection\Reference;
20 | use Zenstruck\Foundry\InMemory\InMemoryRepository;
21 |
22 | /**
23 | * @internal
24 | * @author Nicolas PHILIPPE
25 | */
26 | final class InMemoryCompilerPass implements CompilerPassInterface
27 | {
28 | public function process(ContainerBuilder $container): void
29 | {
30 | // create a service locator with all "in memory" repositories, indexed by target class
31 | $inMemoryRepositoriesServices = $container->findTaggedServiceIds('foundry.in_memory.repository');
32 | $inMemoryRepositoriesLocator = ServiceLocatorTagPass::register(
33 | $container,
34 | \array_combine(
35 | \array_map(
36 | static function(string $serviceId) use ($container) {
37 | /** @var class-string> $inMemoryRepositoryClass */
38 | $inMemoryRepositoryClass = $container->getDefinition($serviceId)->getClass() ?? throw new \LogicException("Service \"{$serviceId}\" should have a class.");
39 |
40 | return $inMemoryRepositoryClass::_class();
41 | },
42 | \array_keys($inMemoryRepositoriesServices)
43 | ),
44 | \array_map(
45 | static fn(string $inMemoryRepositoryId) => new Reference($inMemoryRepositoryId),
46 | \array_keys($inMemoryRepositoriesServices)
47 | ),
48 | )
49 | );
50 |
51 | $container->findDefinition('.zenstruck_foundry.in_memory.repository_registry')
52 | ->setArgument('$inMemoryRepositories', $inMemoryRepositoriesLocator)
53 | ;
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/InMemory/GenericInMemoryRepository.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Zenstruck\Foundry\InMemory;
15 |
16 | /**
17 | * @template T of object
18 | * @implements InMemoryRepository
19 | * @author Nicolas PHILIPPE
20 | * @experimental
21 | *
22 | * This class will be used when a specific "in-memory" repository does not exist for a given class.
23 | */
24 | final class GenericInMemoryRepository implements InMemoryRepository
25 | {
26 | /**
27 | * @var list
28 | */
29 | private array $elements = [];
30 |
31 | /**
32 | * @param class-string $class
33 | */
34 | public function __construct(
35 | private readonly string $class,
36 | ) {
37 | }
38 |
39 | /**
40 | * @param T $item
41 | */
42 | public function _save(object $item): void
43 | {
44 | if (!$item instanceof $this->class) {
45 | throw new \InvalidArgumentException(\sprintf('Given object of class "%s" is not an instance of expected "%s"', $item::class, $this->class));
46 | }
47 |
48 | if (!\in_array($item, $this->elements, true)) {
49 | $this->elements[] = $item;
50 | }
51 | }
52 |
53 | public function _all(): array
54 | {
55 | return $this->elements;
56 | }
57 |
58 | public static function _class(): string
59 | {
60 | throw new \BadMethodCallException('This method should not be called on a GenericInMemoryRepository.');
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/InMemory/InMemoryDoctrineObjectRepositoryAdapter.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Foundry\InMemory;
13 |
14 | use Doctrine\Persistence\ObjectRepository;
15 | use Zenstruck\Foundry\Configuration;
16 |
17 | use function Zenstruck\Foundry\get;
18 |
19 | /**
20 | * @template T of object
21 | * @implements ObjectRepository
22 | */
23 | final class InMemoryDoctrineObjectRepositoryAdapter implements ObjectRepository
24 | {
25 | /** @var InMemoryRepository */
26 | private InMemoryRepository $innerInMemoryRepo;
27 |
28 | /**
29 | * @internal
30 | *
31 | * @param class-string $class
32 | */
33 | public function __construct(private string $class)
34 | {
35 | if (!Configuration::instance()->isInMemoryEnabled()) {
36 | throw new \LogicException('In-memory repositories are not enabled.');
37 | }
38 |
39 | $this->innerInMemoryRepo = Configuration::instance()->inMemoryRepositoryRegistry->get($this->class);
40 | }
41 |
42 | public function find(mixed $id): ?object
43 | {
44 | throw new \BadMethodCallException('find() is not supported in in-memory repositories. Use findBy() instead.');
45 | }
46 |
47 | public function findAll(): array
48 | {
49 | return $this->innerInMemoryRepo->_all();
50 | }
51 |
52 | public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
53 | {
54 | $results = \array_filter(
55 | $this->innerInMemoryRepo->_all(),
56 | static function($o) use ($criteria) {
57 | foreach ($criteria as $key => $criterion) {
58 | if (get($o, $key) !== $criterion) {
59 | return false;
60 | }
61 | }
62 |
63 | return true;
64 | }
65 | );
66 |
67 | $results = \array_values($results);
68 |
69 | if ($orderBy) {
70 | if (\count($orderBy) > 1) {
71 | throw new \InvalidArgumentException('Order by multiple fields is not supported.');
72 | }
73 |
74 | $field = \array_key_first($orderBy);
75 | $direction = $orderBy[$field];
76 |
77 | if ('asc' === \mb_strtolower($direction)) {
78 | \usort($results, static fn($a, $b) => get($a, $field) <=> get($b, $field));
79 | } else {
80 | \usort($results, static fn($a, $b) => get($b, $field) <=> get($a, $field));
81 | }
82 | }
83 |
84 | if (null !== $offset) {
85 | $results = \array_slice($results, $offset);
86 | }
87 |
88 | if (null !== $limit) {
89 | $results = \array_slice($results, 0, $limit);
90 | }
91 |
92 | return $results;
93 | }
94 |
95 | public function findOneBy(array $criteria): ?object
96 | {
97 | return $this->findBy($criteria, limit: 1)[0] ?? null;
98 | }
99 |
100 | public function getClassName(): string
101 | {
102 | return $this->class;
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/src/InMemory/InMemoryFactoryRegistry.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Zenstruck\Foundry\InMemory;
15 |
16 | use Zenstruck\Foundry\Configuration;
17 | use Zenstruck\Foundry\Factory;
18 | use Zenstruck\Foundry\FactoryRegistryInterface;
19 | use Zenstruck\Foundry\ObjectFactory;
20 | use Zenstruck\Foundry\Persistence\PersistentObjectFactory;
21 |
22 | /**
23 | * @internal
24 | * @author Nicolas PHILIPPE
25 | */
26 | final class InMemoryFactoryRegistry implements FactoryRegistryInterface
27 | {
28 | public function __construct(
29 | private readonly FactoryRegistryInterface $decorated,
30 | ) {
31 | }
32 |
33 | /**
34 | * @template T of Factory
35 | *
36 | * @param class-string $class
37 | *
38 | * @return T
39 | */
40 | public function get(string $class): Factory
41 | {
42 | $factory = $this->decorated->get($class);
43 |
44 | if (!$factory instanceof ObjectFactory || !Configuration::instance()->isInMemoryEnabled()) {
45 | return $factory;
46 | }
47 |
48 | if ($factory instanceof PersistentObjectFactory) {
49 | $factory = $factory->withoutPersisting();
50 | }
51 |
52 | return $factory // @phpstan-ignore argument.templateType
53 | ->afterInstantiate(
54 | function(object $object) use ($factory) {
55 | Configuration::instance()->inMemoryRepositoryRegistry?->get($factory::class())->_save($object);
56 | }
57 | );
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/InMemory/InMemoryRepository.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Zenstruck\Foundry\InMemory;
15 |
16 | /**
17 | * @author Nicolas PHILIPPE
18 | *
19 | * @template T of object
20 | * @experimental
21 | */
22 | interface InMemoryRepository
23 | {
24 | /**
25 | * @return class-string
26 | */
27 | public static function _class(): string;
28 |
29 | /**
30 | * @param T $item
31 | */
32 | public function _save(object $item): void;
33 |
34 | /**
35 | * @return list
36 | */
37 | public function _all(): array;
38 | }
39 |
--------------------------------------------------------------------------------
/src/InMemory/InMemoryRepositoryRegistry.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Zenstruck\Foundry\InMemory;
15 |
16 | use Symfony\Component\DependencyInjection\ServiceLocator;
17 |
18 | /**
19 | * @internal
20 | * @author Nicolas PHILIPPE
21 | */
22 | final class InMemoryRepositoryRegistry
23 | {
24 | /**
25 | * @var array>
26 | */
27 | private array $genericInMemoryRepositories = [];
28 |
29 | public function __construct(
30 | /** @var ServiceLocator> */
31 | private readonly ServiceLocator $inMemoryRepositories,
32 | ) {
33 | }
34 |
35 | /**
36 | * @template T of object
37 | *
38 | * @param class-string $class
39 | *
40 | * @return InMemoryRepository
41 | */
42 | public function get(string $class): InMemoryRepository
43 | {
44 | if (!$this->inMemoryRepositories->has($class)) {
45 | return $this->genericInMemoryRepositories[$class] ??= new GenericInMemoryRepository($class); // @phpstan-ignore return.type
46 | }
47 |
48 | return $this->inMemoryRepositories->get($class); // @phpstan-ignore return.type
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/InMemory/InMemoryRepositoryTrait.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Zenstruck\Foundry\InMemory;
15 |
16 | /**
17 | * @template T of object
18 | * @phpstan-require-implements InMemoryRepository
19 | * @experimental
20 | *
21 | * @author Nicolas PHILIPPE
22 | */
23 | trait InMemoryRepositoryTrait
24 | {
25 | /**
26 | * @var list
27 | */
28 | private array $items = [];
29 |
30 | /**
31 | * @param T $item
32 | */
33 | public function _save(object $item): void
34 | {
35 | if (!\is_a($item, self::_class(), allow_string: true)) {
36 | throw new \InvalidArgumentException(\sprintf('Given object of class "%s" is not an instance of expected "%s"', $item::class, self::_class()));
37 | }
38 |
39 | if (!\in_array($item, $this->items, true)) {
40 | $this->items[] = $item;
41 | }
42 | }
43 |
44 | /**
45 | * @return list
46 | */
47 | public function _all(): array
48 | {
49 | return $this->items;
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/LazyValue.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Foundry;
13 |
14 | /**
15 | * @author Kevin Bond
16 | */
17 | final class LazyValue
18 | {
19 | /** @var \Closure():mixed */
20 | private \Closure $factory;
21 | private mixed $memoizedValue = null;
22 |
23 | /**
24 | * @param callable():mixed $factory
25 | */
26 | private function __construct(callable $factory, private bool $memoize = false)
27 | {
28 | $this->factory = $factory(...);
29 | }
30 |
31 | /**
32 | * @internal
33 | */
34 | public function __invoke(): mixed
35 | {
36 | if ($this->memoize && isset($this->memoizedValue)) {
37 | return $this->memoizedValue;
38 | }
39 |
40 | $value = ($this->factory)();
41 |
42 | if ($value instanceof self) {
43 | $value = ($value)();
44 | }
45 |
46 | if (\is_array($value)) {
47 | $value = self::normalizeArray($value);
48 | }
49 |
50 | if ($this->memoize) {
51 | return $this->memoizedValue = $value;
52 | }
53 |
54 | return $value;
55 | }
56 |
57 | /**
58 | * @param callable():mixed $factory
59 | */
60 | public static function new(callable $factory): self
61 | {
62 | return new self($factory, false);
63 | }
64 |
65 | /**
66 | * @param callable():mixed $factory
67 | */
68 | public static function memoize(callable $factory): self
69 | {
70 | return new self($factory, true);
71 | }
72 |
73 | /**
74 | * @param array $value
75 | * @return array
76 | */
77 | private static function normalizeArray(array $value): array
78 | {
79 | \array_walk_recursive($value, static function(mixed &$v): void {
80 | if ($v instanceof self) {
81 | $v = $v();
82 | }
83 | });
84 |
85 | return $value;
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/Maker/Factory/AbstractDefaultPropertyGuesser.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Foundry\Maker\Factory;
13 |
14 | use Symfony\Bundle\MakerBundle\Str;
15 | use Symfony\Component\Console\Style\SymfonyStyle;
16 |
17 | /**
18 | * @internal
19 | */
20 | abstract class AbstractDefaultPropertyGuesser implements DefaultPropertiesGuesser
21 | {
22 | public function __construct(private FactoryClassMap $factoryClassMap, private FactoryGenerator $factoryGenerator)
23 | {
24 | }
25 |
26 | /** @param class-string $fieldClass */
27 | protected function addDefaultValueUsingFactory(SymfonyStyle $io, MakeFactoryData $makeFactoryData, MakeFactoryQuery $makeFactoryQuery, string $fieldName, string $fieldClass): void
28 | {
29 | if (!$factoryClass = $this->factoryClassMap->getFactoryForClass($fieldClass)) {
30 | if ($makeFactoryQuery->generateAllFactories() || $io->confirm(
31 | "A factory for class \"{$fieldClass}\" is missing for field {$makeFactoryData->getObjectShortName()}::\${$fieldName}. Do you want to create it?",
32 | )) {
33 | $factoryClass = $this->factoryGenerator->generateFactory($io, $makeFactoryQuery->withClass($fieldClass));
34 | } else {
35 | $makeFactoryData->addDefaultProperty(\lcfirst($fieldName), "null, // TODO add {$fieldClass} type manually");
36 |
37 | return;
38 | }
39 | }
40 |
41 | $makeFactoryData->addUse($factoryClass);
42 |
43 | $factoryShortName = Str::getShortClassName($factoryClass);
44 | $makeFactoryData->addDefaultProperty(\lcfirst($fieldName), "{$factoryShortName}::new(),");
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/Maker/Factory/AbstractDoctrineDefaultPropertiesGuesser.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Foundry\Maker\Factory;
13 |
14 | use Doctrine\Persistence\Mapping\ClassMetadata;
15 | use Zenstruck\Foundry\Persistence\PersistenceManager;
16 |
17 | /**
18 | * @internal
19 | */
20 | abstract class AbstractDoctrineDefaultPropertiesGuesser extends AbstractDefaultPropertyGuesser
21 | {
22 | public function __construct(protected PersistenceManager $persistenceManager, FactoryClassMap $factoryClassMap, FactoryGenerator $factoryGenerator)
23 | {
24 | parent::__construct($factoryClassMap, $factoryGenerator);
25 | }
26 |
27 | protected function getClassMetadata(MakeFactoryData $makeFactoryData): ClassMetadata
28 | {
29 | $class = $makeFactoryData->getObjectFullyQualifiedClassName();
30 |
31 | return $this->persistenceManager->metadataFor($class);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Maker/Factory/DefaultPropertiesGuesser.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Foundry\Maker\Factory;
13 |
14 | use Symfony\Component\Console\Style\SymfonyStyle;
15 |
16 | /**
17 | * @internal
18 | */
19 | interface DefaultPropertiesGuesser
20 | {
21 | public function __invoke(SymfonyStyle $io, MakeFactoryData $makeFactoryData, MakeFactoryQuery $makeFactoryQuery): void;
22 |
23 | public function supports(MakeFactoryData $makeFactoryData): bool;
24 | }
25 |
--------------------------------------------------------------------------------
/src/Maker/Factory/DoctrineScalarFieldsDefaultPropertiesGuesser.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Foundry\Maker\Factory;
13 |
14 | use Doctrine\ODM\MongoDB\Mapping\ClassMetadata as ODMClassMetadata;
15 | use Doctrine\ORM\Mapping\ClassMetadata as ORMClassMetadata;
16 | use Doctrine\ORM\Mapping\FieldMapping;
17 | use Symfony\Component\Console\Style\SymfonyStyle;
18 |
19 | /**
20 | * @internal
21 | */
22 | final class DoctrineScalarFieldsDefaultPropertiesGuesser extends AbstractDoctrineDefaultPropertiesGuesser
23 | {
24 | private const DEFAULTS = [
25 | 'ARRAY' => '[],',
26 | 'ASCII_STRING' => 'self::faker()->text({length}),',
27 | 'BIGINT' => 'self::faker()->randomNumber(),',
28 | 'BLOB' => 'self::faker()->text(),',
29 | 'BOOLEAN' => 'self::faker()->boolean(),',
30 | 'DATE' => 'self::faker()->dateTime(),',
31 | 'DATE_MUTABLE' => 'self::faker()->dateTime(),',
32 | 'DATE_IMMUTABLE' => '\DateTimeImmutable::createFromMutable(self::faker()->dateTime()),',
33 | 'DATETIME' => 'self::faker()->dateTime(),',
34 | 'DATETIME_MUTABLE' => 'self::faker()->dateTime(),',
35 | 'DATETIME_IMMUTABLE' => '\DateTimeImmutable::createFromMutable(self::faker()->dateTime()),',
36 | 'DATETIMETZ_MUTABLE' => 'self::faker()->dateTime(),',
37 | 'DATETIMETZ_IMMUTABLE' => '\DateTimeImmutable::createFromMutable(self::faker()->dateTime()),',
38 | 'DECIMAL' => 'self::faker()->randomFloat(),',
39 | 'FLOAT' => 'self::faker()->randomFloat(),',
40 | 'INTEGER' => 'self::faker()->randomNumber(),',
41 | 'INT' => 'self::faker()->randomNumber(),',
42 | 'JSON' => '[],',
43 | 'JSON_ARRAY' => '[],',
44 | 'SIMPLE_ARRAY' => '[],',
45 | 'SMALLINT' => 'self::faker()->numberBetween(1, 32767),',
46 | 'STRING' => 'self::faker()->text({length}),',
47 | 'TEXT' => 'self::faker()->text({length}),',
48 | 'TIME_MUTABLE' => 'self::faker()->datetime(),',
49 | 'TIME_IMMUTABLE' => '\DateTimeImmutable::createFromMutable(self::faker()->datetime()),',
50 | ];
51 |
52 | public function __invoke(SymfonyStyle $io, MakeFactoryData $makeFactoryData, MakeFactoryQuery $makeFactoryQuery): void
53 | {
54 | /** @var ODMClassMetadata|ORMClassMetadata $metadata */
55 | $metadata = $this->getClassMetadata($makeFactoryData);
56 |
57 | $ids = $metadata->getIdentifierFieldNames();
58 |
59 | foreach ($metadata->fieldMappings as $property) {
60 | if (\is_array($property) && ($property['embedded'] ?? false)) {
61 | // skip ODM embedded
62 | continue;
63 | }
64 |
65 | $fieldName = $this->extractFieldMappingData($property, 'fieldName');
66 |
67 | if (\str_contains($fieldName, '.')) {
68 | // this is a "subfield" of an ORM embeddable field.
69 | continue;
70 | }
71 |
72 | // ignore identifiers and nullable fields
73 | if ((!$makeFactoryQuery->isAllFields() && $this->extractFieldMappingData($property, 'nullable', false)) || \in_array($fieldName, $ids, true)) {
74 | continue;
75 | }
76 |
77 | $type = \mb_strtoupper($this->extractFieldMappingData($property, 'type'));
78 | if ($this->extractFieldMappingData($property, 'enumType')) {
79 | $makeFactoryData->addEnumDefaultProperty($fieldName, $this->extractFieldMappingData($property, 'enumType'));
80 |
81 | continue;
82 | }
83 |
84 | $value = "null, // TODO add {$type} type manually";
85 | $length = $this->extractFieldMappingData($property, 'length', '');
86 |
87 | if (\array_key_exists($type, self::DEFAULTS)) {
88 | $value = self::DEFAULTS[$type];
89 | }
90 |
91 | $makeFactoryData->addDefaultProperty($fieldName, \str_replace('{length}', (string) $length, $value));
92 | }
93 | }
94 |
95 | public function supports(MakeFactoryData $makeFactoryData): bool
96 | {
97 | return $makeFactoryData->isPersisted();
98 | }
99 |
100 | // handles both ORM 3 & 4
101 | private function extractFieldMappingData(FieldMapping|array $fieldMapping, string $field, mixed $default = null): mixed
102 | {
103 | if ($fieldMapping instanceof FieldMapping) {
104 | return $fieldMapping->{$field};
105 | } else {
106 | return $fieldMapping[$field] ?? $default;
107 | }
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/src/Maker/Factory/Exception/FactoryClassAlreadyExistException.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Zenstruck\Foundry\Maker\Factory\Exception;
15 |
16 | final class FactoryClassAlreadyExistException extends \InvalidArgumentException
17 | {
18 | public function __construct(string $factoryClass)
19 | {
20 | parent::__construct("Factory \"{$factoryClass}\" already exists.");
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/Maker/Factory/FactoryCandidatesClassesExtractor.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Foundry\Maker\Factory;
13 |
14 | use Doctrine\ORM\Mapping\ClassMetadata as ORMClassMetadata;
15 | use Doctrine\Persistence\Mapping\ClassMetadata;
16 | use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException;
17 | use Zenstruck\Foundry\Persistence\PersistenceManager;
18 |
19 | /**
20 | * @internal
21 | */
22 | final class FactoryCandidatesClassesExtractor
23 | {
24 | public function __construct(private ?PersistenceManager $persistenceManager, private FactoryClassMap $factoryClassMap)
25 | {
26 | }
27 |
28 | /**
29 | * @return list
30 | */
31 | public function factoryCandidatesClasses(): array
32 | {
33 | $choices = [];
34 |
35 | $embeddedClasses = [];
36 |
37 | foreach ($this->persistenceManager?->allMetadata() ?? [] as $metadata) {
38 | if ($metadata->getReflectionClass()->isAbstract()) {
39 | continue;
40 | }
41 |
42 | if (!$this->factoryClassMap->classHasFactory($metadata->getName())) {
43 | $choices[] = $metadata->getName();
44 | }
45 |
46 | $embeddedClasses[] = $this->findEmbeddedClasses($metadata);
47 | }
48 |
49 | $choices = [
50 | ...$choices,
51 | ...\array_values(\array_unique(\array_merge(...$embeddedClasses))),
52 | ];
53 |
54 | \sort($choices);
55 |
56 | if (empty($choices)) {
57 | throw new RuntimeCommandException('No entities or documents found, or none left to make factories for.');
58 | }
59 |
60 | return $choices;
61 | }
62 |
63 | /**
64 | * @return list
65 | */
66 | private function findEmbeddedClasses(ClassMetadata $metadata): array
67 | {
68 | // - Doctrine ORM embedded objects does NOT have metadata classes, so we have to find all embedded classes inside entities
69 | // - Doctrine ODM embedded objects HAVE metadata classes, so they are already returned by factoryCandidatesClasses()
70 | return match (true) {
71 | $metadata instanceof ORMClassMetadata => \array_column($metadata->embeddedClasses, 'class'),
72 | default => [],
73 | };
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/Maker/Factory/FactoryClassMap.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Foundry\Maker\Factory;
13 |
14 | use Zenstruck\Foundry\Factory;
15 | use Zenstruck\Foundry\Maker\Factory\Exception\FactoryClassAlreadyExistException;
16 | use Zenstruck\Foundry\ObjectFactory;
17 |
18 | /**
19 | * @internal
20 | */
21 | final class FactoryClassMap
22 | {
23 | /**
24 | * @var array factory classes as keys, object class as values
25 | */
26 | private array $classesWithFactories;
27 |
28 | /** @param \Traversable $factories */
29 | public function __construct(\Traversable $factories)
30 | {
31 | $this->classesWithFactories = \array_unique(
32 | \array_reduce(
33 | \array_filter(\iterator_to_array($factories), static fn(Factory $f) => $f instanceof ObjectFactory),
34 | static function(array $carry, ObjectFactory $factory): array {
35 | $carry[$factory::class] = $factory::class();
36 |
37 | return $carry;
38 | },
39 | [],
40 | ),
41 | );
42 | }
43 |
44 | /** @param class-string $class */
45 | public function classHasFactory(string $class): bool
46 | {
47 | return \in_array($class, $this->classesWithFactories, true);
48 | }
49 |
50 | /**
51 | * @param class-string $class
52 | *
53 | * @return class-string|null
54 | */
55 | public function getFactoryForClass(string $class): ?string
56 | {
57 | $factories = \array_flip($this->classesWithFactories);
58 |
59 | return $factories[$class] ?? null;
60 | }
61 |
62 | /**
63 | * @param class-string $factoryClass
64 | * @param class-string $class
65 | */
66 | public function addFactoryForClass(string $factoryClass, string $class): void
67 | {
68 | if (\array_key_exists($factoryClass, $this->classesWithFactories)) {
69 | throw new FactoryClassAlreadyExistException($factoryClass);
70 | }
71 |
72 | $this->classesWithFactories[$factoryClass] = $class;
73 | }
74 |
75 | public function factoryClassExists(string $factoryClass): bool
76 | {
77 | return \array_key_exists($factoryClass, $this->classesWithFactories);
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/Maker/Factory/LegacyORMDefaultPropertiesGuesser.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Zenstruck\Foundry\Maker\Factory;
15 |
16 | use Doctrine\ORM\Mapping\ClassMetadataInfo as ORMClassMetadata;
17 | use Symfony\Component\Console\Style\SymfonyStyle;
18 | use Zenstruck\Foundry\ORM\DoctrineOrmVersionGuesser;
19 | use Zenstruck\Foundry\Persistence\Exception\NoPersistenceStrategy;
20 |
21 | /**
22 | * @internal
23 | * @see ORMDefaultPropertiesGuesser
24 | *
25 | * This file is basically a copy/paste of ORMDefaultPropertiesGuesser, but offers doctrine/orm 2 compatibility
26 | */
27 | final class LegacyORMDefaultPropertiesGuesser extends AbstractDoctrineDefaultPropertiesGuesser
28 | {
29 | public function __invoke(SymfonyStyle $io, MakeFactoryData $makeFactoryData, MakeFactoryQuery $makeFactoryQuery): void
30 | {
31 | if (DoctrineOrmVersionGuesser::isOrmV3()) {
32 | return;
33 | }
34 |
35 | $metadata = $this->getClassMetadata($makeFactoryData);
36 |
37 | if (!$metadata instanceof ORMClassMetadata) {
38 | throw new \InvalidArgumentException("\"{$makeFactoryData->getObjectFullyQualifiedClassName()}\" is not a valid ORM class.");
39 | }
40 |
41 | $this->guessDefaultValueForORMAssociativeFields($io, $makeFactoryData, $makeFactoryQuery, $metadata);
42 | $this->guessDefaultValueForEmbedded($io, $makeFactoryData, $makeFactoryQuery, $metadata);
43 | }
44 |
45 | public function supports(MakeFactoryData $makeFactoryData): bool
46 | {
47 | try {
48 | $metadata = $this->getClassMetadata($makeFactoryData);
49 |
50 | return $metadata instanceof ORMClassMetadata;
51 | } catch (NoPersistenceStrategy) {
52 | return false;
53 | }
54 | }
55 |
56 | private function guessDefaultValueForORMAssociativeFields(SymfonyStyle $io, MakeFactoryData $makeFactoryData, MakeFactoryQuery $makeFactoryQuery, ORMClassMetadata $metadata): void
57 | {
58 | foreach ($metadata->associationMappings as $item) {
59 | // if joinColumns is not written entity is default nullable ($nullable = true;)
60 | if (true === ($item['joinColumns'][0]['nullable'] ?? true)) {
61 | continue;
62 | }
63 |
64 | if (isset($item['mappedBy']) || isset($item['joinTable'])) {
65 | // we don't want to add defaults for X-To-Many relationships
66 | continue;
67 | }
68 |
69 | $this->addDefaultValueUsingFactory($io, $makeFactoryData, $makeFactoryQuery, $item['fieldName'], $item['targetEntity']);
70 | }
71 | }
72 |
73 | private function guessDefaultValueForEmbedded(SymfonyStyle $io, MakeFactoryData $makeFactoryData, MakeFactoryQuery $makeFactoryQuery, ORMClassMetadata $metadata): void
74 | {
75 | foreach ($metadata->embeddedClasses as $fieldName => $item) {
76 | $isNullable = $makeFactoryData->getObject()->getProperty($fieldName)->getType()?->allowsNull() ?? true;
77 |
78 | if (!$makeFactoryQuery->isAllFields() && $isNullable) {
79 | continue;
80 | }
81 |
82 | $this->addDefaultValueUsingFactory($io, $makeFactoryData, $makeFactoryQuery, $fieldName, $item['class']);
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/Maker/Factory/MakeFactoryPHPDocMethod.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Foundry\Maker\Factory;
13 |
14 | use Doctrine\ODM\MongoDB\Repository\DocumentRepository;
15 |
16 | /**
17 | * @internal
18 | */
19 | final class MakeFactoryPHPDocMethod
20 | {
21 | // @phpstan-ignore-next-line
22 | public function __construct(private string $objectName, private string $prototype, private bool $returnsCollection, private bool $isStatic = true, private ?\ReflectionClass $repository = null)
23 | {
24 | }
25 |
26 | /** @return non-empty-list */
27 | public static function createAll(MakeFactoryData $makeFactoryData): array
28 | {
29 | $methods = [];
30 |
31 | $methods[] = new self($makeFactoryData->getObjectShortName(), 'create(array|callable $attributes = [])', returnsCollection: false, isStatic: false);
32 | $methods[] = new self($makeFactoryData->getObjectShortName(), 'createOne(array $attributes = [])', returnsCollection: false);
33 |
34 | $methods[] = new self($makeFactoryData->getObjectShortName(), 'createMany(int $number, array|callable $attributes = [])', returnsCollection: true);
35 | $methods[] = new self($makeFactoryData->getObjectShortName(), 'createSequence(iterable|callable $sequence)', returnsCollection: true);
36 |
37 | if ($makeFactoryData->isPersisted()) {
38 | $methods[] = new self($makeFactoryData->getObjectShortName(), 'find(object|array|mixed $criteria)', returnsCollection: false);
39 | $methods[] = new self($makeFactoryData->getObjectShortName(), 'findOrCreate(array $attributes)', returnsCollection: false);
40 | $methods[] = new self($makeFactoryData->getObjectShortName(), 'first(string $sortBy = \'id\')', returnsCollection: false);
41 | $methods[] = new self($makeFactoryData->getObjectShortName(), 'last(string $sortBy = \'id\')', returnsCollection: false);
42 | $methods[] = new self($makeFactoryData->getObjectShortName(), 'random(array $attributes = [])', returnsCollection: false);
43 | $methods[] = new self($makeFactoryData->getObjectShortName(), 'randomOrCreate(array $attributes = [])', returnsCollection: false);
44 |
45 | $methods[] = new self($makeFactoryData->getObjectShortName(), 'all()', returnsCollection: true);
46 | $methods[] = new self($makeFactoryData->getObjectShortName(), 'findBy(array $attributes)', returnsCollection: true);
47 | $methods[] = new self($makeFactoryData->getObjectShortName(), 'randomRange(int $min, int $max, array $attributes = [])', returnsCollection: true);
48 | $methods[] = new self($makeFactoryData->getObjectShortName(), 'randomSet(int $number, array $attributes = [])', returnsCollection: true);
49 |
50 | if (null !== $makeFactoryData->getRepositoryReflectionClass()) {
51 | $methods[] = new self($makeFactoryData->getObjectShortName(), 'repository()', returnsCollection: false, repository: $makeFactoryData->getRepositoryReflectionClass());
52 | }
53 | }
54 |
55 | return $methods;
56 | }
57 |
58 | public function toString(?string $staticAnalysisTool = null): string
59 | {
60 | $annotation = $staticAnalysisTool ? "{$staticAnalysisTool}-method" : 'method';
61 | $static = $this->isStatic ? 'static' : ' ';
62 |
63 | if ($this->repository) {
64 | $returnType = match ((bool) $staticAnalysisTool) {
65 | false => "{$this->repository->getShortName()}|ProxyRepositoryDecorator",
66 | true => \sprintf(
67 | "ProxyRepositoryDecorator<{$this->objectName}, %s>",
68 | \is_a($this->repository->getName(), DocumentRepository::class, allow_string: true)
69 | ? "DocumentRepository<{$this->objectName}>"
70 | : "EntityRepository<{$this->objectName}>"
71 | ),
72 | };
73 | } else {
74 | $returnType = match ([$this->returnsCollection, (bool) $staticAnalysisTool]) {
75 | [true, true] => "list<{$this->objectName}&Proxy<{$this->objectName}>>",
76 | [true, false] => "{$this->objectName}[]|Proxy[]",
77 | [false, true] => "{$this->objectName}&Proxy<{$this->objectName}>",
78 | [false, false] => "{$this->objectName}|Proxy",
79 | };
80 | }
81 |
82 | return " * @{$annotation} {$static} {$returnType} {$this->prototype}";
83 | }
84 |
85 | public function sortValue(): string
86 | {
87 | return \sprintf(
88 | "returnsCollection:%s, prototype:{$this->prototype}",
89 | $this->returnsCollection ? '1' : '0',
90 | );
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/Maker/Factory/MakeFactoryQuery.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Foundry\Maker\Factory;
13 |
14 | use Symfony\Bundle\MakerBundle\Generator;
15 | use Symfony\Component\Console\Input\InputInterface;
16 |
17 | /**
18 | * @internal
19 | */
20 | final class MakeFactoryQuery
21 | {
22 | private function __construct(
23 | private string $namespace,
24 | private bool $test,
25 | private bool $persisted,
26 | private bool $allFields,
27 | private bool $withPhpDoc,
28 | private string $class,
29 | private bool $generateAllFactories,
30 | private bool $addHints,
31 | private Generator $generator,
32 | ) {
33 | }
34 |
35 | public static function fromInput(InputInterface $input, string $class, bool $generateAllFactories, Generator $generator, string $defaultNamespace, bool $addHints): self
36 | {
37 | return new self(
38 | namespace: $defaultNamespace,
39 | test: (bool) $input->getOption('test'),
40 | persisted: !$input->getOption('no-persistence'),
41 | allFields: (bool) $input->getOption('all-fields'),
42 | withPhpDoc: (bool) $input->getOption('with-phpdoc'),
43 | class: $class,
44 | generateAllFactories: $generateAllFactories,
45 | addHints: $addHints,
46 | generator: $generator,
47 | );
48 | }
49 |
50 | public function getNamespace(): string
51 | {
52 | return $this->namespace;
53 | }
54 |
55 | public function isTest(): bool
56 | {
57 | return $this->test;
58 | }
59 |
60 | public function isPersisted(): bool
61 | {
62 | return $this->persisted;
63 | }
64 |
65 | public function isAllFields(): bool
66 | {
67 | return $this->allFields;
68 | }
69 |
70 | public function addPhpDoc(): bool
71 | {
72 | return $this->withPhpDoc;
73 | }
74 |
75 | public function getClass(): string
76 | {
77 | return $this->class;
78 | }
79 |
80 | public function generateAllFactories(): bool
81 | {
82 | return $this->generateAllFactories;
83 | }
84 |
85 | public function getGenerator(): Generator
86 | {
87 | return $this->generator;
88 | }
89 |
90 | public function withClass(string $class): self
91 | {
92 | $clone = clone $this;
93 | $clone->class = $class;
94 |
95 | return $clone;
96 | }
97 |
98 | public function shouldAddHints(): bool
99 | {
100 | return $this->addHints;
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/src/Maker/Factory/NoPersistenceObjectsAutoCompleter.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Foundry\Maker\Factory;
13 |
14 | /**
15 | * @internal
16 | */
17 | final class NoPersistenceObjectsAutoCompleter
18 | {
19 | public function __construct(private string $kernelRootDir)
20 | {
21 | }
22 |
23 | /**
24 | * @return list
25 | */
26 | public function getAutocompleteValues(): array
27 | {
28 | $classes = [];
29 |
30 | $excludedFiles = $this->excludedFiles();
31 |
32 | foreach ($this->getDefinedNamespaces() as $namespacePrefix => $rootFragment) {
33 | $allFiles = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($rootPath = "{$this->kernelRootDir}/{$rootFragment}"));
34 |
35 | /** @var \SplFileInfo $phpFile */
36 | foreach (new \RegexIterator($allFiles, '/\.php$/') as $phpFile) {
37 | if (\in_array($phpFile->getRealPath(), $excludedFiles, true)) {
38 | continue;
39 | }
40 |
41 | $class = $this->toPSR4($rootPath, $phpFile, $namespacePrefix);
42 |
43 | if (\in_array($class, ['Zenstruck\Foundry\Proxy', 'Zenstruck\Foundry\RepositoryProxy', 'Zenstruck\Foundry\RepositoryAssertions'])) {
44 | // do not load legacy Proxy: prevents deprecations in tests.
45 | continue;
46 | }
47 |
48 | try {
49 | // @phpstan-ignore-next-line $class is not always a class-string
50 | $reflection = new \ReflectionClass($class);
51 | } catch (\Throwable) {
52 | // remove all files which are not class / interface / traits
53 | continue;
54 | }
55 |
56 | /** @var class-string $class */
57 | if (!$reflection->isInstantiable()) {
58 | // remove abstract classes / interfaces / traits
59 | continue;
60 | }
61 |
62 | $classes[] = $class;
63 | }
64 | }
65 |
66 | \sort($classes);
67 |
68 | return $classes;
69 | }
70 |
71 | private function toPSR4(string $rootPath, \SplFileInfo $fileInfo, string $namespacePrefix): string
72 | {
73 | // /app/src/Bundle/Maker/Factory/NoPersistenceObjectsAutoCompleter.php => /Bundle/Maker/Factory/NoPersistenceObjectsAutoCompleter
74 | $relativeFileNameWithoutExtension = \str_replace([$rootPath, '.php'], ['', ''], $fileInfo->getRealPath());
75 |
76 | return $namespacePrefix.\str_replace('/', '\\', $relativeFileNameWithoutExtension);
77 | }
78 |
79 | /**
80 | * @return array
81 | */
82 | private function getDefinedNamespaces(): array
83 | {
84 | $composerConfig = $this->getComposerConfiguration();
85 |
86 | /** @var array $definedNamespaces */
87 | $definedNamespaces = $composerConfig['autoload']['psr-4'] ?? [];
88 |
89 | return \array_combine(
90 | \array_map(
91 | static fn(string $namespacePrefix): string => \trim($namespacePrefix, '\\'),
92 | \array_keys($definedNamespaces),
93 | ),
94 | \array_map(
95 | static fn(string $rootFragment): string => \trim($rootFragment, '/'),
96 | \array_values($definedNamespaces),
97 | ),
98 | );
99 | }
100 |
101 | /**
102 | * @return array
103 | */
104 | private function excludedFiles(): array
105 | {
106 | $composerConfig = $this->getComposerConfiguration();
107 |
108 | return \array_map(
109 | fn(string $file): string => "{$this->kernelRootDir}/{$file}",
110 | $composerConfig['autoload']['files'] ?? [],
111 | );
112 | }
113 |
114 | /**
115 | * @return array
116 | */
117 | private function getComposerConfiguration(): array
118 | {
119 | $composerConfigFilePath = "{$this->kernelRootDir}/composer.json";
120 | if (!\is_file($composerConfigFilePath)) {
121 | return [];
122 | }
123 |
124 | return \json_decode((string) \file_get_contents($composerConfigFilePath), true, 512, \JSON_THROW_ON_ERROR);
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/src/Maker/Factory/ODMDefaultPropertiesGuesser.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Foundry\Maker\Factory;
13 |
14 | use Doctrine\ODM\MongoDB\Mapping\ClassMetadata as ODMClassMetadata;
15 | use Symfony\Component\Console\Style\SymfonyStyle;
16 | use Zenstruck\Foundry\Persistence\Exception\NoPersistenceStrategy;
17 |
18 | /**
19 | * @internal
20 | */
21 | class ODMDefaultPropertiesGuesser extends AbstractDoctrineDefaultPropertiesGuesser
22 | {
23 | public function __invoke(SymfonyStyle $io, MakeFactoryData $makeFactoryData, MakeFactoryQuery $makeFactoryQuery): void
24 | {
25 | $metadata = $this->getClassMetadata($makeFactoryData);
26 |
27 | if (!$metadata instanceof ODMClassMetadata) {
28 | throw new \InvalidArgumentException("\"{$makeFactoryData->getObjectFullyQualifiedClassName()}\" is not a valid ODM class.");
29 | }
30 |
31 | foreach ($metadata->associationMappings as $item) {
32 | // @phpstan-ignore-next-line
33 | if (!($item['embedded'] ?? false) || !($item['targetDocument'] ?? false)) {
34 | // foundry does not support ODM references
35 | continue;
36 | }
37 |
38 | /** @phpstan-ignore-next-line */
39 | $isMultiple = ODMClassMetadata::MANY === $item['type'];
40 | if ($isMultiple) {
41 | continue;
42 | }
43 |
44 | $fieldName = $item['fieldName']; // @phpstan-ignore class.notFound
45 |
46 | if (!$makeFactoryQuery->isAllFields() && $item['nullable']) { // @phpstan-ignore class.notFound
47 | continue;
48 | }
49 |
50 | $this->addDefaultValueUsingFactory($io, $makeFactoryData, $makeFactoryQuery, $fieldName, $item['targetDocument']); // @phpstan-ignore class.notFound
51 | }
52 | }
53 |
54 | public function supports(MakeFactoryData $makeFactoryData): bool
55 | {
56 | try {
57 | $metadata = $this->getClassMetadata($makeFactoryData);
58 |
59 | return $metadata instanceof ODMClassMetadata;
60 | } catch (NoPersistenceStrategy) {
61 | return false;
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/Maker/Factory/ORMDefaultPropertiesGuesser.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Foundry\Maker\Factory;
13 |
14 | use Doctrine\ORM\Mapping\ClassMetadata as ORMClassMetadata;
15 | use Doctrine\ORM\Mapping\ToOneAssociationMapping;
16 | use Symfony\Component\Console\Style\SymfonyStyle;
17 | use Zenstruck\Foundry\ORM\DoctrineOrmVersionGuesser;
18 | use Zenstruck\Foundry\Persistence\Exception\NoPersistenceStrategy;
19 |
20 | /**
21 | * @internal
22 | */
23 | final class ORMDefaultPropertiesGuesser extends AbstractDoctrineDefaultPropertiesGuesser
24 | {
25 | public function __invoke(SymfonyStyle $io, MakeFactoryData $makeFactoryData, MakeFactoryQuery $makeFactoryQuery): void
26 | {
27 | if (!DoctrineOrmVersionGuesser::isOrmV3()) {
28 | return;
29 | }
30 |
31 | $metadata = $this->getClassMetadata($makeFactoryData);
32 |
33 | if (!$metadata instanceof ORMClassMetadata) {
34 | throw new \InvalidArgumentException("\"{$makeFactoryData->getObjectFullyQualifiedClassName()}\" is not a valid ORM class.");
35 | }
36 |
37 | $this->guessDefaultValueForORMAssociativeFields($io, $makeFactoryData, $makeFactoryQuery, $metadata);
38 | $this->guessDefaultValueForEmbedded($io, $makeFactoryData, $makeFactoryQuery, $metadata);
39 | }
40 |
41 | public function supports(MakeFactoryData $makeFactoryData): bool
42 | {
43 | try {
44 | $metadata = $this->getClassMetadata($makeFactoryData);
45 |
46 | return $metadata instanceof ORMClassMetadata;
47 | } catch (NoPersistenceStrategy) {
48 | return false;
49 | }
50 | }
51 |
52 | private function guessDefaultValueForORMAssociativeFields(SymfonyStyle $io, MakeFactoryData $makeFactoryData, MakeFactoryQuery $makeFactoryQuery, ORMClassMetadata $metadata): void
53 | {
54 | foreach ($metadata->associationMappings as $item) {
55 | if (!$item instanceof ToOneAssociationMapping) {
56 | // we don't want to add defaults for X-To-Many relationships
57 | continue;
58 | }
59 |
60 | if ($item->joinColumns[0]->nullable ?? true) {
61 | continue;
62 | }
63 |
64 | $this->addDefaultValueUsingFactory($io, $makeFactoryData, $makeFactoryQuery, $item->fieldName, $item->targetEntity);
65 | }
66 | }
67 |
68 | private function guessDefaultValueForEmbedded(SymfonyStyle $io, MakeFactoryData $makeFactoryData, MakeFactoryQuery $makeFactoryQuery, ORMClassMetadata $metadata): void
69 | {
70 | foreach ($metadata->embeddedClasses as $fieldName => $item) {
71 | $isNullable = $makeFactoryData->getObject()->getProperty($fieldName)->getType()?->allowsNull() ?? true;
72 |
73 | if (!$makeFactoryQuery->isAllFields() && $isNullable) {
74 | continue;
75 | }
76 |
77 | $this->addDefaultValueUsingFactory($io, $makeFactoryData, $makeFactoryQuery, $fieldName, $item->class);
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/Maker/Factory/ObjectDefaultPropertiesGuesser.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Foundry\Maker\Factory;
13 |
14 | use Symfony\Component\Console\Style\SymfonyStyle;
15 |
16 | /**
17 | * @internal
18 | */
19 | class ObjectDefaultPropertiesGuesser extends AbstractDefaultPropertyGuesser
20 | {
21 | private const DEFAULTS_FOR_NOT_PERSISTED = [
22 | 'array' => '[],',
23 | 'string' => 'self::faker()->sentence(),',
24 | 'int' => 'self::faker()->randomNumber(),',
25 | 'float' => 'self::faker()->randomFloat(),',
26 | 'bool' => 'self::faker()->boolean(),',
27 | \DateTime::class => 'self::faker()->dateTime(),',
28 | \DateTimeImmutable::class => '\DateTimeImmutable::createFromMutable(self::faker()->dateTime()),',
29 | ];
30 |
31 | public function __invoke(SymfonyStyle $io, MakeFactoryData $makeFactoryData, MakeFactoryQuery $makeFactoryQuery): void
32 | {
33 | foreach ($makeFactoryData->getObject()->getProperties() as $property) {
34 | if (!$this->shouldAddPropertyToFactory($makeFactoryQuery, $property)) {
35 | continue;
36 | }
37 |
38 | $type = $this->getPropertyType($property) ?? '';
39 |
40 | $value = \sprintf('null, // TODO add %svalue manually', $type ? "{$type} " : '');
41 |
42 | if (\PHP_VERSION_ID >= 80100 && \enum_exists($type)) {
43 | $makeFactoryData->addEnumDefaultProperty($property->getName(), $type);
44 |
45 | continue;
46 | }
47 |
48 | if ($type && \class_exists($type) && !\is_a($type, \DateTimeInterface::class, true)) {
49 | $this->addDefaultValueUsingFactory($io, $makeFactoryData, $makeFactoryQuery, $property->getName(), $type);
50 |
51 | continue;
52 | }
53 |
54 | if (\array_key_exists($type, self::DEFAULTS_FOR_NOT_PERSISTED)) {
55 | $value = self::DEFAULTS_FOR_NOT_PERSISTED[$type];
56 | }
57 |
58 | $makeFactoryData->addDefaultProperty($property->getName(), $value);
59 | }
60 | }
61 |
62 | public function supports(MakeFactoryData $makeFactoryData): bool
63 | {
64 | return !$makeFactoryData->isPersisted();
65 | }
66 |
67 | private function shouldAddPropertyToFactory(MakeFactoryQuery $makeFactoryQuery, \ReflectionProperty $property): bool
68 | {
69 | // if option "--all-fields" was passed
70 | if ($makeFactoryQuery->isAllFields()) {
71 | return true;
72 | }
73 |
74 | // if property is inside constructor, check if it has a default value
75 | if ($constructorParameter = $this->getConstructorParameterForProperty($property)) {
76 | return !$constructorParameter->isDefaultValueAvailable();
77 | }
78 |
79 | // if the property has a default value, we should not add it to the factory
80 | if ($property->hasDefaultValue()) {
81 | return false;
82 | }
83 |
84 | // if property has type, we need to add it to the factory
85 | return $property->hasType();
86 | }
87 |
88 | private function getPropertyType(\ReflectionProperty $property): ?string
89 | {
90 | if (!$property->hasType()) {
91 | $type = $this->getConstructorParameterForProperty($property)?->getType();
92 | } else {
93 | $type = $property->getType();
94 | }
95 |
96 | if (!$type instanceof \ReflectionNamedType) {
97 | return null;
98 | }
99 |
100 | return $type->getName();
101 | }
102 |
103 | private function getConstructorParameterForProperty(\ReflectionProperty $property): ?\ReflectionParameter
104 | {
105 | if ($constructor = $property->getDeclaringClass()->getConstructor()) {
106 | foreach ($constructor->getParameters() as $parameter) {
107 | if ($parameter->getName() === $property->getName()) {
108 | return $parameter;
109 | }
110 | }
111 | }
112 |
113 | return null;
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/src/Maker/MakeStory.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Foundry\Maker;
13 |
14 | use Symfony\Bundle\MakerBundle\ConsoleStyle;
15 | use Symfony\Bundle\MakerBundle\DependencyBuilder;
16 | use Symfony\Bundle\MakerBundle\Generator;
17 | use Symfony\Bundle\MakerBundle\InputConfiguration;
18 | use Symfony\Bundle\MakerBundle\Maker\AbstractMaker;
19 | use Symfony\Bundle\MakerBundle\Validator;
20 | use Symfony\Component\Console\Command\Command;
21 | use Symfony\Component\Console\Input\InputArgument;
22 | use Symfony\Component\Console\Input\InputInterface;
23 | use Symfony\Component\Console\Input\InputOption;
24 |
25 | /**
26 | * @author Kevin Bond
27 | *
28 | * @internal
29 | */
30 | final class MakeStory extends AbstractMaker
31 | {
32 | public function __construct(
33 | private NamespaceGuesser $namespaceGuesser,
34 | private string $defaultNamespace,
35 | ) {
36 | }
37 |
38 | public static function getCommandName(): string
39 | {
40 | return 'make:story';
41 | }
42 |
43 | public static function getCommandDescription(): string
44 | {
45 | return 'Creates a Foundry story';
46 | }
47 |
48 | public function configureCommand(Command $command, InputConfiguration $inputConfig): void
49 | {
50 | $command
51 | ->setDescription(self::getCommandDescription())
52 | ->addArgument('name', InputArgument::OPTIONAL, 'The name of the story class (e.g. DefaultCategoriesStory>)')
53 | ->addOption('test', null, InputOption::VALUE_NONE, 'Create in tests/> instead of src/>')
54 | ->addOption('namespace', null, InputOption::VALUE_REQUIRED, 'Customize the namespace for generated factories')
55 | ;
56 |
57 | $inputConfig->setArgumentAsNonInteractive('name');
58 | }
59 |
60 | public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void
61 | {
62 | if ($input->getArgument('name')) {
63 | return;
64 | }
65 |
66 | if (!$input->getOption('test')) {
67 | $io->text('// Note: pass --test> if you want to generate stories in your tests/> directory');
68 | $io->newLine();
69 | }
70 |
71 | $argument = $command->getDefinition()->getArgument('name');
72 | $value = $io->ask($argument->getDescription(), null, static fn(?string $value = null): string => Validator::notBlank($value));
73 | $input->setArgument($argument->getName(), $value);
74 | }
75 |
76 | public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void
77 | {
78 | $class = $input->getArgument('name');
79 | $namespace = ($this->namespaceGuesser)($generator, $class, $input->getOption('namespace') ?? $this->defaultNamespace, $input->getOption('test'));
80 |
81 | $storyClassNameDetails = $generator->createClassNameDetails(
82 | $input->getArgument('name'),
83 | $namespace,
84 | 'Story'
85 | );
86 |
87 | $generator->generateClass(
88 | $storyClassNameDetails->getFullName(),
89 | __DIR__.'/../../skeleton/Story.tpl.php',
90 | []
91 | );
92 |
93 | $generator->writeChanges();
94 |
95 | $this->writeSuccessMessage($io);
96 |
97 | $io->text([
98 | 'Next: Open your story class and start customizing it.',
99 | 'Find the documentation at https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#stories',
100 | ]);
101 | }
102 |
103 | public function configureDependencies(DependencyBuilder $dependencies): void
104 | {
105 | // noop
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/src/Maker/NamespaceGuesser.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Zenstruck\Foundry\Maker;
15 |
16 | use Symfony\Bundle\MakerBundle\Generator;
17 | use Symfony\Bundle\MakerBundle\Str;
18 | use Zenstruck\Foundry\Persistence\PersistenceManager;
19 |
20 | /**
21 | * Guesses namespaces depending on:
22 | * - user input "--namespace": will be used as a prefix (after root namespace "App\")
23 | * - user input "--test": will add "Test\" just after root namespace
24 | * - doctrine mapping: if the original class is a doctrine object, will suffix the namespace relative to doctrine's mapping.
25 | *
26 | * @internal
27 | */
28 | final class NamespaceGuesser
29 | {
30 | /** @var list */
31 | private array $doctrineNamespaces;
32 |
33 | public function __construct(?PersistenceManager $persistenceManager)
34 | {
35 | $this->doctrineNamespaces = $persistenceManager?->managedNamespaces() ?? [];
36 | }
37 |
38 | public function __invoke(Generator $generator, string $originalClass, string $baseNamespace, bool $test): string
39 | {
40 | // strip maker's root namespace if set
41 | $baseNamespace = $this->stripRootNamespace($baseNamespace, $generator->getRootNamespace());
42 |
43 | $doctrineBasedNamespace = $this->namespaceSuffixFromDoctrineMapping($originalClass);
44 |
45 | if ($doctrineBasedNamespace) {
46 | $baseNamespace = "{$baseNamespace}\\{$doctrineBasedNamespace}";
47 | }
48 |
49 | // if creating in tests dir, ensure namespace prefixed with Tests\
50 | if ($test && 0 !== \mb_strpos($baseNamespace, 'Tests\\')) {
51 | $baseNamespace = 'Tests\\'.$baseNamespace;
52 | }
53 |
54 | return $baseNamespace;
55 | }
56 |
57 | private function namespaceSuffixFromDoctrineMapping(string $originalClass): ?string
58 | {
59 | $originalClassNamespace = Str::getNamespace($originalClass);
60 |
61 | foreach ($this->doctrineNamespaces as $doctrineNamespace) {
62 | if (\str_starts_with($originalClassNamespace, $doctrineNamespace)) {
63 | return $this->stripRootNamespace($originalClassNamespace, $doctrineNamespace);
64 | }
65 | }
66 |
67 | return null;
68 | }
69 |
70 | private static function stripRootNamespace(string $class, string $rootNamespace): string
71 | {
72 | if (0 === \mb_strpos($class, $rootNamespace)) {
73 | $class = \mb_substr($class, \mb_strlen($rootNamespace));
74 | }
75 |
76 | return \trim($class, '\\');
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/Mongo/MongoPersistenceStrategy.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Foundry\Mongo;
13 |
14 | use Doctrine\ODM\MongoDB\DocumentManager;
15 | use Doctrine\ODM\MongoDB\Mapping\MappingException as MongoMappingException;
16 | use Doctrine\Persistence\Mapping\MappingException;
17 | use Zenstruck\Foundry\Persistence\PersistenceStrategy;
18 |
19 | /**
20 | * @author Kevin Bond
21 | *
22 | * @internal
23 | *
24 | * @method DocumentManager objectManagerFor(string $class)
25 | * @method list objectManagers()
26 | */
27 | final class MongoPersistenceStrategy extends PersistenceStrategy
28 | {
29 | public function contains(object $object): bool
30 | {
31 | $dm = $this->objectManagerFor($object::class);
32 |
33 | return $dm->contains($object) && !$dm->getUnitOfWork()->isScheduledForInsert($object);
34 | }
35 |
36 | public function hasChanges(object $object): bool
37 | {
38 | $dm = $this->objectManagerFor($object::class);
39 |
40 | if (!$dm->contains($object)) {
41 | return false;
42 | }
43 |
44 | // cannot use UOW::recomputeSingleEntityChangeSet() here as it wrongly computes embedded objects as changed
45 | $dm->getUnitOfWork()->computeChangeSet($dm->getClassMetadata($object::class), $object);
46 |
47 | return (bool) $dm->getUnitOfWork()->getDocumentChangeSet($object);
48 | }
49 |
50 | public function truncate(string $class): void
51 | {
52 | $this->objectManagerFor($class)->getDocumentCollection($class)->deleteMany([]);
53 | }
54 |
55 | public function managedNamespaces(): array
56 | {
57 | $namespaces = [];
58 |
59 | foreach ($this->objectManagers() as $objectManager) {
60 | $namespaces[] = $objectManager->getConfiguration()->getDocumentNamespaces();
61 | }
62 |
63 | return \array_values(\array_merge(...$namespaces));
64 | }
65 |
66 | public function embeddablePropertiesFor(object $object, string $owner): ?array
67 | {
68 | try {
69 | $metadata = $this->objectManagerFor($owner)->getClassMetadata($object::class);
70 | } catch (MappingException|MongoMappingException) {
71 | return null;
72 | }
73 |
74 | if (!$metadata->isEmbeddedDocument) {
75 | return null;
76 | }
77 |
78 | $properties = [];
79 |
80 | foreach ($metadata->getFieldNames() as $field) {
81 | $properties[$field] = $metadata->getFieldValue($object, $field);
82 | }
83 |
84 | return $properties;
85 | }
86 |
87 | public function isEmbeddable(object $object): bool
88 | {
89 | return $this->objectManagerFor($object::class)->getClassMetadata($object::class)->isEmbeddedDocument;
90 | }
91 |
92 | public function isScheduledForInsert(object $object): bool
93 | {
94 | $uow = $this->objectManagerFor($object::class)->getUnitOfWork();
95 |
96 | return $uow->isScheduledForInsert($object) || $uow->isScheduledForUpsert($object);
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/Mongo/MongoResetter.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Zenstruck\Foundry\Mongo;
15 |
16 | use Zenstruck\Foundry\Persistence\ResetDatabase\BeforeEachTestResetter;
17 |
18 | interface MongoResetter extends BeforeEachTestResetter
19 | {
20 | }
21 |
--------------------------------------------------------------------------------
/src/Mongo/MongoSchemaResetter.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Zenstruck\Foundry\Mongo;
15 |
16 | use Symfony\Component\HttpKernel\KernelInterface;
17 |
18 | use function Zenstruck\Foundry\application;
19 | use function Zenstruck\Foundry\runCommand;
20 |
21 | /**
22 | * @internal
23 | * @author Nicolas PHILIPPE
24 | */
25 | final class MongoSchemaResetter implements MongoResetter
26 | {
27 | /**
28 | * @param list $managers
29 | */
30 | public function __construct(private array $managers)
31 | {
32 | }
33 |
34 | public function resetBeforeEachTest(KernelInterface $kernel): void
35 | {
36 | $application = application($kernel);
37 |
38 | foreach ($this->managers as $manager) {
39 | try {
40 | runCommand($application, "doctrine:mongodb:schema:drop --dm={$manager}");
41 | } catch (\Exception) {
42 | }
43 |
44 | runCommand($application, "doctrine:mongodb:schema:create --dm={$manager}");
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/ORM/AbstractORMPersistenceStrategy.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Foundry\ORM;
13 |
14 | use Doctrine\ORM\EntityManagerInterface;
15 | use Doctrine\ORM\Mapping\MappingException as ORMMappingException;
16 | use Doctrine\Persistence\Mapping\MappingException;
17 | use Zenstruck\Foundry\Persistence\PersistenceStrategy;
18 |
19 | /**
20 | * @author Kevin Bond
21 | *
22 | * @internal
23 | *
24 | * @method EntityManagerInterface objectManagerFor(string $class)
25 | * @method list objectManagers()
26 | */
27 | abstract class AbstractORMPersistenceStrategy extends PersistenceStrategy
28 | {
29 | final public function contains(object $object): bool
30 | {
31 | $em = $this->objectManagerFor($object::class);
32 |
33 | return $em->contains($object) && !$em->getUnitOfWork()->isScheduledForInsert($object);
34 | }
35 |
36 | final public function hasChanges(object $object): bool
37 | {
38 | $em = $this->objectManagerFor($object::class);
39 |
40 | if (!$em->contains($object)) {
41 | return false;
42 | }
43 |
44 | // we're cloning the UOW because computing change set has side effect
45 | $unitOfWork = clone $em->getUnitOfWork();
46 |
47 | // cannot use UOW::recomputeSingleEntityChangeSet() here as it wrongly computes embedded objects as changed
48 | $unitOfWork->computeChangeSet($em->getClassMetadata($object::class), $object);
49 |
50 | return (bool) $unitOfWork->getEntityChangeSet($object);
51 | }
52 |
53 | final public function truncate(string $class): void
54 | {
55 | $this->objectManagerFor($class)->createQuery("DELETE {$class} e")->execute();
56 | }
57 |
58 | final public function embeddablePropertiesFor(object $object, string $owner): ?array
59 | {
60 | try {
61 | $metadata = $this->objectManagerFor($owner)->getClassMetadata($object::class);
62 | } catch (MappingException|ORMMappingException) {
63 | return null;
64 | }
65 |
66 | if (!$metadata->isEmbeddedClass) {
67 | return null;
68 | }
69 |
70 | $properties = [];
71 |
72 | foreach ($metadata->getFieldNames() as $field) {
73 | $properties[$field] = $metadata->getFieldValue($object, $field);
74 | }
75 |
76 | return $properties;
77 | }
78 |
79 | final public function isEmbeddable(object $object): bool
80 | {
81 | return $this->objectManagerFor($object::class)->getClassMetadata($object::class)->isEmbeddedClass;
82 | }
83 |
84 | final public function isScheduledForInsert(object $object): bool
85 | {
86 | return $this->objectManagerFor($object::class)->getUnitOfWork()->isScheduledForInsert($object);
87 | }
88 |
89 | final public function managedNamespaces(): array
90 | {
91 | $namespaces = [];
92 |
93 | foreach ($this->objectManagers() as $objectManager) {
94 | $namespaces[] = $objectManager->getConfiguration()->getEntityNamespaces();
95 | }
96 |
97 | return \array_values(\array_merge(...$namespaces));
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/src/ORM/DoctrineOrmVersionGuesser.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Zenstruck\Foundry\ORM;
15 |
16 | use Doctrine\ORM\Mapping\FieldMapping;
17 |
18 | final class DoctrineOrmVersionGuesser
19 | {
20 | public static function isOrmV3(): bool
21 | {
22 | return \class_exists(FieldMapping::class);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/ORM/OrmV2PersistenceStrategy.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Zenstruck\Foundry\ORM;
15 |
16 | use Doctrine\ORM\Mapping\ClassMetadataInfo;
17 | use Doctrine\ORM\Mapping\MappingException as ORMMappingException;
18 | use Doctrine\Persistence\Mapping\MappingException;
19 | use Zenstruck\Foundry\Persistence\Relationship\ManyToOneRelationship;
20 | use Zenstruck\Foundry\Persistence\Relationship\OneToManyRelationship;
21 | use Zenstruck\Foundry\Persistence\Relationship\OneToOneRelationship;
22 | use Zenstruck\Foundry\Persistence\Relationship\RelationshipMetadata;
23 |
24 | /**
25 | * @internal
26 | *
27 | * @phpstan-import-type AssociationMapping from \Doctrine\ORM\Mapping\ClassMetadata
28 | */
29 | final class OrmV2PersistenceStrategy extends AbstractORMPersistenceStrategy
30 | {
31 | public function bidirectionalRelationshipMetadata(string $parent, string $child, string $field): ?RelationshipMetadata
32 | {
33 | $associationMapping = $this->getAssociationMapping($parent, $child, $field);
34 |
35 | if (null === $associationMapping) {
36 | return null;
37 | }
38 |
39 | if (!\is_a(
40 | $child,
41 | $associationMapping['targetEntity'],
42 | allow_string: true
43 | )) { // is_a() handles inheritance as well
44 | throw new \LogicException("Cannot find correct association named \"{$field}\" between classes [parent: \"{$parent}\", child: \"{$child}\"]");
45 | }
46 |
47 | $inverseField = $associationMapping['isOwningSide'] ? $associationMapping['inversedBy'] ?? null : $associationMapping['mappedBy'] ?? null;
48 |
49 | if (null === $inverseField) {
50 | return null;
51 | }
52 |
53 | return match (true) {
54 | ClassMetadataInfo::ONE_TO_MANY === $associationMapping['type'] => new OneToManyRelationship(
55 | inverseField: $inverseField,
56 | collectionIndexedBy: $associationMapping['indexBy'] ?? null
57 | ),
58 | ClassMetadataInfo::ONE_TO_ONE === $associationMapping['type'] => new OneToOneRelationship(
59 | inverseField: $inverseField,
60 | isOwning: $associationMapping['isOwningSide'] ?? false
61 | ),
62 | ClassMetadataInfo::MANY_TO_ONE === $associationMapping['type'] => new ManyToOneRelationship(
63 | inverseField: $inverseField,
64 | ),
65 | default => null,
66 | };
67 | }
68 |
69 | /**
70 | * @param class-string $entityClass
71 | * @return array[]|null
72 | * @phpstan-return AssociationMapping|null
73 | */
74 | private function getAssociationMapping(string $entityClass, string $targetEntity, string $field): ?array
75 | {
76 | try {
77 | $associationMapping = $this->objectManagerFor($entityClass)->getClassMetadata($entityClass)->getAssociationMapping($field);
78 | } catch (MappingException|ORMMappingException) {
79 | return null;
80 | }
81 |
82 | if (!\is_a($targetEntity, $associationMapping['targetEntity'], allow_string: true)) {
83 | return null;
84 | }
85 |
86 | return $associationMapping;
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/ORM/OrmV3PersistenceStrategy.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Zenstruck\Foundry\ORM;
15 |
16 | use Doctrine\ORM\Mapping\AssociationMapping;
17 | use Doctrine\ORM\Mapping\ManyToOneAssociationMapping;
18 | use Doctrine\ORM\Mapping\MappingException as ORMMappingException;
19 | use Doctrine\ORM\Mapping\OneToManyAssociationMapping;
20 | use Doctrine\ORM\Mapping\OneToOneAssociationMapping;
21 | use Doctrine\Persistence\Mapping\MappingException;
22 | use Zenstruck\Foundry\Persistence\Relationship\ManyToOneRelationship;
23 | use Zenstruck\Foundry\Persistence\Relationship\OneToManyRelationship;
24 | use Zenstruck\Foundry\Persistence\Relationship\OneToOneRelationship;
25 | use Zenstruck\Foundry\Persistence\Relationship\RelationshipMetadata;
26 |
27 | final class OrmV3PersistenceStrategy extends AbstractORMPersistenceStrategy
28 | {
29 | public function bidirectionalRelationshipMetadata(string $parent, string $child, string $field): ?RelationshipMetadata
30 | {
31 | $associationMapping = $this->getAssociationMapping($parent, $child, $field);
32 |
33 | if (null === $associationMapping) {
34 | return null;
35 | }
36 |
37 | if (!\is_a(
38 | $child,
39 | $associationMapping->targetEntity,
40 | allow_string: true
41 | )) { // is_a() handles inheritance as well
42 | throw new \LogicException("Cannot find correct association named \"{$field}\" between classes [parent: \"{$parent}\", child: \"{$child}\"]");
43 | }
44 |
45 | $inverseField = $associationMapping->isOwningSide() ? $associationMapping->inversedBy : $associationMapping->mappedBy;
46 |
47 | if (null === $inverseField) {
48 | return null;
49 | }
50 |
51 | return match (true) {
52 | $associationMapping instanceof OneToManyAssociationMapping => new OneToManyRelationship(
53 | inverseField: $inverseField,
54 | collectionIndexedBy: $associationMapping->isIndexed() ? $associationMapping->indexBy() : null
55 | ),
56 | $associationMapping instanceof OneToOneAssociationMapping => new OneToOneRelationship(
57 | inverseField: $inverseField,
58 | isOwning: $associationMapping->isOwningSide()
59 | ),
60 | $associationMapping instanceof ManyToOneAssociationMapping => new ManyToOneRelationship(
61 | inverseField: $inverseField,
62 | ),
63 | default => null,
64 | };
65 | }
66 |
67 | /**
68 | * @param class-string $entityClass
69 | */
70 | private function getAssociationMapping(string $entityClass, string $targetEntity, string $field): ?AssociationMapping
71 | {
72 | try {
73 | $associationMapping = $this->objectManagerFor($entityClass)->getClassMetadata($entityClass)->getAssociationMapping($field);
74 | } catch (MappingException|ORMMappingException) {
75 | return null;
76 | }
77 |
78 | if (!\is_a($targetEntity, $associationMapping->targetEntity, allow_string: true)) {
79 | return null;
80 | }
81 |
82 | return $associationMapping;
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/ORM/ResetDatabase/BaseOrmResetter.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Zenstruck\Foundry\ORM\ResetDatabase;
15 |
16 | use Doctrine\Bundle\DoctrineBundle\Registry;
17 | use Doctrine\DBAL\Connection;
18 | use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
19 | use Doctrine\DBAL\Platforms\SQLitePlatform;
20 | use Symfony\Bundle\FrameworkBundle\Console\Application;
21 | use Symfony\Component\Filesystem\Filesystem;
22 | use Symfony\Component\HttpKernel\KernelInterface;
23 |
24 | use function Zenstruck\Foundry\runCommand;
25 |
26 | /**
27 | * @author Nicolas PHILIPPE
28 | * @internal
29 | */
30 | abstract class BaseOrmResetter implements OrmResetter
31 | {
32 | private static bool $inFirstTest = true;
33 |
34 | /**
35 | * @param list $managers
36 | * @param list $connections
37 | */
38 | public function __construct(
39 | private readonly Registry $registry,
40 | protected readonly array $managers,
41 | protected readonly array $connections,
42 | ) {
43 | }
44 |
45 | final public function resetBeforeEachTest(KernelInterface $kernel): void
46 | {
47 | if (self::$inFirstTest) {
48 | self::$inFirstTest = false;
49 |
50 | return;
51 | }
52 |
53 | $this->doResetBeforeEachTest($kernel);
54 | }
55 |
56 | abstract protected function doResetBeforeEachTest(KernelInterface $kernel): void;
57 |
58 | final protected function dropAndResetDatabase(Application $application): void
59 | {
60 | foreach ($this->connections as $connectionName) {
61 | /** @var Connection $connection */
62 | $connection = $this->registry->getConnection($connectionName);
63 | $databasePlatform = $connection->getDatabasePlatform();
64 |
65 | if ($databasePlatform instanceof SQLitePlatform) {
66 | // we don't need to create the sqlite database - it's created when the schema is created
67 | // let's only drop the .db file
68 |
69 | $dbPath = $connection->getParams()['path'] ?? null;
70 | if ($dbPath && (new Filesystem())->exists($dbPath)) {
71 | \file_put_contents($dbPath, '');
72 | }
73 |
74 | continue;
75 | }
76 |
77 | if ($databasePlatform instanceof PostgreSQLPlatform) {
78 | // let's drop all connections to the database to be able to drop it
79 | $sql = 'SELECT pid, pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = current_database() AND pid <> pg_backend_pid()';
80 | runCommand($application, "dbal:run-sql --connection={$connectionName} '{$sql}'", canFail: true);
81 | }
82 |
83 | runCommand($application, "doctrine:database:drop --connection={$connectionName} --force --if-exists");
84 |
85 | runCommand($application, "doctrine:database:create --connection={$connectionName}");
86 | }
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/ORM/ResetDatabase/DamaDatabaseResetter.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Zenstruck\Foundry\ORM\ResetDatabase;
15 |
16 | use DAMA\DoctrineTestBundle\Doctrine\DBAL\StaticDriver;
17 | use Symfony\Component\HttpKernel\KernelInterface;
18 | use Zenstruck\Foundry\Configuration;
19 | use Zenstruck\Foundry\Persistence\PersistenceManager;
20 | use Zenstruck\Foundry\Persistence\ResetDatabase\ResetDatabaseManager;
21 |
22 | /**
23 | * @internal
24 | * @author Nicolas PHILIPPE
25 | */
26 | final class DamaDatabaseResetter implements OrmResetter
27 | {
28 | public function __construct(
29 | private OrmResetter $decorated,
30 | ) {
31 | }
32 |
33 | public function resetBeforeFirstTest(KernelInterface $kernel): void
34 | {
35 | $isDAMADoctrineTestBundleEnabled = ResetDatabaseManager::isDAMADoctrineTestBundleEnabled();
36 |
37 | if (!$isDAMADoctrineTestBundleEnabled) {
38 | $this->decorated->resetBeforeFirstTest($kernel);
39 |
40 | return;
41 | }
42 |
43 | // disable static connections for this operation
44 | StaticDriver::setKeepStaticConnections(false);
45 |
46 | $this->decorated->resetBeforeFirstTest($kernel);
47 |
48 | if (PersistenceManager::isOrmOnly()) {
49 | // add global stories so they are available after transaction rollback
50 | Configuration::instance()->stories->loadGlobalStories();
51 | }
52 |
53 | // shutdown kernel before re-enabling static connections
54 | // this would prevent any error if any ResetInterface execute sql queries (example: symfony/doctrine-messenger)
55 | $kernel->shutdown();
56 |
57 | // re-enable static connections
58 | StaticDriver::setKeepStaticConnections(true);
59 | }
60 |
61 | public function resetBeforeEachTest(KernelInterface $kernel): void
62 | {
63 | if (ResetDatabaseManager::isDAMADoctrineTestBundleEnabled()) {
64 | // not required as the DAMADoctrineTestBundle wraps each test in a transaction
65 | return;
66 | }
67 |
68 | $this->decorated->resetBeforeEachTest($kernel);
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/ORM/ResetDatabase/MigrateDatabaseResetter.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Zenstruck\Foundry\ORM\ResetDatabase;
15 |
16 | use Doctrine\Bundle\DoctrineBundle\Registry;
17 | use Symfony\Component\HttpKernel\KernelInterface;
18 |
19 | use function Zenstruck\Foundry\application;
20 | use function Zenstruck\Foundry\runCommand;
21 |
22 | /**
23 | * @internal
24 | * @author Nicolas PHILIPPE
25 | */
26 | final class MigrateDatabaseResetter extends BaseOrmResetter
27 | {
28 | /**
29 | * @param list $configurations
30 | */
31 | public function __construct(
32 | private readonly array $configurations,
33 | Registry $registry,
34 | array $managers,
35 | array $connections,
36 | ) {
37 | parent::__construct($registry, $managers, $connections);
38 | }
39 |
40 | public function resetBeforeFirstTest(KernelInterface $kernel): void
41 | {
42 | $this->resetWithMigration($kernel);
43 | }
44 |
45 | public function doResetBeforeEachTest(KernelInterface $kernel): void
46 | {
47 | $this->resetWithMigration($kernel);
48 | }
49 |
50 | private function resetWithMigration(KernelInterface $kernel): void
51 | {
52 | $application = application($kernel);
53 |
54 | $this->dropAndResetDatabase($application);
55 |
56 | if (!$this->configurations) {
57 | runCommand($application, 'doctrine:migrations:migrate');
58 |
59 | return;
60 | }
61 |
62 | foreach ($this->configurations as $configuration) {
63 | runCommand($application, "doctrine:migrations:migrate --configuration={$configuration}");
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/ORM/ResetDatabase/OrmResetter.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Zenstruck\Foundry\ORM\ResetDatabase;
15 |
16 | use Zenstruck\Foundry\Persistence\ResetDatabase\BeforeEachTestResetter;
17 | use Zenstruck\Foundry\Persistence\ResetDatabase\BeforeFirstTestResetter;
18 |
19 | /**
20 | * @author Nicolas PHILIPPE
21 | */
22 | interface OrmResetter extends BeforeFirstTestResetter, BeforeEachTestResetter
23 | {
24 | }
25 |
--------------------------------------------------------------------------------
/src/ORM/ResetDatabase/ResetDatabaseMode.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Zenstruck\Foundry\ORM\ResetDatabase;
15 |
16 | /**
17 | * @author Nicolas PHILIPPE
18 | */
19 | enum ResetDatabaseMode: string
20 | {
21 | case SCHEMA = 'schema';
22 | case MIGRATE = 'migrate';
23 | }
24 |
--------------------------------------------------------------------------------
/src/ORM/ResetDatabase/SchemaDatabaseResetter.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Zenstruck\Foundry\ORM\ResetDatabase;
15 |
16 | use Symfony\Bundle\FrameworkBundle\Console\Application;
17 | use Symfony\Component\HttpKernel\KernelInterface;
18 |
19 | use function Zenstruck\Foundry\application;
20 | use function Zenstruck\Foundry\runCommand;
21 |
22 | /**
23 | * @internal
24 | * @author Nicolas PHILIPPE
25 | */
26 | final class SchemaDatabaseResetter extends BaseOrmResetter
27 | {
28 | public function resetBeforeFirstTest(KernelInterface $kernel): void
29 | {
30 | $application = application($kernel);
31 |
32 | $this->dropAndResetDatabase($application);
33 | $this->createSchema($application);
34 | }
35 |
36 | protected function doResetBeforeEachTest(KernelInterface $kernel): void
37 | {
38 | $application = application($kernel);
39 |
40 | $this->dropSchema($application);
41 | $this->createSchema($application);
42 | }
43 |
44 | private function createSchema(Application $application): void
45 | {
46 | foreach ($this->managers as $manager) {
47 | runCommand($application, "doctrine:schema:update --em={$manager} --force -v");
48 | }
49 | }
50 |
51 | private function dropSchema(Application $application): void
52 | {
53 | foreach ($this->managers as $manager) {
54 | runCommand($application, "doctrine:schema:drop --em={$manager} --force --full-database");
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/PHPUnit/BootFoundryOnDataProviderMethodCalled.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Zenstruck\Foundry\PHPUnit;
15 |
16 | use PHPUnit\Event;
17 | use Zenstruck\Foundry\Configuration;
18 | use Zenstruck\Foundry\InMemory\AsInMemoryTest;
19 |
20 | /**
21 | * @internal
22 | * @author Nicolas PHILIPPE
23 | */
24 | final class BootFoundryOnDataProviderMethodCalled implements Event\Test\DataProviderMethodCalledSubscriber
25 | {
26 | public function notify(Event\Test\DataProviderMethodCalled $event): void
27 | {
28 | if (\method_exists($event->testMethod()->className(), '_bootForDataProvider')) {
29 | $event->testMethod()->className()::_bootForDataProvider();
30 | }
31 |
32 | $testMethod = $event->testMethod();
33 |
34 | if (AsInMemoryTest::shouldEnableInMemory($testMethod->className(), $testMethod->methodName())) {
35 | Configuration::instance()->enableInMemory();
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/PHPUnit/BuildStoryOnTestPrepared.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Zenstruck\Foundry\PHPUnit;
15 |
16 | use PHPUnit\Event;
17 | use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
18 | use Zenstruck\Foundry\Attribute\WithStory;
19 | use Zenstruck\Foundry\Exception\FactoriesTraitNotUsed;
20 |
21 | /**
22 | * @internal
23 | * @author Nicolas PHILIPPE
24 | */
25 | final class BuildStoryOnTestPrepared implements Event\Test\PreparedSubscriber
26 | {
27 | public function notify(Event\Test\Prepared $event): void
28 | {
29 | $test = $event->test();
30 |
31 | if (!$test->isTestMethod()) {
32 | return;
33 | }
34 |
35 | /** @var Event\Code\TestMethod $test */
36 | $reflectionClass = new \ReflectionClass($test->className());
37 | $withStoryAttributes = [
38 | ...$this->collectWithStoryAttributesFromClassAndParents($reflectionClass),
39 | ...$reflectionClass->getMethod($test->methodName())->getAttributes(WithStory::class),
40 | ];
41 |
42 | if (!$withStoryAttributes) {
43 | return;
44 | }
45 |
46 | if (!\is_subclass_of($test->className(), KernelTestCase::class)) {
47 | throw new \InvalidArgumentException(\sprintf('The test class "%s" must extend "%s" to use the "%s" attribute.', $test->className(), KernelTestCase::class, WithStory::class));
48 | }
49 |
50 | FactoriesTraitNotUsed::throwIfClassDoesNotHaveFactoriesTrait($test->className());
51 |
52 | foreach ($withStoryAttributes as $withStoryAttribute) {
53 | $withStoryAttribute->newInstance()->story::load();
54 | }
55 | }
56 |
57 | /**
58 | * @return list<\ReflectionAttribute>
59 | */
60 | private function collectWithStoryAttributesFromClassAndParents(\ReflectionClass $class): array // @phpstan-ignore missingType.generics
61 | {
62 | return [
63 | ...$class->getAttributes(WithStory::class),
64 | ...(
65 | $class->getParentClass()
66 | ? $this->collectWithStoryAttributesFromClassAndParents($class->getParentClass())
67 | : []
68 | ),
69 | ];
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/PHPUnit/DisplayFakerSeedOnTestSuiteFinished.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Zenstruck\Foundry\PHPUnit;
15 |
16 | use PHPUnit\Event\TestRunner\Finished;
17 | use PHPUnit\Event\TestRunner\FinishedSubscriber;
18 | use Zenstruck\Foundry\Configuration;
19 |
20 | final class DisplayFakerSeedOnTestSuiteFinished implements FinishedSubscriber
21 | {
22 | public function notify(Finished $event): void
23 | {
24 | echo "\n\nFaker seed: ".Configuration::fakerSeed(); // @phpstan-ignore ekinoBannedCode.expression
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/PHPUnit/EnableInMemoryBeforeTest.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Zenstruck\Foundry\PHPUnit;
15 |
16 | use PHPUnit\Event;
17 | use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
18 | use Zenstruck\Foundry\Configuration;
19 | use Zenstruck\Foundry\InMemory\AsInMemoryTest;
20 | use Zenstruck\Foundry\InMemory\CannotEnableInMemory;
21 |
22 | final class EnableInMemoryBeforeTest implements Event\Test\PreparedSubscriber
23 | {
24 | public function notify(Event\Test\Prepared $event): void
25 | {
26 | $test = $event->test();
27 |
28 | if (!$test instanceof Event\Code\TestMethod) {
29 | return;
30 | }
31 |
32 | $testClass = $test->className();
33 |
34 | if (!AsInMemoryTest::shouldEnableInMemory($testClass, $test->methodName())) {
35 | return;
36 | }
37 |
38 | if (!\is_subclass_of($testClass, KernelTestCase::class)) {
39 | throw CannotEnableInMemory::testIsNotAKernelTestCase("{$test->className()}::{$test->methodName()}");
40 | }
41 |
42 | Configuration::instance()->enableInMemory();
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/PHPUnit/FoundryExtension.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Zenstruck\Foundry\PHPUnit;
15 |
16 | use PHPUnit\Metadata\Version\ConstraintRequirement;
17 | use PHPUnit\Runner;
18 | use PHPUnit\TextUI;
19 | use Zenstruck\Foundry\Configuration;
20 |
21 | /**
22 | * @internal
23 | * @author Nicolas PHILIPPE
24 | */
25 | final class FoundryExtension implements Runner\Extension\Extension
26 | {
27 | public function bootstrap(
28 | TextUI\Configuration\Configuration $configuration,
29 | Runner\Extension\Facade $facade,
30 | Runner\Extension\ParameterCollection $parameters,
31 | ): void {
32 | // shutdown Foundry if for some reason it has been booted before
33 | if (Configuration::isBooted()) {
34 | Configuration::shutdown();
35 | }
36 |
37 | $subscribers = [
38 | new BuildStoryOnTestPrepared(),
39 | new EnableInMemoryBeforeTest(),
40 | new DisplayFakerSeedOnTestSuiteFinished(),
41 | ];
42 |
43 | if (ConstraintRequirement::from('>=11.4')->isSatisfiedBy(Runner\Version::id())) {
44 | // those deal with data provider events which can be useful only if PHPUnit >=11.4 is used
45 | $subscribers[] = new BootFoundryOnDataProviderMethodCalled();
46 | $subscribers[] = new ShutdownFoundryOnDataProviderMethodFinished();
47 | }
48 |
49 | $facade->registerSubscribers(...$subscribers);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/PHPUnit/ShutdownFoundryOnDataProviderMethodFinished.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Zenstruck\Foundry\PHPUnit;
15 |
16 | use PHPUnit\Event;
17 |
18 | /**
19 | * @internal
20 | * @author Nicolas PHILIPPE
21 | */
22 | final class ShutdownFoundryOnDataProviderMethodFinished implements Event\Test\DataProviderMethodFinishedSubscriber
23 | {
24 | public function notify(Event\Test\DataProviderMethodFinished $event): void
25 | {
26 | if (\method_exists($event->testMethod()->className(), '_shutdownAfterDataProvider')) {
27 | $event->testMethod()->className()::_shutdownAfterDataProvider();
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Persistence/Exception/NoPersistenceStrategy.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Zenstruck\Foundry\Persistence\Exception;
15 |
16 | final class NoPersistenceStrategy extends \LogicException
17 | {
18 | /**
19 | * @param class-string $class
20 | */
21 | public function __construct(string $class, ?\Throwable $previous = null)
22 | {
23 | parent::__construct(
24 | \sprintf('No persistence strategy found for "%s".', $class),
25 | previous: $previous
26 | );
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Persistence/Exception/NotEnoughObjects.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Foundry\Persistence\Exception;
13 |
14 | /**
15 | * @author Kevin Bond
16 | */
17 | final class NotEnoughObjects extends \RuntimeException
18 | {
19 | }
20 |
--------------------------------------------------------------------------------
/src/Persistence/Exception/RefreshObjectFailed.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Zenstruck\Foundry\Persistence\Exception;
15 |
16 | final class RefreshObjectFailed extends \RuntimeException
17 | {
18 | private function __construct(string $message, private bool $objectWasDeleted = false)
19 | {
20 | parent::__construct($message);
21 | }
22 |
23 | public static function objectNoLongExists(): static
24 | {
25 | return new self('object no longer exists...', objectWasDeleted: true);
26 | }
27 |
28 | /**
29 | * @param class-string $objectClass
30 | */
31 | public static function objectHasUnsavedChanges(string $objectClass): static
32 | {
33 | return new self(
34 | "Cannot auto refresh \"{$objectClass}\" as there are unsaved changes. Be sure to call ->_save() or disable auto refreshing (see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#auto-refresh for details)."
35 | );
36 | }
37 |
38 | public function objectWasDeleted(): bool
39 | {
40 | return $this->objectWasDeleted;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Persistence/IsProxy.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Foundry\Persistence;
13 |
14 | use Symfony\Component\VarExporter\LazyProxyTrait;
15 | use Zenstruck\Assert;
16 | use Zenstruck\Foundry\Configuration;
17 | use Zenstruck\Foundry\Exception\PersistenceNotAvailable;
18 | use Zenstruck\Foundry\Object\Hydrator;
19 | use Zenstruck\Foundry\Persistence\Exception\RefreshObjectFailed;
20 |
21 | /**
22 | * @author Kevin Bond
23 | *
24 | * @internal
25 | *
26 | * @mixin LazyProxyTrait
27 | */
28 | trait IsProxy // @phpstan-ignore trait.unused
29 | {
30 | private static array $_autoRefresh = [];
31 |
32 | public function _enableAutoRefresh(): static
33 | {
34 | $this->_setAutoRefresh(true);
35 |
36 | return $this;
37 | }
38 |
39 | public function _disableAutoRefresh(): static
40 | {
41 | $this->_setAutoRefresh(false);
42 |
43 | return $this;
44 | }
45 |
46 | public function _withoutAutoRefresh(callable $callback): static
47 | {
48 | $original = $this->_getAutoRefresh();
49 | $this->_setAutoRefresh(false);
50 |
51 | $callback($this);
52 |
53 | $this->_setAutoRefresh($original);
54 |
55 | return $this;
56 | }
57 |
58 | public function _save(): static
59 | {
60 | Configuration::instance()->persistence()->save($this->initializeLazyObject());
61 |
62 | return $this;
63 | }
64 |
65 | public function _refresh(): static
66 | {
67 | $this->initializeLazyObject();
68 | $object = $this->lazyObjectState->realInstance;
69 |
70 | Configuration::instance()->persistence()->refresh($object);
71 |
72 | $this->lazyObjectState->realInstance = $object;
73 |
74 | return $this;
75 | }
76 |
77 | public function _delete(): static
78 | {
79 | Configuration::instance()->persistence()->delete($this->initializeLazyObject());
80 |
81 | return $this;
82 | }
83 |
84 | public function _get(string $property): mixed
85 | {
86 | $this->_autoRefresh();
87 |
88 | return Hydrator::get($this->initializeLazyObject(), $property);
89 | }
90 |
91 | public function _set(string $property, mixed $value): static
92 | {
93 | $this->_autoRefresh();
94 |
95 | Hydrator::set($this->initializeLazyObject(), $property, $value);
96 |
97 | return $this;
98 | }
99 |
100 | public function _real(bool $withAutoRefresh = true): object
101 | {
102 | if ($withAutoRefresh) {
103 | try {
104 | // we don't want the auto-refresh mechanism to break "real" object retrieval
105 | $this->_autoRefresh();
106 | } catch (\Throwable) {
107 | }
108 | }
109 |
110 | return $this->initializeLazyObject();
111 | }
112 |
113 | public function _repository(): ProxyRepositoryDecorator
114 | {
115 | return new ProxyRepositoryDecorator(parent::class, Configuration::instance()->isInMemoryEnabled());
116 | }
117 |
118 | public function _assertPersisted(string $message = '{entity} is not persisted.'): static
119 | {
120 | Assert::that($this->isPersisted())->isTrue($message, ['entity' => parent::class]);
121 |
122 | return $this;
123 | }
124 |
125 | public function _assertNotPersisted(string $message = '{entity} is persisted but it should not be.'): static
126 | {
127 | Assert::that($this->isPersisted())->isFalse($message, ['entity' => parent::class]);
128 |
129 | return $this;
130 | }
131 |
132 | public function _initializeLazyObject(): void
133 | {
134 | $this->initializeLazyObject();
135 | }
136 |
137 | private function isPersisted(): bool
138 | {
139 | $this->initializeLazyObject();
140 | $object = $this->lazyObjectState->realInstance;
141 |
142 | return Configuration::instance()->persistence()->isPersisted($object);
143 | }
144 |
145 | private function _autoRefresh(): void
146 | {
147 | if (!$this->_getAutoRefresh()) {
148 | return;
149 | }
150 |
151 | try {
152 | // we don't want that "transparent" calls to _refresh() to trigger a PersistenceNotAvailable exception
153 | // or a RefreshObjectFailed exception when the object was deleted
154 | $this->_refresh();
155 | } catch (PersistenceNotAvailable|RefreshObjectFailed $e) {
156 | if ($e instanceof RefreshObjectFailed && false === $e->objectWasDeleted()) {
157 | throw $e;
158 | }
159 | }
160 | }
161 |
162 | private function _getAutoRefresh(): bool
163 | {
164 | $real = $this->initializeLazyObject();
165 |
166 | static::$_autoRefresh[\spl_object_id($real)] ??= true;
167 |
168 | return static::$_autoRefresh[\spl_object_id($real)];
169 | }
170 |
171 | private function _setAutoRefresh(bool $autoRefresh): void
172 | {
173 | $real = $this->initializeLazyObject();
174 |
175 | static::$_autoRefresh[\spl_object_id($real)] = $autoRefresh;
176 | }
177 |
178 | // used in ProxyGenerator
179 | private function unproxyArgs(array $args): array
180 | {
181 | return \array_map(unproxy(...), $args);
182 | }
183 | }
184 |
--------------------------------------------------------------------------------
/src/Persistence/PersistMode.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Zenstruck\Foundry\Persistence;
15 |
16 | /**
17 | * @internal
18 | * @author Nicolas PHILIPPE
19 | */
20 | enum PersistMode
21 | {
22 | case PERSIST;
23 | case WITHOUT_PERSISTING;
24 | case NO_PERSIST_BUT_SCHEDULE_FOR_INSERT;
25 |
26 | public function isPersisting(): bool
27 | {
28 | return self::WITHOUT_PERSISTING !== $this;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Persistence/PersistenceStrategy.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Foundry\Persistence;
13 |
14 | use Doctrine\Persistence\ManagerRegistry;
15 | use Doctrine\Persistence\Mapping\ClassMetadata;
16 | use Doctrine\Persistence\Mapping\MappingException;
17 | use Doctrine\Persistence\ObjectManager;
18 | use Zenstruck\Foundry\Persistence\Relationship\RelationshipMetadata;
19 |
20 | /**
21 | * @author Kevin Bond
22 | *
23 | * @internal
24 | */
25 | abstract class PersistenceStrategy
26 | {
27 | public function __construct(protected readonly ManagerRegistry $registry)
28 | {
29 | }
30 |
31 | /**
32 | * @param class-string $class
33 | */
34 | public function supports(string $class): bool
35 | {
36 | return (bool) $this->registry->getManagerForClass($class);
37 | }
38 |
39 | /**
40 | * @param class-string $class
41 | */
42 | public function objectManagerFor(string $class): ObjectManager
43 | {
44 | return $this->registry->getManagerForClass($class) ?? throw new \LogicException(\sprintf('No manager found for "%s".', $class));
45 | }
46 |
47 | /**
48 | * @return ObjectManager[]
49 | */
50 | public function objectManagers(): array
51 | {
52 | return $this->registry->getManagers();
53 | }
54 |
55 | /**
56 | * @param class-string $parent
57 | * @param class-string $child
58 | */
59 | public function bidirectionalRelationshipMetadata(string $parent, string $child, string $field): ?RelationshipMetadata
60 | {
61 | return null;
62 | }
63 |
64 | /**
65 | * @template T of object
66 | * @param class-string $class
67 | * @return ClassMetadata
68 | *
69 | * @throws MappingException If $class is not managed by Doctrine
70 | */
71 | public function classMetadata(string $class): ClassMetadata
72 | {
73 | return $this->objectManagerFor($class)->getClassMetadata($class);
74 | }
75 |
76 | abstract public function hasChanges(object $object): bool;
77 |
78 | abstract public function contains(object $object): bool;
79 |
80 | abstract public function truncate(string $class): void;
81 |
82 | /**
83 | * @return list
84 | */
85 | abstract public function managedNamespaces(): array;
86 |
87 | /**
88 | * @param class-string $owner
89 | *
90 | * @return array|null
91 | */
92 | abstract public function embeddablePropertiesFor(object $object, string $owner): ?array;
93 |
94 | abstract public function isEmbeddable(object $object): bool;
95 |
96 | abstract public function isScheduledForInsert(object $object): bool;
97 | }
98 |
--------------------------------------------------------------------------------
/src/Persistence/PersistentProxyObjectFactory.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Foundry\Persistence;
13 |
14 | use Doctrine\Persistence\ObjectRepository;
15 | use Zenstruck\Foundry\Configuration;
16 |
17 | /**
18 | * @author Kevin Bond
19 | *
20 | * @template T of object
21 | * @extends PersistentObjectFactory>
22 | */
23 | abstract class PersistentProxyObjectFactory extends PersistentObjectFactory
24 | {
25 | /**
26 | * @return class-string
27 | */
28 | abstract public static function class(): string;
29 |
30 | /**
31 | * @return T|Proxy
32 | * @phpstan-return T&Proxy
33 | */
34 | final public function create(callable|array $attributes = []): object
35 | {
36 | $configuration = Configuration::instance();
37 | if ($configuration->inADataProvider()) {
38 | return ProxyGenerator::wrapFactory($this, $attributes);
39 | }
40 |
41 | return proxy(parent::create($attributes)); // @phpstan-ignore function.unresolvableReturnType
42 | }
43 |
44 | /**
45 | * @return T|Proxy
46 | * @phpstan-return T&Proxy
47 | */
48 | final public static function createOne(array|callable $attributes = []): mixed
49 | {
50 | return proxy(parent::createOne($attributes)); // @phpstan-ignore function.unresolvableReturnType
51 | }
52 |
53 | /**
54 | * @return T|Proxy
55 | * @phpstan-return T&Proxy
56 | */
57 | final public static function find(mixed $criteriaOrId): object
58 | {
59 | return proxy(parent::find($criteriaOrId)); // @phpstan-ignore function.unresolvableReturnType
60 | }
61 |
62 | /**
63 | * @return T|Proxy
64 | * @phpstan-return T&Proxy
65 | */
66 | final public static function findOrCreate(array $criteria): object
67 | {
68 | return proxy(parent::findOrCreate($criteria)); // @phpstan-ignore function.unresolvableReturnType
69 | }
70 |
71 | /**
72 | * @return T|Proxy
73 | * @phpstan-return T&Proxy
74 | */
75 | final public static function randomOrCreate(array $criteria = []): object
76 | {
77 | return proxy(parent::randomOrCreate($criteria)); // @phpstan-ignore function.unresolvableReturnType
78 | }
79 |
80 | /**
81 | * @return list>
82 | */
83 | final public static function randomSet(int $count, array $criteria = []): array
84 | {
85 | return \array_map(proxy(...), parent::randomSet($count, $criteria));
86 | }
87 |
88 | /**
89 | * @return list>
90 | */
91 | final public static function randomRange(int $min, int $max, array $criteria = []): array
92 | {
93 | return \array_map(proxy(...), parent::randomRange($min, $max, $criteria));
94 | }
95 |
96 | /**
97 | * @return list>
98 | */
99 | final public static function findBy(array $criteria): array
100 | {
101 | return \array_map(proxy(...), parent::findBy($criteria));
102 | }
103 |
104 | /**
105 | * @return T|Proxy
106 | * @phpstan-return T&Proxy
107 | */
108 | final public static function random(array $criteria = []): object
109 | {
110 | return proxy(parent::random($criteria)); // @phpstan-ignore function.unresolvableReturnType
111 | }
112 |
113 | /**
114 | * @return T|Proxy
115 | * @phpstan-return T&Proxy
116 | */
117 | final public static function first(string $sortBy = 'id'): object
118 | {
119 | return proxy(parent::first($sortBy)); // @phpstan-ignore function.unresolvableReturnType
120 | }
121 |
122 | /**
123 | * @return T|Proxy
124 | * @phpstan-return T&Proxy
125 | */
126 | final public static function last(string $sortBy = 'id'): object
127 | {
128 | return proxy(parent::last($sortBy)); // @phpstan-ignore function.unresolvableReturnType
129 | }
130 |
131 | /**
132 | * @return list>
133 | */
134 | final public static function all(): array
135 | {
136 | return \array_map(proxy(...), parent::all());
137 | }
138 |
139 | /**
140 | * @return ProxyRepositoryDecorator>
141 | */
142 | final public static function repository(): ObjectRepository
143 | {
144 | Configuration::instance()->assertPersistenceEnabled();
145 |
146 | return new ProxyRepositoryDecorator(static::class(), Configuration::instance()->isInMemoryEnabled()); // @phpstan-ignore argument.type, return.type
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/src/Persistence/Proxy.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Foundry\Persistence;
13 |
14 | use Doctrine\Persistence\ObjectRepository;
15 |
16 | /**
17 | * @author Kevin Bond
18 | *
19 | * @template T of object
20 | * @mixin T
21 | */
22 | interface Proxy
23 | {
24 | /**
25 | * @psalm-return T&Proxy
26 | * @phpstan-return static
27 | */
28 | public function _enableAutoRefresh(): static;
29 |
30 | /**
31 | * @psalm-return T&Proxy
32 | * @phpstan-return static
33 | */
34 | public function _disableAutoRefresh(): static;
35 |
36 | /**
37 | * @param callable(static):void $callback
38 | * @psalm-return T&Proxy
39 | * @phpstan-return static
40 | */
41 | public function _withoutAutoRefresh(callable $callback): static;
42 |
43 | /**
44 | * @psalm-return T&Proxy
45 | * @phpstan-return static
46 | */
47 | public function _save(): static;
48 |
49 | /**
50 | * @psalm-return T&Proxy
51 | * @phpstan-return static
52 | */
53 | public function _refresh(): static;
54 |
55 | /**
56 | * @psalm-return T&Proxy
57 | * @phpstan-return static
58 | */
59 | public function _delete(): static;
60 |
61 | public function _get(string $property): mixed;
62 |
63 | /**
64 | * @psalm-return T&Proxy
65 | * @phpstan-return static
66 | */
67 | public function _set(string $property, mixed $value): static;
68 |
69 | /**
70 | * @return T
71 | */
72 | public function _real(bool $withAutoRefresh = true): object;
73 |
74 | /**
75 | * @psalm-return T&Proxy
76 | * @phpstan-return static
77 | */
78 | public function _assertPersisted(string $message = '{entity} is not persisted.'): static;
79 |
80 | /**
81 | * @psalm-return T&Proxy
82 | * @phpstan-return static
83 | */
84 | public function _assertNotPersisted(string $message = '{entity} is persisted but it should not be.'): static;
85 |
86 | /**
87 | * @return ProxyRepositoryDecorator>
88 | */
89 | public function _repository(): ProxyRepositoryDecorator;
90 |
91 | /**
92 | * @internal
93 | */
94 | public function _initializeLazyObject(): void;
95 | }
96 |
--------------------------------------------------------------------------------
/src/Persistence/ProxyRepositoryDecorator.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Foundry\Persistence;
13 |
14 | use Doctrine\Persistence\ObjectRepository;
15 |
16 | /**
17 | * @author Kevin Bond
18 | *
19 | * @template T of object
20 | * @template I of ObjectRepository
21 | * @extends RepositoryDecorator, I>
22 | */
23 | final class ProxyRepositoryDecorator extends RepositoryDecorator
24 | {
25 | /**
26 | * @return T|Proxy|null
27 | * @psalm-return (T&Proxy)|null
28 | */
29 | public function first(string $sortBy = 'id'): ?object
30 | {
31 | return $this->proxyNullableObject(parent::first($sortBy));
32 | }
33 |
34 | /**
35 | * @return T|Proxy
36 | * @psalm-return T&Proxy
37 | */
38 | public function firstOrFail(string $sortBy = 'id'): object
39 | {
40 | return proxy(parent::firstOrFail($sortBy));
41 | }
42 |
43 | /**
44 | * @return T|Proxy|null
45 | * @psalm-return (T&Proxy)|null
46 | */
47 | public function last(string $sortBy = 'id'): ?object
48 | {
49 | return $this->proxyNullableObject(parent::last($sortBy));
50 | }
51 |
52 | /**
53 | * @return T|Proxy
54 | * @psalm-return T&Proxy
55 | */
56 | public function lastOrFail(string $sortBy = 'id'): object
57 | {
58 | return proxy(parent::lastOrFail($sortBy));
59 | }
60 |
61 | /**
62 | * @return T|Proxy|null
63 | * @psalm-return (T&Proxy)|null
64 | */
65 | public function find($id): ?object
66 | {
67 | return $this->proxyNullableObject(parent::find($id));
68 | }
69 |
70 | /**
71 | * @return T|Proxy
72 | * @psalm-return T&Proxy
73 | */
74 | public function findOrFail(mixed $id): object
75 | {
76 | return proxy(parent::findOrFail($id));
77 | }
78 |
79 | /**
80 | * @phpstan-return list>
81 | * @psalm-return list>
82 | */
83 | public function findAll(): array
84 | {
85 | return $this->proxyArray(parent::findAll());
86 | }
87 |
88 | /**
89 | * @psalm-return list>
90 | */
91 | public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array
92 | {
93 | return $this->proxyArray(parent::findBy($criteria, $orderBy, $limit, $offset));
94 | }
95 |
96 | /**
97 | * @return T|Proxy|null
98 | * @psalm-return (T&Proxy)|null
99 | */
100 | public function findOneBy(array $criteria): ?object
101 | {
102 | return $this->proxyNullableObject(parent::findOneBy($criteria));
103 | }
104 |
105 | /**
106 | * @return T|Proxy|null
107 | * @psalm-return T&Proxy
108 | */
109 | public function random(array $criteria = []): object
110 | {
111 | return proxy(parent::random($criteria));
112 | }
113 |
114 | /**
115 | * @psalm-return list>
116 | */
117 | public function randomSet(int $count, array $criteria = []): array
118 | {
119 | return $this->proxyArray(
120 | parent::randomSet($count, $criteria)
121 | );
122 | }
123 |
124 | /**
125 | * @psalm-return list>
126 | */
127 | public function randomRange(int $min, int $max, array $criteria = []): array
128 | {
129 | return $this->proxyArray(
130 | parent::randomRange($min, $max, $criteria)
131 | );
132 | }
133 |
134 | public function getIterator(): \Traversable
135 | {
136 | foreach (parent::getIterator() as $item) {
137 | yield proxy($item);
138 | }
139 | }
140 |
141 | public function count(array $criteria = []): int
142 | {
143 | return parent::count($criteria);
144 | }
145 |
146 | public function getClassName(): string
147 | {
148 | return parent::getClassName();
149 | }
150 |
151 | /**
152 | * @param list $objects
153 | * @return list>
154 | */
155 | private function proxyArray(array $objects): array
156 | {
157 | return \array_map(
158 | static fn(object $object) => proxy($object),
159 | $objects
160 | );
161 | }
162 |
163 | /**
164 | * @param T|null $object
165 | * @return (T&Proxy)|null
166 | */
167 | private function proxyNullableObject(?object $object): ?object
168 | {
169 | if (null === $object) {
170 | return null;
171 | }
172 |
173 | return proxy($object);
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/src/Persistence/Relationship/ManyToOneRelationship.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Zenstruck\Foundry\Persistence\Relationship;
15 |
16 | /**
17 | * @author Nicolas PHILIPPE
18 | *
19 | * @internal
20 | */
21 | final class ManyToOneRelationship implements RelationshipMetadata
22 | {
23 | public function __construct(
24 | private readonly string $inverseField,
25 | ) {
26 | }
27 |
28 | public function inverseField(): string
29 | {
30 | return $this->inverseField;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Persistence/Relationship/OneToManyRelationship.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Zenstruck\Foundry\Persistence\Relationship;
15 |
16 | /**
17 | * @author Nicolas PHILIPPE
18 | *
19 | * @internal
20 | */
21 | final class OneToManyRelationship implements RelationshipMetadata
22 | {
23 | public function __construct(
24 | private readonly string $inverseField,
25 | public readonly ?string $collectionIndexedBy,
26 | ) {
27 | }
28 |
29 | public function inverseField(): string
30 | {
31 | return $this->inverseField;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Persistence/Relationship/OneToOneRelationship.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Zenstruck\Foundry\Persistence\Relationship;
15 |
16 | /**
17 | * @author Nicolas PHILIPPE
18 | *
19 | * @internal
20 | */
21 | final class OneToOneRelationship implements RelationshipMetadata
22 | {
23 | public function __construct(
24 | private readonly string $inverseField,
25 | public readonly bool $isOwning,
26 | ) {
27 | }
28 |
29 | public function inverseField(): string
30 | {
31 | return $this->inverseField;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Persistence/Relationship/RelationshipMetadata.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Foundry\Persistence\Relationship;
13 |
14 | /**
15 | * @author Kevin Bond
16 | *
17 | * @internal
18 | */
19 | interface RelationshipMetadata
20 | {
21 | public function inverseField(): string;
22 | }
23 |
--------------------------------------------------------------------------------
/src/Persistence/RepositoryAssertions.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Zenstruck\Foundry\Persistence;
13 |
14 | use Doctrine\Persistence\ObjectRepository;
15 | use Zenstruck\Assert;
16 | use Zenstruck\Foundry\Factory;
17 |
18 | /**
19 | * @author Kevin Bond
20 | *
21 | * @phpstan-import-type Parameters from Factory
22 | */
23 | final class RepositoryAssertions
24 | {
25 | /**
26 | * @internal
27 | *
28 | * @param RepositoryDecorator