├── travis.php.ini ├── tests ├── Trappar │ └── AliceGeneratorBundle │ │ ├── SymfonyApp │ │ ├── config │ │ │ ├── alice-generator │ │ │ │ └── Userasdf.yml │ │ │ ├── services.yml │ │ │ └── config.yml │ │ ├── TestBundle │ │ │ ├── TestBundle.php │ │ │ ├── DataFixtures │ │ │ │ └── Faker │ │ │ │ │ └── Provider │ │ │ │ │ ├── ServiceProvider.php │ │ │ │ │ ├── NonArrayReturningProvider.php │ │ │ │ │ ├── CustomProvider.php │ │ │ │ │ └── ArgTestProvider.php │ │ │ ├── ObjectHandler │ │ │ │ └── CustomHandler.php │ │ │ └── Entity │ │ │ │ ├── Post.php │ │ │ │ └── User.php │ │ └── AppKernel.php │ │ ├── TrapparAliceGeneratorBundleTest.php │ │ ├── FixtureGeneratorTest.php │ │ ├── DependencyInjection │ │ ├── Compiler │ │ │ └── CompilerPassTest.php │ │ └── TrapparAliceGeneratorExtensionTest.php │ │ └── Command │ │ └── GenerateFixturesCommandTest.php └── bootstrap.php ├── .gitignore ├── phpunit.xml.dist ├── src └── Trappar │ └── AliceGeneratorBundle │ ├── TrapparAliceGeneratorBundle.php │ ├── Persister │ └── DoctrinePersister.php │ ├── Resources │ ├── meta │ │ └── LICENSE │ ├── doc │ │ └── configuration.md │ └── config │ │ └── services.yml │ ├── DependencyInjection │ ├── Compiler │ │ └── CompilerPass.php │ ├── Configuration.php │ └── TrapparAliceGeneratorExtension.php │ ├── Metadata │ └── Resolver │ │ └── Faker │ │ └── ServiceFakerResolver.php │ └── Command │ ├── Validators.php │ ├── Questions.php │ └── GenerateFixturesCommand.php ├── CHANGELOG.md ├── .travis.yml ├── console ├── LICENSE ├── composer.json ├── UPGRADING.md └── README.md /travis.php.ini: -------------------------------------------------------------------------------- 1 | memory_limit = -1 -------------------------------------------------------------------------------- /tests/Trappar/AliceGeneratorBundle/SymfonyApp/config/alice-generator/Userasdf.yml: -------------------------------------------------------------------------------- 1 | Trappar\AliceGeneratorBundle\Tests\SymfonyApp\TestBundle\Entity\User: -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/* 2 | composer.lock 3 | tests/Trappar/AliceGeneratorBundle/SymfonyApp/cache 4 | tests/Trappar/AliceGeneratorBundle/SymfonyApp/logs 5 | generated.yml -------------------------------------------------------------------------------- /tests/Trappar/AliceGeneratorBundle/SymfonyApp/config/services.yml: -------------------------------------------------------------------------------- 1 | services: 2 | Trappar\AliceGeneratorBundle\Tests\SymfonyApp\TestBundle\ObjectHandler\CustomHandler: 3 | tags: [ { name: trappar_alice_generator.object_handler } ] -------------------------------------------------------------------------------- /tests/Trappar/AliceGeneratorBundle/SymfonyApp/TestBundle/TestBundle.php: -------------------------------------------------------------------------------- 1 | addPsr4('Trappar\AliceGeneratorBundle\Tests\\', __DIR__ . '/Trappar/AliceGeneratorBundle'); 8 | -------------------------------------------------------------------------------- /tests/Trappar/AliceGeneratorBundle/SymfonyApp/TestBundle/DataFixtures/Faker/Provider/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | '; 12 | } 13 | } -------------------------------------------------------------------------------- /tests/Trappar/AliceGeneratorBundle/SymfonyApp/TestBundle/ObjectHandler/CustomHandler.php: -------------------------------------------------------------------------------- 1 | value]; 17 | } 18 | } -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ./tests 10 | 11 | 12 | 13 | 14 | 15 | src 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Trappar/AliceGeneratorBundle/TrapparAliceGeneratorBundle.php: -------------------------------------------------------------------------------- 1 | addCompilerPass(new CompilerPass()); 19 | } 20 | } -------------------------------------------------------------------------------- /tests/Trappar/AliceGeneratorBundle/SymfonyApp/TestBundle/DataFixtures/Faker/Provider/ArgTestProvider.php: -------------------------------------------------------------------------------- 1 | Trappar\AliceGenerator\FixtureGenerator) 10 | 11 | ## Version 1.1.0 12 | * Compatibility with AliceGenerator v0.2.0 13 | * Added option to specify which Doctrine entity manager to use in Generate Fixtures Command 14 | 15 | ## Version 1.0.0 16 | * Moved code to [AliceGenerator](https://github.com/trappar/AliceGenerator) library 17 | * Added built-in command for generating Fixtures 18 | 19 | ## Version 0.1.0 20 | * Initial version 21 | * Support all baseline functionality -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.2 5 | - 7.1 6 | - nightly 7 | 8 | matrix: 9 | fast_finish: true 10 | include: 11 | - php: '7.1' 12 | env: SYMFONY_VERSION="^3.0" 13 | - php: '7.1' 14 | env: SYMFONY_VERSION="^4.0" 15 | - php: '7.2' 16 | env: SYMFONY_VERSION="^4.0" 17 | allow_failures: 18 | - php: nightly 19 | 20 | cache: 21 | directories: 22 | - $HOME/.composer/cache/files 23 | 24 | before_install: 25 | # Don't run with xdebug, and don't error when xdebug isn't installed 26 | - | 27 | phpenv config-rm xdebug.ini || true 28 | phpenv config-add travis.php.ini 29 | if [ "$SYMFONY_VERSION" != "" ]; then 30 | composer require "symfony/symfony:${SYMFONY_VERSION}" --no-update 31 | fi 32 | 33 | install: composer update --prefer-dist --no-interaction 34 | 35 | script: ./vendor/bin/phpunit --stop-on-failure --verbose -------------------------------------------------------------------------------- /console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | getParameterOption(array('--env', '-e'), getenv('SYMFONY_ENV') ?: 'dev'); 17 | $debug = getenv('SYMFONY_DEBUG') !== '0' && !$input->hasParameterOption(array('--no-debug', '')) && $env !== 'prod'; 18 | 19 | if ($debug) { 20 | Debug::enable(); 21 | } 22 | 23 | error_reporting(E_ALL & ~E_USER_DEPRECATED); 24 | 25 | $kernel = new AppKernel($env, $debug); 26 | $application = new Application($kernel); 27 | $application->run($input); 28 | -------------------------------------------------------------------------------- /src/Trappar/AliceGeneratorBundle/Persister/DoctrinePersister.php: -------------------------------------------------------------------------------- 1 | getManager()); 18 | $this->doctrine = $doctrine; 19 | } 20 | 21 | protected function getMetadata($object) 22 | { 23 | try { 24 | $class = $this->getClass($object); 25 | 26 | return $this->doctrine->getManagerForClass($class)->getClassMetadata($class); 27 | } catch (\Exception $e) { 28 | return false; 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /tests/Trappar/AliceGeneratorBundle/SymfonyApp/AppKernel.php: -------------------------------------------------------------------------------- 1 | load(__DIR__ . '/config/config.yml'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/Trappar/AliceGeneratorBundle/SymfonyApp/config/config.yml: -------------------------------------------------------------------------------- 1 | imports: 2 | - { resource: services.yml } 3 | 4 | framework: 5 | secret: test 6 | validation: 7 | enabled: true 8 | enable_annotations: true 9 | session: ~ 10 | test: ~ 11 | 12 | doctrine: 13 | orm: 14 | default_entity_manager: default 15 | entity_managers: 16 | default: 17 | connection: default 18 | mappings: 19 | TestBundle: ~ 20 | dbal: 21 | connections: 22 | default: 23 | driver: pdo_sqlite 24 | path: "%kernel.cache_dir%/db.sqlite" 25 | charset: UTF8 26 | 27 | trappar_alice_generator: 28 | metadata: 29 | auto_detection: false 30 | directories: 31 | entities: 32 | namespace_prefix: 'Trappar\AliceGeneratorBundle\Tests\SymfonyApp\TestBundle\Entity' 33 | path: '%kernel.root_dir%/config/alice-generator' 34 | -------------------------------------------------------------------------------- /tests/Trappar/AliceGeneratorBundle/TrapparAliceGeneratorBundleTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Bundle::class, $bundle); 18 | 19 | $containerBuilder = $this->getMockBuilder(ContainerBuilder::class) 20 | ->setMethods(['addCompilerPass']) 21 | ->getMock(); 22 | 23 | $containerBuilder->expects($this->once()) 24 | ->method('addCompilerPass') 25 | ->with(new CompilerPass()); 26 | 27 | $bundle->build($containerBuilder); 28 | } 29 | } -------------------------------------------------------------------------------- /tests/Trappar/AliceGeneratorBundle/SymfonyApp/TestBundle/Entity/Post.php: -------------------------------------------------------------------------------- 1 | getContainer()->get(FixtureGenerator::class); 15 | 16 | $valueVisitor = $this->readAttribute($fg, 'valueVisitor'); 17 | 18 | $metadataResolver = $this->readAttribute($valueVisitor, 'metadataResolver'); 19 | $fakerResolvers = $this->readAttribute($metadataResolver, 'fakerResolvers'); 20 | $this->assertCount(5, $fakerResolvers); 21 | 22 | $objectHandlerRegistry = $this->readAttribute($valueVisitor, 'objectHandlerRegistry'); 23 | $handlers = $this->readAttribute($objectHandlerRegistry, 'handlers'); 24 | $this->assertCount(3, $handlers); 25 | $this->assertInstanceOf(CustomHandler::class, $handlers[0]); 26 | } 27 | } -------------------------------------------------------------------------------- /src/Trappar/AliceGeneratorBundle/Resources/meta/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Trappar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "trappar/alice-generator-bundle", 3 | "description": "Symfony bundle for generating Alice fixtures from Doctrine entities", 4 | "keywords": ["fixture", "data", "test", "orm", "doctrine"], 5 | "type": "symfony-bundle", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Jeff Way", 10 | "email": "jeff.way@me.com" 11 | } 12 | ], 13 | "require": { 14 | "php": "^7.1", 15 | "trappar/alice-generator": "^0.3", 16 | "doctrine/orm": "~2.4", 17 | "doctrine/doctrine-bundle": "^1.6", 18 | "symfony/finder": "^3.0|^4.0", 19 | "symfony/validator": "^3.0|^4.0", 20 | "symfony/console": "^3.0|^4.0", 21 | "symfony/framework-bundle": "^3.0|^4.0", 22 | "symfony/yaml": "^3.0|^4.0" 23 | }, 24 | "require-dev": { 25 | "phpunit/phpunit": "^7.1", 26 | "symfony/phpunit-bridge": "^3.0|^4.0", 27 | "matthiasnoback/symfony-dependency-injection-test": "^3.0" 28 | }, 29 | "minimum-stability": "dev", 30 | "prefer-stable": true, 31 | "autoload": { 32 | "psr-4": { "Trappar\\AliceGeneratorBundle\\": "src/Trappar/AliceGeneratorBundle" } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/Trappar/AliceGeneratorBundle/SymfonyApp/TestBundle/Entity/User.php: -------------------------------------------------------------------------------- 1 | posts = new ArrayCollection(); 66 | } 67 | 68 | public function blah(ValueContext $context) 69 | { 70 | return [$context->getContextObject()->roles]; 71 | } 72 | } 73 | 74 | -------------------------------------------------------------------------------- /UPGRADING.md: -------------------------------------------------------------------------------- 1 | # Upgrade Guide 2 | 3 | ## Upgrading from 0.1 to 1.0 4 | 5 | ##### AbstractFixtureGenerationCommand is no longer provided 6 | 7 | If you wrote a command based on this you can do one of: 8 | 9 | * Reimplement your custom command 10 | * Inject the FixtureGenerator service manually into the constructor, or retrieve it from the container (this was previously handled by the abstract service) 11 | * Handle writing the yaml file yourself. This can be as simple as using `file_put_contents`. 12 | * Remove your custom command 13 | * If your command was fairly simple then it's likely that the functionality is now offered natively through the bundle's new `generate:fixtures` command. 14 | 15 | ##### Annotations have moved to the AliceGenerator library 16 | 17 | You will need to replace use statements resembling 18 | 19 | ```php 20 | hasDefinition(MetadataResolver::class)) { 20 | $taggedServices = $container->findTaggedServiceIds('trappar_alice_generator.faker_resolver'); 21 | $resolverDefinition = $container->getDefinition(MetadataResolver::class); 22 | $resolvers = []; 23 | foreach ($taggedServices as $resolverId => $tags) { 24 | $resolvers[] = new Reference($resolverId); 25 | } 26 | $resolverDefinition->addMethodCall('addFakerResolvers', [$resolvers]); 27 | } 28 | 29 | // Add Object Handlers to ObjectHandlerRegistry 30 | if ($container->hasDefinition(ObjectHandlerRegistry::class)) { 31 | $taggedServices = $container->findTaggedServiceIds('trappar_alice_generator.object_handler'); 32 | $handlerDefinition = $container->getDefinition(ObjectHandlerRegistry::class); 33 | $handlers = []; 34 | foreach ($taggedServices as $handlerId => $tags) { 35 | $handlers[] = new Reference($handlerId); 36 | } 37 | $handlerDefinition->addMethodCall('registerHandlers', [$handlers]); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Trappar/AliceGeneratorBundle/DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | getRootNode(); 19 | } else { 20 | // BC layer for symfony/config 4.1 and older 21 | $root = $tb->root('trappar_alice_generator'); 22 | } 23 | 24 | $root->children() 25 | ->arrayNode('metadata') 26 | ->addDefaultsIfNotSet() 27 | ->fixXmlConfig('directory', 'directories') 28 | ->children() 29 | ->booleanNode('auto_detection')->defaultTrue()->end() 30 | ->arrayNode('directories') 31 | ->prototype('array') 32 | ->children() 33 | ->scalarNode('path')->isRequired()->end() 34 | ->scalarNode('namespace_prefix')->defaultValue('')->end() 35 | ->end() 36 | ->end() 37 | ->end() 38 | ->end() 39 | ->end() 40 | ->arrayNode('yaml') 41 | ->addDefaultsIfNotSet() 42 | ->children() 43 | ->integerNode('inline')->defaultValue(3)->end() 44 | ->integerNode('indent')->defaultValue(4)->end() 45 | ->end() 46 | ->end() 47 | ->booleanNode('strictTypeChecking')->defaultValue(true)->end() 48 | ->end(); 49 | 50 | return $tb; 51 | } 52 | } -------------------------------------------------------------------------------- /tests/Trappar/AliceGeneratorBundle/DependencyInjection/Compiler/CompilerPassTest.php: -------------------------------------------------------------------------------- 1 | setDefinition(MetadataResolver::class, new Definition()); 18 | $this->setDefinition(ObjectHandlerRegistry::class, new Definition()); 19 | 20 | $testFakerResolver = new Definition(); 21 | $testFakerResolver->addTag('trappar_alice_generator.faker_resolver'); 22 | $this->setDefinition('custom_faker_resolver', $testFakerResolver); 23 | 24 | $testObjectHandler = new Definition(); 25 | $testObjectHandler->addTag('trappar_alice_generator.object_handler'); 26 | $this->setDefinition('custom_object_handler', $testObjectHandler); 27 | 28 | $this->compile(); 29 | 30 | $this->assertContainerBuilderHasServiceDefinitionWithMethodCall( 31 | MetadataResolver::class, 32 | 'addFakerResolvers', 33 | [[new Reference('custom_faker_resolver')]] 34 | ); 35 | 36 | $this->assertContainerBuilderHasServiceDefinitionWithMethodCall( 37 | ObjectHandlerRegistry::class, 38 | 'registerHandlers', 39 | [[new Reference('custom_object_handler')]] 40 | ); 41 | } 42 | 43 | protected function registerCompilerPass(ContainerBuilder $container) 44 | { 45 | $container->addCompilerPass(new CompilerPass()); 46 | } 47 | } -------------------------------------------------------------------------------- /src/Trappar/AliceGeneratorBundle/Metadata/Resolver/Faker/ServiceFakerResolver.php: -------------------------------------------------------------------------------- 1 | container = $container; 20 | } 21 | 22 | /** 23 | * @inheritDoc 24 | */ 25 | public function getType() 26 | { 27 | return 'service'; 28 | } 29 | 30 | public function validate(ValueContext $valueContext) 31 | { 32 | list($serviceName, $method) = $this->getTarget($valueContext); 33 | 34 | if (!$this->container->has($serviceName)) { 35 | throw new FakerResolverException($valueContext, sprintf( 36 | 'non-existent service given: "%s".', 37 | $serviceName 38 | )); 39 | } 40 | 41 | $service = $this->container->get($serviceName); 42 | if (!is_callable([$service, $method])) { 43 | throw new FakerResolverException($valueContext, sprintf( 44 | 'service "%s" does not have a callable "%s" method', 45 | $serviceName, 46 | $method 47 | )); 48 | } 49 | } 50 | 51 | /** 52 | * @inheritDoc 53 | */ 54 | public function handle(ValueContext $valueContext) 55 | { 56 | list($serviceName, $method) = $this->getTarget($valueContext); 57 | 58 | return call_user_func([$this->container->get($serviceName), $method], $valueContext); 59 | } 60 | 61 | private function getTarget(ValueContext $valueContext) 62 | { 63 | $args = $valueContext->getMetadata()->fakerResolverArgs; 64 | $service = isset($args[0]) ? $args[0] : null; 65 | $method = isset($args[1]) ? $args[1] : 'toFixture'; 66 | 67 | return [$service, $method]; 68 | } 69 | } -------------------------------------------------------------------------------- /src/Trappar/AliceGeneratorBundle/Resources/doc/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | ## Custom Object Handlers 4 | 5 | You can register any service as an object handler by adding tag `trappar_alice_generator.object_handler` 6 | 7 | ```yaml 8 | # app/config/services.yml 9 | 10 | services: 11 | object_handler.my_handler: 12 | class: AppBundle\ObjectHandler\MyHandler 13 | tags: [ { name: trappar_alice_generator.object_handler } ] 14 | ``` 15 | 16 | Any object handlers added this way will be called before the built-in object handlers. This means that, for example, if you wish to override the native behavior for handling `DateTime` objects, you can do so. 17 | 18 | For more information on Custom Object Handlers, check out the [standalone library's documentation](https://github.com/trappar/AliceGenerator/blob/master/doc/custom-object-handlers.md). 19 | 20 | ## Overriding Third-Party Metadata 21 | 22 | Sometimes you want to generate fixtures for objects which are shipped by a third-party bundle. Such a third-party bundle might not ship with metadata that suits your needs, or possibly none, at all. In such a case, you can override the default location that is searched for metadata with a path that is under your control. 23 | 24 | ```yaml 25 | trappar_alice_generator: 26 | metadata: 27 | directories: 28 | FOSUB: 29 | namespace_prefix: "FOS\\UserBundle" 30 | path: "%kernel.root_dir%/fixture/FOSUB" 31 | ``` 32 | 33 | ## Extension Reference 34 | 35 | Below you find a reference of all configuration options with their default values: 36 | 37 | ```yaml 38 | # config.yml 39 | trappar_alice_generator: 40 | metadata: 41 | # Using auto-detection, the mapping files for each bundle will be 42 | # expected in the Resources/config/fixture directory. 43 | # 44 | # Example: 45 | # class: My\FooBundle\Entity\User 46 | # expected path: @MyFooBundle/Resources/config/fixture/Entity.User.yml 47 | auto_detection: true 48 | 49 | # if you don't want to use auto-detection, you can also define the 50 | # namespace prefix and the corresponding directory explicitly 51 | directories: 52 | any-name: 53 | namespace_prefix: "My\\FooBundle" 54 | path: "@MyFooBundle/Resources/config/fixture" 55 | another-name: 56 | namespace_prefix: "My\\BarBundle" 57 | path: "@MyBarBundle/Resources/config/fixture" 58 | yaml: 59 | # These settings directly control the arguments for \Symfony\Component\Yaml\Yaml::dump(). 60 | inline: 3 61 | indent: 4 62 | # See documentation of AliceGenerator here: https://github.com/trappar/AliceGenerator/blob/master/doc/configuration.md#disabling-strict-type-checking 63 | strictTypeChecking: true 64 | ``` 65 | 66 | [Back to the README](/README.md) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | AliceGeneratorBundle [![Build Status](https://travis-ci.org/trappar/AliceGeneratorBundle.svg?branch=master)](https://travis-ci.org/trappar/AliceGeneratorBundle) 2 | ==================== 3 | 4 | This bundle integrates the [AliceGenerator](https://github.com/trappar/AliceGenerator) library into Symfony. 5 | 6 | ## Introduction 7 | 8 | TrapparAliceGeneratorBundle allows you to generate Alice Fixtures from your existing data. 9 | 10 | You can learn more in the [documentation for the standalone library](https://github.com/trappar/AliceGenerator). 11 | 12 | ## Table of Contents 13 | 14 | * [Installation](#installation) 15 | * [Configuration](#configuration) 16 | * [Full Configuration Reference](src/Trappar/AliceGeneratorBundle/Resources/doc/configuration.md) 17 | * [Usage](#usage) 18 | * [Resources](#resources) 19 | * [Credits](#credits) 20 | * [License](#license) 21 | 22 | ## Installation 23 | 24 | ```bash 25 | composer require trappar/alice-generator-bundle 26 | ``` 27 | 28 | Then, enable the bundle by updating your `app/AppKernel.php` file to enable the bundle: 29 | 30 | ```php 31 | getEnvironment(), ['dev', 'test'])) { 35 | // ... 36 | $bundles[] = new Trappar\AliceGeneratorBundle\TrapparAliceGeneratorBundle(); 37 | // ... 38 | } 39 | ``` 40 | 41 | ## Configuration 42 | 43 | TrapparAliceGeneratorBundle requires no initial configuration to get you started. 44 | 45 | For all available configuration options, please see the [configuration reference](src/Trappar/AliceGeneratorBundle/Resources/doc/configuration.md). 46 | 47 | ## Usage 48 | 49 | The main method for using this bundle is the included command line application. Use this by running: 50 | 51 | ```bash 52 | console generate:fixtures 53 | ``` 54 | 55 | And simply follow along with the prompts. 56 | 57 | You can also request the FixtureGenerator as a service from the container: 58 | 59 | ```php 60 | $fixtureGenerator = $container->get('trappar_alice_generator.fixture_generator'); 61 | $yaml = $fixtureGenerator->generateYaml($entities); 62 | ``` 63 | 64 | Learn more in the [documentation for the dedicated library](https://github.com/trappar/AliceGenerator/blob/master/doc/usage.md). 65 | 66 | ## Resources 67 | 68 | * [Changelog](CHANGELOG.md) 69 | * [Upgrade Guide](UPGRADING.md) 70 | * [AliceGenerator](https://github.com/trappar/AliceGenerator) 71 | * [Alice](https://github.com/nelmio/alice) 72 | * [AliceBundle](https://github.com/hautelook/AliceBundle) 73 | * [Faker](https://github.com/fzaninotto/Faker) 74 | 75 | ## Credits 76 | 77 | This bundle was developed by [Jeff Way](https://github.com/trappar) with quite a lot of inspiration from: 78 | 79 | * [AliceBundle](https://github.com/hautelook/AliceBundle) 80 | * [JMSSerializerBundle](https://github.com/schmittjoh/JMSSerializerBundle) 81 | * [SensioGeneratorBundle](https://github.com/sensiolabs/SensioGeneratorBundle) 82 | 83 | [Other contributors](https://github.com/trappar/AliceGeneratorBundle/graphs/contributors). 84 | 85 | ## License 86 | 87 | [![license](https://img.shields.io/badge/license-MIT-red.svg?style=flat-square)](Resources/meta/LICENSE) 88 | -------------------------------------------------------------------------------- /src/Trappar/AliceGeneratorBundle/DependencyInjection/TrapparAliceGeneratorExtension.php: -------------------------------------------------------------------------------- 1 | load('services.yml'); 26 | 27 | $bundles = $container->getParameter('kernel.bundles'); 28 | 29 | $directories = []; 30 | if ($config['metadata']['auto_detection']) { 31 | foreach ($bundles as $name => $class) { 32 | $ref = new \ReflectionClass($class); 33 | $directories[$ref->getNamespaceName()] = dirname($ref->getFileName()) . '/Resources/config/fixture'; 34 | } 35 | } 36 | 37 | foreach ($config['metadata']['directories'] as $directory) { 38 | $directory['path'] = rtrim(str_replace('\\', '/', $directory['path']), '/'); 39 | if ('@' === $directory['path'][0]) { 40 | preg_match('~^@([a-z0-9_]+)~i', $directory['path'], $match); 41 | 42 | if (isset($match[1])) { 43 | list($prefixedBundleName, $bundleName) = $match; 44 | if (!isset($bundles[$bundleName])) { 45 | throw new RuntimeException(sprintf('The bundle "%s" has not been registered with AppKernel. Available bundles: %s', $bundleName, implode(', ', array_keys($bundles)))); 46 | } 47 | $ref = new \ReflectionClass($bundles[$bundleName]); 48 | $directory['path'] = dirname($ref->getFileName()).substr($directory['path'], strlen($prefixedBundleName)); 49 | } 50 | } 51 | $directories[rtrim($directory['namespace_prefix'], '\\')] = rtrim($directory['path'], '\\/'); 52 | } 53 | 54 | $container 55 | ->getDefinition('trappar_alice_generator.metadata.file_locator') 56 | ->addArgument($directories); 57 | 58 | $container 59 | ->getDefinition(YamlWriter::class) 60 | ->addArgument($config['yaml']['inline']) 61 | ->addArgument($config['yaml']['indent']); 62 | 63 | $container 64 | ->getDefinition(ValueVisitor::class) 65 | ->addArgument($config['strictTypeChecking']); 66 | } 67 | 68 | public function getConfiguration(array $config, ContainerBuilder $container) 69 | { 70 | return new Configuration(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /tests/Trappar/AliceGeneratorBundle/DependencyInjection/TrapparAliceGeneratorExtensionTest.php: -------------------------------------------------------------------------------- 1 | setBundles(); 23 | $this->load([ 24 | 'metadata' => [ 25 | 'auto_detection' => false 26 | ] 27 | ]); 28 | 29 | $this->assertContainerBuilderHasServiceDefinitionWithArgument( 30 | 'trappar_alice_generator.metadata.file_locator', 31 | 0, 32 | [] 33 | ); 34 | } 35 | 36 | public function testWithCustomDirectory() 37 | { 38 | $this->setBundles(); 39 | $this->load([ 40 | 'metadata' => [ 41 | 'auto_detection' => false, 42 | 'directories' => [ 43 | 'myname' => [ 44 | 'namespace_prefix' => 'some_prefix', 45 | 'path' => '@TestBundle' 46 | ] 47 | ] 48 | ] 49 | ]); 50 | 51 | $this->assertContainerBuilderHasServiceDefinitionWithArgument( 52 | 'trappar_alice_generator.metadata.file_locator', 53 | 0, 54 | ['some_prefix' => realpath(__DIR__ . '/../SymfonyApp/TestBundle')] 55 | ); 56 | } 57 | 58 | public function testAutoDetectionEnabled() 59 | { 60 | $this->setBundles(); 61 | $this->load(); 62 | 63 | $this->assertContainerBuilderHasServiceDefinitionWithArgument( 64 | 'trappar_alice_generator.metadata.file_locator', 65 | 0 66 | ); 67 | } 68 | 69 | public function testWithInvalidBundle() 70 | { 71 | $this->expectException(RuntimeException::class); 72 | 73 | $this->setBundles(); 74 | $this->load([ 75 | 'metadata' => [ 76 | 'directories' => [ 77 | 'myname' => [ 78 | 'namespace_prefix' => 'some_prefix', 79 | 'path' => '@Blah' 80 | ] 81 | ] 82 | ] 83 | ]); 84 | } 85 | 86 | public function testWithCustomYamlOptions() 87 | { 88 | $this->setBundles(); 89 | $this->load([ 90 | 'yaml' => [ 91 | 'indent' => 1 92 | ] 93 | ]); 94 | 95 | $this->assertContainerBuilderHasServiceDefinitionWithArgument( 96 | YamlWriter::class, 97 | 0, 98 | 3 99 | ); 100 | $this->assertContainerBuilderHasServiceDefinitionWithArgument( 101 | YamlWriter::class, 102 | 1, 103 | 1 104 | ); 105 | } 106 | 107 | public function testWithDisabledStrictTypeChecking() 108 | { 109 | $this->setBundles(); 110 | $this->load(); 111 | $this->assertContainerBuilderHasServiceDefinitionWithArgument( 112 | ValueVisitor::class, 113 | 5, 114 | true 115 | ); 116 | 117 | $this->load(['strictTypeChecking' => false]); 118 | $this->assertContainerBuilderHasServiceDefinitionWithArgument( 119 | ValueVisitor::class, 120 | 5, 121 | false 122 | ); 123 | } 124 | 125 | private function setBundles() 126 | { 127 | $this->setParameter('kernel.bundles', [ 128 | 'TestBundle' => 'Trappar\AliceGeneratorBundle\Tests\SymfonyApp\TestBundle\TestBundle' 129 | ]); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Trappar/AliceGeneratorBundle/Command/Validators.php: -------------------------------------------------------------------------------- 1 | getManagerForClass($entityAlias)->getClassMetadata($entityAlias); 33 | } catch (\Exception $e) { 34 | throw new \InvalidArgumentException(sprintf( 35 | 'Unable to fetch entity information for "%s"', 36 | $entityAlias 37 | )); 38 | } 39 | } 40 | 41 | return [$entityAlias, $metadata]; 42 | } 43 | 44 | public static function validateSelectionType(array $selectionTypes, $value) 45 | { 46 | if (!in_array($value, $selectionTypes)) { 47 | throw new \InvalidArgumentException(sprintf('Invalid selection type "%s".', $value)); 48 | } 49 | 50 | return $value; 51 | } 52 | 53 | public static function validateInt($value) 54 | { 55 | if (false === $result = filter_var($value, FILTER_VALIDATE_INT)) { 56 | throw new \InvalidArgumentException(sprintf('Invalid non-int given: %s.', $value)); 57 | } 58 | 59 | return $result; 60 | } 61 | 62 | public static function validateID($value) 63 | { 64 | $ids = preg_split('~\s*,\s*~', $value); 65 | 66 | foreach ($ids as $key => $id) { 67 | if ($id === '') { 68 | unset($ids[$key]); 69 | continue; 70 | } 71 | 72 | $ids[$key] = self::validateInt($id); 73 | } 74 | 75 | return array_values($ids); 76 | } 77 | 78 | public static function validateWhereConditions(ClassMetadata $metadata, $value) 79 | { 80 | // Ensure surrounding brackets exist 81 | $value = preg_replace('~^\s*\{?~', '{', $value, 1); 82 | $value = preg_replace('~\}?\s*$~', '}', $value, 1); 83 | 84 | $parsed = Yaml::parse($value); 85 | 86 | if (!count($parsed)) { 87 | throw new \InvalidArgumentException('You must include at least one condition.'); 88 | } 89 | 90 | $knownFields = $metadata->getFieldNames(); 91 | 92 | foreach ($parsed as $field => $value) { 93 | if (!in_array($field, $knownFields)) { 94 | throw new \InvalidArgumentException(sprintf( 95 | 'Unknown field "%s".', 96 | $field 97 | )); 98 | } 99 | } 100 | 101 | return $parsed; 102 | } 103 | 104 | public static function validateYesNo($value) 105 | { 106 | switch ($value) { 107 | case 'y': 108 | case 'yes': 109 | return true; 110 | break; 111 | case 'n': 112 | case 'no': 113 | return false; 114 | break; 115 | default: 116 | throw new \InvalidArgumentException('Must be either "yes" or "no".'); 117 | } 118 | } 119 | 120 | public static function validateOutputPath($path) 121 | { 122 | $ext = pathinfo($path, PATHINFO_EXTENSION); 123 | 124 | if ($ext != 'yml') { 125 | throw new \InvalidArgumentException('Output file must have .yml extension.'); 126 | } 127 | 128 | return $path; 129 | } 130 | } -------------------------------------------------------------------------------- /src/Trappar/AliceGeneratorBundle/Command/Questions.php: -------------------------------------------------------------------------------- 1 | input = $input; 30 | $this->output = $output; 31 | $this->helperSet = $helperSet; 32 | } 33 | 34 | public function askForRecursionDepth() 35 | { 36 | $question = $this->getQuestion('Maximum recursion depth', 5); 37 | $question->setValidator([Validators::class, 'validateInt']); 38 | 39 | return $this->ask($question); 40 | } 41 | 42 | public function askForOutputDirectory($bundleNames, $defaultOutputPath) 43 | { 44 | $question = $this->getQuestion('Output location', $defaultOutputPath); 45 | $question->setAutocompleterValues(array_map(function ($bundleName) { 46 | return "@$bundleName/DataFixtures/ORM/generated.yml"; 47 | }, $bundleNames)); 48 | $question->setValidator([Validators::class, 'validateOutputPath']); 49 | 50 | return $this->ask($question); 51 | } 52 | 53 | public function askIfFileOverwrite() 54 | { 55 | $question = $this->getQuestion('Overwrite', 'yes'); 56 | $question->setAutocompleterValues(['yes', 'no']); 57 | $question->setValidator([Validators::class, 'validateYesNo']); 58 | 59 | return $this->ask($question); 60 | } 61 | 62 | public function askForEntity(ManagerRegistry $doctrine, $entityAutocomplete) 63 | { 64 | $question = $this->getQuestion('Entity shortcut name', false); 65 | $question->setAutocompleterValues($entityAutocomplete); 66 | $question->setValidator(Validators::createBoundValidator('validateEntity', $doctrine)); 67 | 68 | return $this->ask($question); 69 | } 70 | 71 | public function askForSelectionType($selectionTypes) 72 | { 73 | $question = $this->getQuestion('Entity selection type', $selectionTypes[0]); 74 | $question->setAutocompleterValues($selectionTypes); 75 | $question->setValidator(Validators::createBoundValidator('validateSelectionType', $selectionTypes)); 76 | 77 | return $this->ask($question); 78 | } 79 | 80 | public function askForIDs() 81 | { 82 | $question = $this->getQuestion('IDs to include', '1'); 83 | $question->setValidator([Validators::class, 'validateID']); 84 | 85 | return $this->ask($question); 86 | } 87 | 88 | public function askForWhereConditions(ClassMetadata $entityMetadata) 89 | { 90 | $question = $this->getQuestion('Where conditions', false); 91 | $question->setValidator(Validators::createBoundValidator('validateWhereConditions', $entityMetadata)); 92 | 93 | return $this->ask($question); 94 | } 95 | 96 | public function askIfAddAsEntityConstraints() 97 | { 98 | $question = $this->getQuestion('Add as object constraints', 'no'); 99 | $question->setAutocompleterValues(['yes', 'no']); 100 | $question->setValidator([Validators::class, 'validateYesNo']); 101 | 102 | return $this->ask($question); 103 | } 104 | 105 | private function getQuestion($question, $default, $sep = ':') 106 | { 107 | $questionString = $default 108 | ? sprintf('%s [%s]%s ', $question, $default, $sep) 109 | : sprintf('%s%s ', $question, $sep); 110 | 111 | return new Question($questionString, $default); 112 | } 113 | 114 | private function ask(Question $question) 115 | { 116 | return $this->helperSet->get('question')->ask($this->input, $this->output, $question); 117 | } 118 | } -------------------------------------------------------------------------------- /src/Trappar/AliceGeneratorBundle/Resources/config/services.yml: -------------------------------------------------------------------------------- 1 | services: 2 | # Metadata Drivers 3 | trappar_alice_generator.metadata.file_locator: 4 | public: false 5 | class: Metadata\Driver\FileLocator 6 | 7 | Trappar\AliceGenerator\Metadata\Driver\YamlDriver: 8 | public: false 9 | arguments: ['@trappar_alice_generator.metadata.file_locator'] 10 | 11 | Trappar\AliceGenerator\Metadata\Driver\AnnotationDriver: 12 | public: false 13 | arguments: ['@annotation_reader'] 14 | 15 | trappar_alice_generator.metadata.chain_driver: 16 | public: false 17 | class: Metadata\Driver\DriverChain 18 | arguments: 19 | - ['@Trappar\AliceGenerator\Metadata\Driver\YamlDriver', '@Trappar\AliceGenerator\Metadata\Driver\AnnotationDriver'] 20 | 21 | trappar_alice_generator.metadata.lazy_loading_driver: 22 | public: false 23 | class: Metadata\Driver\LazyLoadingDriver 24 | arguments: 25 | - '@service_container' 26 | - 'trappar_alice_generator.metadata.chain_driver' 27 | 28 | # Metadata Factory 29 | trappar_alice_generator.metadata.factory: 30 | public: false 31 | class: Metadata\MetadataFactory 32 | arguments: ['@trappar_alice_generator.metadata.lazy_loading_driver'] 33 | 34 | # Metadata Resolvers Faker 35 | Trappar\AliceGenerator\Metadata\Resolver\Faker\ArrayFakerResolver: 36 | public: false 37 | 38 | Trappar\AliceGenerator\Metadata\Resolver\Faker\CallbackFakerResolver: 39 | public: false 40 | 41 | Trappar\AliceGenerator\Metadata\Resolver\Faker\NoArgFakerResolver: 42 | public: false 43 | 44 | Trappar\AliceGenerator\Metadata\Resolver\Faker\ValueAsArgFakerResolver: 45 | public: false 46 | 47 | Trappar\AliceGeneratorBundle\Metadata\Resolver\Faker\ServiceFakerResolver: 48 | public: false 49 | arguments: ['@service_container'] 50 | 51 | # Metadata Resolvers 52 | Trappar\AliceGenerator\Metadata\Resolver\MetadataResolver: 53 | public: false 54 | arguments: 55 | - 56 | - '@Trappar\AliceGenerator\Metadata\Resolver\Faker\ArrayFakerResolver' 57 | - '@Trappar\AliceGenerator\Metadata\Resolver\Faker\CallbackFakerResolver' 58 | - '@Trappar\AliceGenerator\Metadata\Resolver\Faker\NoArgFakerResolver' 59 | - '@Trappar\AliceGenerator\Metadata\Resolver\Faker\ValueAsArgFakerResolver' 60 | - '@Trappar\AliceGeneratorBundle\Metadata\Resolver\Faker\ServiceFakerResolver' 61 | 62 | # Persisters 63 | Trappar\AliceGeneratorBundle\Persister\DoctrinePersister: 64 | public: false 65 | arguments: ['@doctrine'] 66 | 67 | # Object Handlers 68 | Trappar\AliceGenerator\ObjectHandler\CollectionHandler: 69 | public: false 70 | 71 | Trappar\AliceGenerator\ObjectHandler\DateTimeHandler: 72 | public: false 73 | 74 | # Object Handler Registries 75 | Trappar\AliceGenerator\ObjectHandlerRegistry: 76 | public: false 77 | arguments: 78 | - 79 | - '@Trappar\AliceGenerator\ObjectHandler\CollectionHandler' 80 | - '@Trappar\AliceGenerator\ObjectHandler\DateTimeHandler' 81 | 82 | # Yaml Writer 83 | Trappar\AliceGenerator\YamlWriter: 84 | public: false 85 | 86 | # Property Namer 87 | Trappar\AliceGenerator\PropertyNamer\PropertyNamer: 88 | public: false 89 | Trappar\AliceGenerator\PropertyNamer\PropertyNamerInterface: '@Trappar\AliceGenerator\PropertyNamer\PropertyNamer' 90 | 91 | # Fixture Generator 92 | Trappar\AliceGenerator\ValueVisitor: 93 | public: false 94 | arguments: 95 | - '@trappar_alice_generator.metadata.factory' 96 | - '@Trappar\AliceGeneratorBundle\Persister\DoctrinePersister' 97 | - '@Trappar\AliceGenerator\Metadata\Resolver\MetadataResolver' 98 | - '@Trappar\AliceGenerator\ObjectHandlerRegistry' 99 | - '@Trappar\AliceGenerator\PropertyNamer\PropertyNamerInterface' 100 | 101 | 102 | Trappar\AliceGenerator\FixtureGenerator: 103 | public: true 104 | arguments: 105 | - '@Trappar\AliceGenerator\ValueVisitor' 106 | - '@Trappar\AliceGenerator\YamlWriter' 107 | - 108 | 109 | # Command 110 | trappar_alice_generator.command.generate_fixtures: 111 | class: Trappar\AliceGeneratorBundle\Command\GenerateFixturesCommand 112 | tags: [ { name: console.command } ] 113 | arguments: 114 | - '@doctrine' 115 | - '@kernel' 116 | - '@Trappar\AliceGenerator\FixtureGenerator' 117 | - '@filesystem' 118 | -------------------------------------------------------------------------------- /tests/Trappar/AliceGeneratorBundle/Command/GenerateFixturesCommandTest.php: -------------------------------------------------------------------------------- 1 | application = new Application(self::$kernel); 33 | $this->application->setAutoExit(false); 34 | $this->doctrine = static::$kernel->getContainer()->get('doctrine'); 35 | } 36 | 37 | /** 38 | * @dataProvider getInteractiveCommandData 39 | * @param Options $options 40 | */ 41 | public function testCommand(Options $options) 42 | { 43 | if ($options->generator === false) { 44 | $options->generator = $this->createMockGenerator(); 45 | } 46 | if ($options->filesystem === false) { 47 | $options->filesystem = $this->createMockFilesystem(); 48 | } 49 | if ($options->entities === false) { 50 | $options->entities = $this->getTestEntities(); 51 | } 52 | if ($options->input) { 53 | for ($i=0; $i<20; $i++) { 54 | $options->input[] = ''; 55 | } 56 | } 57 | 58 | $this->prepareDatabase($options->entities); 59 | 60 | if ($options->exception) { 61 | $this->expectException($options->exception); 62 | } 63 | if ($options->exceptionRegex) { 64 | $this->expectExceptionMessageRegExp($options->exceptionRegex); 65 | } 66 | 67 | $display = $this->getOutputFromCommandForInput($options); 68 | 69 | if ($options->displayRegex) { 70 | $this->assertRegExp($options->displayRegex, $display); 71 | } 72 | if ($options->noDisplayRegex) { 73 | $this->assertNotRegExp($options->noDisplayRegex, $display); 74 | } 75 | if (!$options->anyAssertionUsed()) { 76 | $this->assertRegExp('~Written successfully with no errors\!~', $display); 77 | } 78 | } 79 | 80 | public function getInteractiveCommandData() 81 | { 82 | $data = []; 83 | 84 | /** VALIDATION SPECIFIC TESTS **/ 85 | // Invalid selection type 86 | $options = new Options(); 87 | $options->input = ['TestBundle:User', 'asdf']; 88 | $options->displayRegex = '~invalid selection type~i'; 89 | $data[] = [$options]; 90 | 91 | // Invalid ID/integer 92 | $options = new Options(); 93 | $options->input = ['TestBundle:User', 'id', 'abc']; 94 | $options->displayRegex = '~invalid non-int given~i'; 95 | $data[] = [$options]; 96 | 97 | // ID list with extra comma 98 | $options = new Options(); 99 | $options->input = ['TestBundle:User', 'id', '1,,,2']; 100 | $options->displayRegex = '~id, \[1, 2\]~i'; 101 | $data[] = [$options]; 102 | 103 | // Invalid where conditions 104 | $options = new Options(); 105 | $options->input = ['TestBundle:User', 'where', 'test', 'username: test']; 106 | $options->displayRegex = '~malformed inline yaml string~i'; 107 | $data[] = [$options]; 108 | 109 | // Where condition returned empty 110 | $options = new Options(); 111 | $options->input = ['TestBundle:User', 'where', '', 'username: test']; 112 | $options->displayRegex = '~at least one condition~i'; 113 | $data[] = [$options]; 114 | 115 | // Where condition with non-existent field 116 | $options = new Options(); 117 | $options->input = ['TestBundle:User', 'where', 'test: test', 'username: test']; 118 | $options->displayRegex = '~unknown field~i'; 119 | $data[] = [$options]; 120 | 121 | // Attempt yes/no with garbage 122 | $options = new Options(); 123 | $options->input = ['TestBundle:User', '', 'asdf']; 124 | $options->displayRegex = '~"yes" or "no"~i'; 125 | $data[] = [$options]; 126 | 127 | // Output path with wrong extension 128 | $options = new Options(); 129 | $options->input = ['TestBundle:User', '', '', '', '', 'test.exe']; 130 | $options->displayRegex = '~must have .yml extension~i'; 131 | $data[] = [$options]; 132 | 133 | 134 | /** TESTS WITH NO ERRORS **/ 135 | // Selecting two posts by IDs 136 | $fg = $this->createMockGenerator(); 137 | $fg->method('generateYaml')->with($this->logicalNot($this->arrayHasKey(2))); 138 | $options = new Options(); 139 | $options->generator = $fg; 140 | $options->input = ['TestBundle:Post', 'id', '1,2']; 141 | $data[] = [$options]; 142 | 143 | // Selecting a post by where 144 | $fg = $this->createMockGenerator(); 145 | $fg->method('generateYaml')->with($this->logicalNot($this->arrayHasKey(1))); 146 | $options = new Options(); 147 | $options->generator = $fg; 148 | $options->input = ['TestBundle:Post', 'where', 'title: My Title']; 149 | $data[] = [$options]; 150 | 151 | // Overwrite existing file 152 | $options = new Options(); 153 | $options->filesystem = $this->createMockFilesystem(true); 154 | $options->input = ['TestBundle:User', '', '', '', '', '', 'no']; 155 | $options->displayRegex = '~file already exists~i'; 156 | $data[] = [$options]; 157 | 158 | // Ensure that "text time" command displays custom output options 159 | $options = new Options(); 160 | $options->options = ['-d' => 1, '-o' => 'test.yml']; 161 | $options->input = ['TestBundle:User']; 162 | $options->displayRegex = '~-d.*-o~'; 163 | $data[] = [$options]; 164 | 165 | // Depth and output location are at default so the "next time" command should not display those options 166 | $options = new Options(); 167 | $options->input = ['TestBundle:User']; 168 | $options->noDisplayRegex = '~-d|-o~'; 169 | $data[] = [$options]; 170 | 171 | // Ensure duplicate configuration doesn't result in duplicate selection 172 | $options = new Options(); 173 | $options->input = ['TestBundle:User', '', '', 'TestBundle:User']; 174 | $options->displayRegex = '~--entities="\[\'TestBundle:User\', all\]"~'; 175 | $data[] = [$options]; 176 | 177 | // Ensure that FixtureGenerationContext is being generated correctly 178 | $entities = $this->getTestEntities(); 179 | list($post1, $post2) = $entities; 180 | $fg = $this->createMockGenerator(); 181 | $fg->method('generateYaml') 182 | ->with($this->anything(), $this->callback( 183 | function (FixtureGenerationContext $context) use ($post1, $post2) { 184 | return $context->getPersistedObjectConstraints()->checkValid($post1) === true 185 | && $context->getPersistedObjectConstraints()->checkValid($post2) === false 186 | && $context->getMaximumRecursion() === 100; 187 | } 188 | )); 189 | $options = new Options(); 190 | $options->generator = $fg; 191 | $options->input = ['TestBundle:Post', 'id', '', 'yes']; 192 | $options->entities = $entities; 193 | $options->options = ['-d' => 100]; 194 | $data[] = [$options]; 195 | 196 | 197 | /** TESTS WITH ERROR OUTPUT **/ 198 | // Attempt not to select anything 199 | $options = new Options(); 200 | $options->input = ['', 'TestBundle:User']; 201 | $options->displayRegex = '~no entities are selected for fixture generation~i'; 202 | $data[] = [$options]; 203 | 204 | // Attempt invalid where conditions 205 | $options = new Options(); 206 | $options->options = ['--entities' => "['TestBundle:User', [where, { nonexistent: test }]]"]; 207 | $options->displayRegex = '~unrecognized field: nonexistent~i'; 208 | $data[] = [$options]; 209 | 210 | // Attempt to select records by invalid ID 211 | $options = new Options(); 212 | $options->options = ['--entities' => "['TestBundle:User', [id, [null]]]"]; 213 | $options->displayRegex = '~The identifier id is missing~'; 214 | $data[] = [$options]; 215 | 216 | // Selecting a non-existent post by ID 217 | $fg = $this->createMockGenerator(); 218 | $fg->method('generateYaml')->with($this->logicalNot($this->arrayHasKey(0))); 219 | $options = new Options(); 220 | $options->generator = $fg; 221 | $options->displayRegex = '~no result returned~i'; 222 | $options->input = ['TestBundle:Post', 'id', '3']; 223 | $data[] = [$options]; 224 | 225 | // Attempt to save to an invalid location 226 | $options = new Options(); 227 | $options->exception = \RuntimeException::class; 228 | $options->exceptionRegex = '~invalid characters~'; 229 | $options->options = [ 230 | '--entities' => '"[\'TestBundle:User\', all]"', 231 | '-o' => '@../blah.yml' 232 | ]; 233 | $data[] = [$options]; 234 | 235 | // Attempt to select non-existent entity 236 | $options = new Options(); 237 | $options->displayRegex = '~Unable to locate repository~'; 238 | $options->options = ['--entities' => "['TestBundle:BadEntity', all]"]; 239 | $data[] = [$options]; 240 | 241 | // Attempt to select all records for entity with no records 242 | $options = new Options(); 243 | $options->displayRegex = '~no results returned~i'; 244 | $options->options = ['--entities' => "['TestBundle:User', all]"]; 245 | $options->entities = []; 246 | $data[] = [$options]; 247 | 248 | // Attempt to select entities with selection type is missing 249 | $options = new Options(); 250 | $options->displayRegex = '~not formatted correctly~i'; 251 | // 252 | $options->options = ['--entities' => "['TestBundle:User']"]; 253 | $data[] = [$options]; 254 | 255 | // Attempt to select entities with malformed yaml - should be an array 256 | $options = new Options(); 257 | $options->displayRegex = '~not formatted correctly~i'; 258 | $options->options = ['--entities' => "'TestBundle:User', all"]; 259 | $data[] = [$options]; 260 | 261 | // Invalid entity name 262 | $options = new Options(); 263 | $options->displayRegex = '~Unable to fetch entity information for "badEntity"~'; 264 | $options->input = ['badEntity', 'TestBundle:Post']; 265 | $data[] = [$options]; 266 | 267 | // Invalid selection type 268 | $options = new Options(); 269 | $options->displayRegex = '~unknown selection type~i'; 270 | $options->options = ['--entities' => "['TestBundle:User', asdf]"]; 271 | $data[] = [$options]; 272 | 273 | // Invalid selection type 274 | $options = new Options(); 275 | $options->displayRegex = '~unknown selection type~i'; 276 | $options->options = ['--entities' => "['TestBundle:User', [sadf]]"]; 277 | $data[] = [$options]; 278 | 279 | return $data; 280 | } 281 | 282 | private function getOutputFromCommandForInput(Options $options) //$input, FixtureGenerator $fixtureGenerator, Filesystem $filesystem, array $options = []) 283 | { 284 | $generateFixturesCommand = new GenerateFixturesCommand($this->doctrine, static::$kernel, $options->generator, $options->filesystem); 285 | 286 | $application = new \Symfony\Component\Console\Application(); 287 | $application->add($generateFixturesCommand); 288 | 289 | $command = $application->find('generate:fixtures'); 290 | $commandTester = new CommandTester($command); 291 | 292 | if (method_exists(CommandTester::class, 'setInputs')) { 293 | $commandTester->setInputs($options->input); 294 | } else { 295 | $stream = fopen('php://memory', 'r+', false); 296 | fwrite($stream, implode("\n", $options->input)); 297 | rewind($stream); 298 | 299 | $helper = $command->getHelper('question'); 300 | $helper->setInputStream($stream); 301 | } 302 | 303 | $commandTester->execute(array_merge(['command' => $command->getName()], $options->options)); 304 | 305 | return $commandTester->getDisplay(true); 306 | } 307 | 308 | private function createMockFilesystem($fileExists = false) 309 | { 310 | $filesystem = $this->createMock(Filesystem::class); 311 | $filesystem 312 | ->expects($this->any()) 313 | ->method('exists') 314 | ->willReturn($fileExists); 315 | $filesystem 316 | ->expects($this->any()) 317 | ->method('dumpFile') 318 | ->with($this->isType('string'), 'test') 319 | ->willReturn(true); 320 | 321 | return $filesystem; 322 | } 323 | 324 | private function createMockGenerator() 325 | { 326 | $fixtureGenerator = $this->createMock(FixtureGenerator::class); 327 | $fixtureGenerator 328 | ->expects($this->any()) 329 | ->method('generateYaml') 330 | ->will($this->returnValue('test')); 331 | 332 | return $fixtureGenerator; 333 | } 334 | 335 | private function getTestEntities() 336 | { 337 | $post = new Post(); 338 | $post->body = 'Test this'; 339 | $post->title = 'My Title'; 340 | 341 | $post2 = new Post(); 342 | $post2->title = 'blah'; 343 | $post2->body = 'test'; 344 | 345 | $user = new User(); 346 | $user->username = 'test'; 347 | 348 | $post->postedBy = $user; 349 | $post2->postedBy = $user; 350 | 351 | return [$post, $post2, $user]; 352 | } 353 | 354 | private function prepareDatabase($entities) 355 | { 356 | $this->runConsole('doctrine:database:drop', ['--force' => true]); 357 | $this->runConsole('doctrine:schema:create'); 358 | 359 | $em = $this->doctrine->getManager(); 360 | foreach ($entities as $entity) { 361 | $em->persist($entity); 362 | } 363 | 364 | $em->flush(); 365 | } 366 | 367 | /** 368 | * Helper to run a Symfony command. 369 | * 370 | * @param string $command 371 | * @param array $options 372 | * 373 | * @return int 374 | * 375 | * @throws \Exception 376 | */ 377 | private function runConsole($command, array $options = []) 378 | { 379 | $options['-e'] = 'test'; 380 | $options['-q'] = null; 381 | $options = array_merge($options, ['command' => $command]); 382 | 383 | return $this->application->run(new ArrayInput($options)); 384 | } 385 | 386 | } 387 | 388 | class Options 389 | { 390 | public $exception = false; 391 | public $exceptionRegex = false; 392 | public $displayRegex = false; 393 | public $noDisplayRegex = false; 394 | public $input = []; 395 | public $options = []; 396 | /** @var bool|FixtureGenerator */ 397 | public $generator = false; 398 | /** @var bool|Filesystem */ 399 | public $filesystem = false; 400 | public $entities = false; 401 | 402 | public function anyAssertionUsed() 403 | { 404 | return $this->exception || $this->exceptionRegex || $this->displayRegex || $this->noDisplayRegex; 405 | } 406 | } -------------------------------------------------------------------------------- /src/Trappar/AliceGeneratorBundle/Command/GenerateFixturesCommand.php: -------------------------------------------------------------------------------- 1 | doctrine = $managerRegistry; 66 | $this->kernel = $kernel; 67 | $this->fixtureGenerator = $fixtureGenerator; 68 | $this->filesystem = $filesystem; 69 | 70 | parent::__construct(); 71 | } 72 | 73 | public function configure() 74 | { 75 | $this 76 | ->setName('generate:fixtures') 77 | ->setAliases(['doctrine:fixtures:generate']) 78 | ->setDescription('Generates fixtures based on Doctrine entities.') 79 | ->addOption('entities', null, InputOption::VALUE_REQUIRED, 'The entities which fixtures will be generator for') 80 | ->addOption('depth', 'd', InputOption::VALUE_OPTIONAL, 'Maximum depth to traverse through entities relations', self::DEFAULT_DEPTH) 81 | ->addOption('output', 'o', InputOption::VALUE_REQUIRED, 'Where to write the generated YAML file', null); 82 | } 83 | 84 | public function initialize(InputInterface $input, OutputInterface $output) 85 | { 86 | $entities = []; 87 | $entityNamespaces = []; 88 | $bundleNamesWithEntities = []; 89 | 90 | foreach ($this->doctrine->getManagers() as $manager) { 91 | if ($manager instanceof EntityManager) { 92 | $configuration = $manager->getConfiguration(); 93 | $entities = array_merge($entities, 94 | $configuration->getMetadataDriverImpl()->getAllClassNames() 95 | ); 96 | $entityNamespaces = array_merge($entityNamespaces, 97 | $configuration->getEntityNamespaces() 98 | ); 99 | $bundleNamesWithEntities = array_merge($bundleNamesWithEntities, 100 | array_keys($configuration->getEntityNamespaces()) 101 | ); 102 | } 103 | } 104 | 105 | $this->entityAutocomplete = array_merge( 106 | array_keys($entityNamespaces), 107 | array_filter(array_map(function ($entity) use ($entityNamespaces) { 108 | foreach ($entityNamespaces as $alias => $entityNamespace) { 109 | $shortEntityName = str_replace($entityNamespace . '\\', $alias . ':', $entity); 110 | if ($shortEntityName != $entity) { 111 | return $shortEntityName; 112 | } 113 | } 114 | 115 | return false; 116 | }, $entities)) 117 | ); 118 | $this->bundleNamesWithEntities = $bundleNamesWithEntities; 119 | 120 | $this->questions = new Questions($input, $output, $this->getHelperSet()); 121 | if (!$input->getOption('output')) { 122 | $input->setOption('output', $this->getDefaultOutputPath()); 123 | } 124 | } 125 | 126 | public function interact(InputInterface $input, OutputInterface $output) 127 | { 128 | $configuredInteractively = !$input->getOption('entities'); 129 | 130 | $this->writeSection($output, 'Welcome to the Alice fixture generator'); 131 | $output->writeln('This command helps generate Alice fixtures based on existing Doctrine entities.'); 132 | 133 | if (!$input->getOption('entities')) { 134 | $entitiesString = $this->askForEntityConfig($output); 135 | $input->setOption('entities', $entitiesString); 136 | } 137 | 138 | if ($configuredInteractively && ($this->isDepthDefault($input) || $this->isOutputPathDefault($input))) { 139 | // We only need to display this if the user hasn't already specified these options directly 140 | $this->writeSection($output, 'Output options'); 141 | } 142 | 143 | if ($configuredInteractively && $this->isDepthDefault($input)) { 144 | $output->writeln(array('', 'How deep should the fixture generator recurse through entities\' relations?')); 145 | 146 | $depth = $this->questions->askForRecursionDepth(); 147 | $input->setOption('depth', $depth); 148 | } 149 | 150 | if ($configuredInteractively && $this->isOutputPathDefault($input)) { 151 | while (true) { 152 | $output->writeln(array('', 'Where should the generated YAML file be written?')); 153 | 154 | $outputPath = $this->questions->askForOutputDirectory($this->bundleNamesWithEntities, $this->getDefaultOutputPath()); 155 | 156 | if ($this->filesystem->exists($this->locate($outputPath))) { 157 | $output->writeln(array('', 'File already exists, overwrite?')); 158 | 159 | if (!$this->questions->askIfFileOverwrite()) { 160 | continue; 161 | } 162 | } 163 | 164 | $input->setOption('output', $outputPath); 165 | break; 166 | } 167 | } 168 | 169 | if ($configuredInteractively) { 170 | $this->writeSection($output, 171 | 'Next time you call this command you can use the following options to rerun with the same options as ' . 172 | 'you configured interactively' 173 | ); 174 | 175 | $includedOptions = [ 176 | '--entities="' . $input->getOption('entities') . '"' 177 | ]; 178 | if (!$this->isDepthDefault($input)) { 179 | $includedOptions[] = '-d' . $input->getOption('depth'); 180 | } 181 | if (!$this->isOutputPathDefault($input)) { 182 | $includedOptions[] = '-o"' . $input->getOption('output') . '"'; 183 | } 184 | 185 | $output->writeln(sprintf( 186 | 'console %s %s', 187 | $this->getName(), 188 | implode(' ', $includedOptions) 189 | )); 190 | } 191 | } 192 | 193 | private function askForEntityConfig(OutputInterface $output) 194 | { 195 | $entities = []; 196 | while (true) { 197 | $output->writeln(['', '', 198 | 'Enter an entity which will be used to generate fixtures or keep blank to exit entity selection.', 199 | 'You must use the shortcut notation like AcmeBlogBundle:Post.' 200 | ]); 201 | 202 | /** @var ClassMetadata $entityMetadata */ 203 | list($entityAlias, $entityMetadata) = $this->questions->askForEntity($this->doctrine, $this->entityAutocomplete); 204 | 205 | if (!$entityMetadata) { 206 | if (count($entities) == 0) { 207 | $output->writeln(['', 208 | 'Currently no entities are selected for fixture generation, you must specify at least one.', 209 | ]); 210 | continue; 211 | } else { 212 | break; 213 | } 214 | } 215 | 216 | $entityConfig = [$entityAlias]; 217 | 218 | $selectionTypesFormatted = implode(', ', array_map(function ($type) { 219 | return "$type"; 220 | }, self::$selectionTypes)); 221 | 222 | $output->writeln(['', 223 | 'Which records will be used for generating entities?', 224 | 'Available selection types: ' . $selectionTypesFormatted 225 | ]); 226 | 227 | $selectionType = $this->questions->askForSelectionType(self::$selectionTypes); 228 | 229 | $selection = ''; 230 | switch ($selectionType) { 231 | case 'all': 232 | $selection = $selectionType; 233 | break; 234 | case 'id': 235 | $output->writeln(['', 236 | 'Enter a comma separated list of all IDs to include in fixtures for ' . $entityAlias . '' 237 | ]); 238 | 239 | $ids = $this->questions->askForIDs(); 240 | $selection = [$selectionType, $ids]; 241 | break; 242 | case 'where': 243 | $output->writeln(['', 244 | 'Enter YAML dictionary of DQL where conditions used to find entities for ' . $entityAlias . '', 245 | sprintf( 246 | 'Available fields: %s', 247 | implode(', ', array_map(function ($field) { 248 | return "$field"; 249 | }, $entityMetadata->getFieldNames())) 250 | ), 251 | 'Example: username:test, email:test@email.com', 252 | ]); 253 | 254 | $where = $this->questions->askForWhereConditions($entityMetadata); 255 | $selection = [$selectionType, $where]; 256 | break; 257 | } 258 | 259 | $entityConfig[] = $selection; 260 | 261 | $output->writeln(['', 262 | 'Would you like to add entities selected in this way as object constraints? (see AliceGenerator usage documentation)' 263 | ]); 264 | 265 | if ($this->questions->askIfAddAsEntityConstraints()) { 266 | $entityConfig[] = true; 267 | } 268 | 269 | $entities[] = $entityConfig; 270 | } 271 | 272 | $entities = array_unique($entities, SORT_REGULAR); 273 | 274 | $entitiesString = Yaml::dump($entities, 0); 275 | $entitiesString = preg_replace('~^\[|\]$~', '', $entitiesString); 276 | 277 | return $entitiesString; 278 | } 279 | 280 | /** 281 | * @see Command 282 | * @param InputInterface $input 283 | * @param OutputInterface $output 284 | * @return int|null|void 285 | */ 286 | public function execute(InputInterface $input, OutputInterface $output) 287 | { 288 | $this->writeSection($output, 'Generating Fixture File'); 289 | 290 | $entitiesConfig = Yaml::parse('[' . $input->getOption('entities') . ']'); 291 | 292 | $entities = []; 293 | $errors = []; 294 | 295 | $fixtureGenerationContext = FixtureGenerationContext::create(); 296 | $fixtureGenerationContext->setMaximumRecursion($input->getOption('depth')); 297 | 298 | foreach ($entitiesConfig as $entityConfig) { 299 | if (!is_array($entityConfig) || count($entityConfig) < 2) { 300 | $errors[] = $this->getEntitySelectionError('Not formatted correctly', $entityConfig); 301 | continue; 302 | } 303 | 304 | list($entityAlias, $selectionType) = $entityConfig; 305 | $addToEntityConstraints = (isset($entityConfig[2])) ? $entityConfig[2] : false; 306 | 307 | try { 308 | $repo = $this->doctrine->getManagerForClass($entityAlias)->getRepository($entityAlias); 309 | } catch (\Exception $e) { 310 | $errors[] = $this->getEntitySelectionError('Unable to locate repository', $entityConfig); 311 | continue; 312 | } 313 | 314 | $results = []; 315 | 316 | $selectionProcessed = true; 317 | if ($selectionType === 'all') { 318 | $results = $repo->findAll(); 319 | 320 | if (!count($results)) { 321 | $errors[] = $this->getEntitySelectionError('No results returned', $entityConfig, false); 322 | } 323 | } elseif (is_array($selectionType) && count($selectionType) == 2) { 324 | if ($selectionType[0] === 'id') { 325 | foreach ($selectionType[1] as $id) { 326 | try { 327 | $result = $repo->find($id); 328 | 329 | if ($result) { 330 | $results[] = $result; 331 | } else { 332 | $errors[] = $this->getEntitySelectionError("No result returned for ID: $id", $entityConfig, false); 333 | } 334 | } catch (\Exception $e) { 335 | $errors[] = $this->getEntitySelectionError($e->getMessage(), $entityConfig); 336 | } 337 | } 338 | } elseif ($selectionType[0] === 'where') { 339 | try { 340 | $results = $repo->findBy($selectionType[1]); 341 | } catch (\Exception $e) { 342 | $errors[] = $this->getEntitySelectionError($e->getMessage(), $entityConfig); 343 | } 344 | } else { 345 | $selectionProcessed = false; 346 | } 347 | } else { 348 | $selectionProcessed = false; 349 | } 350 | 351 | if (!$selectionProcessed) { 352 | $errors[] = $this->getEntitySelectionError("Unknown selection type or format", $entityConfig); 353 | } 354 | 355 | if ($addToEntityConstraints) { 356 | foreach ($results as $entity) { 357 | $fixtureGenerationContext->addPersistedObjectConstraint($entity); 358 | } 359 | } 360 | $entities = array_merge($entities, $results); 361 | } 362 | 363 | $yaml = $this->fixtureGenerator->generateYaml($entities, $fixtureGenerationContext); 364 | 365 | $outputFile = $this->locate($input->getOption('output')); 366 | 367 | $output->writeln([ 368 | sprintf( 369 | 'Writing generated fixtures to "%s"', 370 | $outputFile 371 | ) 372 | ]); 373 | 374 | $this->writeFile($outputFile, $yaml); 375 | 376 | if (!count($errors)) { 377 | $this->writeSection($output, 'Written successfully with no errors!'); 378 | } else { 379 | $this->writeSection($output, 380 | 'There were errors during fixture generation, check the errors below and the generated fixture file for more information.', 381 | 'bg=yellow;fg=black' 382 | ); 383 | foreach ($errors as $error) { 384 | $output->writeln(sprintf('%s', $error)); 385 | } 386 | } 387 | } 388 | 389 | private function writeFile($outputPath, $contents) 390 | { 391 | $outputDirectory = pathinfo($outputPath, PATHINFO_DIRNAME); 392 | 393 | if (!$this->filesystem->exists($outputDirectory)) { 394 | $this->filesystem->mkdir($outputDirectory); 395 | } 396 | 397 | $this->filesystem->dumpFile($outputPath, $contents); 398 | } 399 | 400 | /** 401 | * The implementation of this in FileLocator doesn't allow for files which don't exist, so this is a simple 402 | * reimplementation of that 403 | * 404 | * @param $file 405 | * @return string 406 | */ 407 | private function locate($file) 408 | { 409 | if (isset($file[0]) && '@' === $file[0]) { 410 | if (false !== strpos($file, '..')) { 411 | throw new \RuntimeException(sprintf('File name "%s" contains invalid characters (..).', $file)); 412 | } 413 | 414 | $bundleName = substr($file, 1); 415 | $path = ''; 416 | if (false !== strpos($bundleName, '/')) { 417 | list($bundleName, $path) = explode('/', $bundleName, 2); 418 | } 419 | 420 | $bundle = $this->kernel->getBundle($bundleName); 421 | $file = $bundle->getPath() . '/' . $path; 422 | } 423 | 424 | return $file; 425 | } 426 | 427 | private function isDepthDefault(InputInterface $input) 428 | { 429 | return $input->getOption('depth') == self::DEFAULT_DEPTH; 430 | } 431 | 432 | private function isOutputPathDefault(InputInterface $input) 433 | { 434 | return $input->getOption('output') == $this->getDefaultOutputPath(); 435 | } 436 | 437 | private function getDefaultOutputPath() 438 | { 439 | $defaultOutputPath = false; 440 | if (count($this->bundleNamesWithEntities) > 0) { 441 | $suggestedBundle = $this->bundleNamesWithEntities[0]; 442 | $defaultOutputPath = '@' . $suggestedBundle . self::DEFAULT_OUTPUT_PATH_SUFFIX; 443 | } 444 | 445 | return $defaultOutputPath; 446 | } 447 | 448 | private function writeSection(OutputInterface $output, $text, $style = 'bg=blue;fg=white') 449 | { 450 | $output->writeln(array( 451 | '', 452 | $this->getHelper('formatter')->formatBlock($text, $style, true), 453 | '', 454 | )); 455 | } 456 | 457 | private function getEntitySelectionError($message, $entityConfig, $displaySkipped = true) 458 | { 459 | $parts = [$message]; 460 | 461 | if ($displaySkipped) { 462 | $parts[] = 'Selection skipped'; 463 | } 464 | 465 | return 466 | sprintf('Selection %s - ', Yaml::dump($entityConfig, 0)) 467 | . implode('. ', array_filter($parts)) 468 | . '.'; 469 | } 470 | } --------------------------------------------------------------------------------