├── .gitignore ├── .travis.yml ├── Annotation ├── FixtureSnapshot.php └── Property.php ├── Command ├── GenerateDoctrineCommand.php ├── GenerateDoctrineFixtureCommand.php ├── GeneratorCommand.php ├── Helper │ └── QuestionHelper.php └── Validators.php ├── DependencyInjection ├── Configuration.php └── DoctrineFixtureGeneratorExtension.php ├── DoctrineFixturesGeneratorBundle.php ├── Entity ├── Test.php └── TestRelated.php ├── Generator ├── DoctrineFixtureGenerator.php ├── Entity.php └── Generator.php ├── README.md ├── Resources ├── config │ └── services.yaml ├── doc │ └── index.md └── meta │ └── LICENSE ├── Tests ├── Command │ └── DoctrineFixturesGeneratorTest.php └── bootstrap.php ├── Tool └── FixtureGenerator.php ├── composer.json ├── phpstan.neon └── phpunit.xml.dist /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/* 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.0 5 | - 7.1 6 | - 7.2 7 | - 7.3 8 | 9 | env: 10 | - SYMFONY_VERSION=2.2.* 11 | - SYMFONY_VERSION=2.3.* 12 | - SYMFONY_VERSION=2.7.* 13 | - SYMFONY_VERSION=dev-master 14 | 15 | before_script: composer install --dev --prefer-source 16 | -------------------------------------------------------------------------------- /Annotation/FixtureSnapshot.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 Webonaute\DoctrineFixturesGeneratorBundle\Command; 13 | 14 | use Doctrine\Bundle\DoctrineBundle\Mapping\DisconnectedMetadataFactory; 15 | 16 | abstract class GenerateDoctrineCommand extends GeneratorCommand 17 | { 18 | public function isEnabled() 19 | { 20 | return class_exists('Doctrine\\Bundle\\DoctrineBundle\\DoctrineBundle'); 21 | } 22 | 23 | protected function parseShortcutNotation($shortcut) 24 | { 25 | $entity = str_replace('/', '\\', $shortcut); 26 | 27 | if (false === $pos = strpos($entity, ':')) { 28 | throw new \InvalidArgumentException(sprintf('The entity name must contain a : ("%s" given, expecting something like AcmeBlogBundle:Blog/Post)', $entity)); 29 | } 30 | 31 | return array(substr($entity, 0, $pos), substr($entity, $pos + 1)); 32 | } 33 | 34 | protected function getEntityMetadata($entity) 35 | { 36 | $factory = new DisconnectedMetadataFactory($this->getContainer()->get('doctrine')); 37 | 38 | return $factory->getClassMetadata($entity)->getMetadata(); 39 | } 40 | } -------------------------------------------------------------------------------- /Command/GenerateDoctrineFixtureCommand.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 Webonaute\DoctrineFixturesGeneratorBundle\Command; 13 | 14 | use Doctrine\Common\Annotations\AnnotationReader; 15 | use Doctrine\ORM\EntityManager; 16 | use Doctrine\ORM\Mapping\ClassMetadata; 17 | use Webonaute\DoctrineFixturesGeneratorBundle\Command\GenerateDoctrineCommand; 18 | use Webonaute\DoctrineFixturesGeneratorBundle\Command\Validators; 19 | use Symfony\Component\Console\Helper\QuestionHelper; 20 | use Symfony\Component\Console\Input\InputInterface; 21 | use Symfony\Component\Console\Input\InputOption; 22 | use Symfony\Component\Console\Output\OutputInterface; 23 | use Symfony\Component\Console\Question\ConfirmationQuestion; 24 | use Symfony\Component\Console\Question\Question; 25 | use Symfony\Component\HttpKernel\Bundle\BundleInterface; 26 | use Symfony\Component\HttpKernel\Kernel; 27 | use Webonaute\DoctrineFixturesGeneratorBundle\Annotation\FixtureSnapshot; 28 | use Webonaute\DoctrineFixturesGeneratorBundle\Annotation\Property; 29 | use Webonaute\DoctrineFixturesGeneratorBundle\Generator\DoctrineFixtureGenerator; 30 | use Webonaute\DoctrineFixturesGeneratorBundle\Generator\Entity; 31 | 32 | /** 33 | * Initializes a Doctrine entity fixture inside a bundle. 34 | * 35 | * @author Mathieu Delisle 36 | */ 37 | class GenerateDoctrineFixtureCommand extends GenerateDoctrineCommand 38 | { 39 | 40 | /** 41 | * @var bool 42 | */ 43 | protected $confirmGeneration = true; 44 | 45 | /** 46 | * @var bool 47 | */ 48 | protected $snapshot = false; 49 | 50 | /** 51 | * @var OutputInterface 52 | */ 53 | protected $output; 54 | 55 | /** 56 | * @var EntityManager 57 | */ 58 | protected $entityManager; 59 | 60 | public const SYMFONY_4_BUNDLE_ALIAS = "App"; 61 | 62 | protected function configure() 63 | { 64 | $this 65 | ->setName('doctrine:generate:fixture') 66 | ->setAliases(['generate:doctrine:fixture']) 67 | ->setDescription('Generates a new Doctrine entity fixture inside a bundle from existing data.') 68 | ->addOption( 69 | 'entity', 70 | null, 71 | InputOption::VALUE_REQUIRED, 72 | 'The entity class name to initialize (shortcut notation)' 73 | ) 74 | ->addOption('snapshot', null, InputOption::VALUE_NONE, 'Create a full snapshot of DB.') 75 | ->addOption('overwrite', null, InputOption::VALUE_NONE, 'Overwrite entity fixture file if already exist.') 76 | ->addOption('ids', null, InputOption::VALUE_OPTIONAL, 'Only create fixture for this specific ID.') 77 | ->addOption('name', null, InputOption::VALUE_OPTIONAL, 78 | 'Give a specific name to the fixture or a prefix with snapshot option.') 79 | ->addOption('order', null, InputOption::VALUE_OPTIONAL, 'Give a specific order to the fixture.') 80 | ->addOption( 81 | 'connectionName', 82 | null, 83 | InputOption::VALUE_OPTIONAL, 84 | 'Give a specific connection name if you use multiple connectors ' 85 | ) 86 | ->setHelp( 87 | <<doctrine:generate:fixture task generates a new Doctrine 89 | entity fixture inside a bundle with existing data: 90 | 91 | php app/console doctrine:generate:fixture --entity=AcmeDemoBundle:Address 92 | 93 | The above command would initialize a new entity fixture in the following entity 94 | namespace Acme\BlogBundle\DataFixtures\ORM\LoadAddress. 95 | 96 | You can also optionally specify the id you want to generate in the new 97 | entity fixture. (Helpful when you want to create a new Test case based on real data.): 98 | 99 | php app/console doctrine:generate:fixture --entity=AcmeDemoBundle:Address --ids="12" 100 | 101 | The above command would initialize a new entity fixture in the following entity 102 | namespace Acme\BlogBundle\DataFixtures\ORM\LoadAddress12. 103 | 104 | You can also optionally specify the fixture name of the new entity fixture. 105 | (You can give for example the ticket number of what the fixture is for.): 106 | 107 | php app/console doctrine:generate:fixture --entity=AcmeDemoBundle:Address --ids="12 15-21" --name="ticket2224" 108 | 109 | The above command would initialize a new entity fixture in the following entity 110 | namespace Acme\BlogBundle\DataFixture\ORM\LoadTicket2224. 111 | 112 | If fixture name exist, it will NOT overwrite it. 113 | 114 | To deactivate the interaction mode, simply use the `--no-interaction` option 115 | without forgetting to pass all needed options: 116 | 117 | php app/console doctrine:generate:fixture --entity=AcmeDemoBundle:Address --ids="12" --no-interaction 118 | EOT 119 | ); 120 | } 121 | 122 | /** 123 | * @return DoctrineFixtureGenerator 124 | */ 125 | protected function createGenerator() 126 | { 127 | return new DoctrineFixtureGenerator( 128 | $this->getContainer()->get('filesystem'), 129 | $this->getContainer()->get('doctrine') 130 | ); 131 | } 132 | 133 | /** 134 | * @throws \InvalidArgumentException When the bundle doesn't end with Bundle (Example: "Bundle/MySampleBundle") 135 | */ 136 | protected function execute(InputInterface $input, OutputInterface $output) 137 | { 138 | $this->output = $output; 139 | 140 | $this->snapshot = $input->getOption("snapshot"); 141 | 142 | if ($this->confirmGeneration === false && $this->snapshot === false) { 143 | $output->writeln('Command aborted'); 144 | 145 | return 1; 146 | } 147 | 148 | $connectionName = $input->getOption('connectionName'); 149 | /** @var EntityManager $em */ 150 | $this->entityManager = $this->getContainer()->get('doctrine')->getManager($connectionName); 151 | 152 | $name = $input->getOption('name'); 153 | $generator = $this->getGenerator(); 154 | $overwrite = $input->getOption("overwrite"); 155 | 156 | if ($this->snapshot === true) { 157 | $entitiesMetadata = $this->getEntitiesMetadata($connectionName); 158 | $entities = $this->getOrderedEntities($entitiesMetadata, $output); 159 | 160 | if (!empty($entities)) { 161 | $this->writeSection($output, 'Entities generation'); 162 | foreach ($entities as $entity) { 163 | $result = $generator->generate( 164 | $entity->bundle, 165 | $entity->name, 166 | $generator->getFixtureNameFromEntityName($entity->name, [], $name), 167 | [], //not applicable in snapshot mode. 168 | $entity->level, 169 | $connectionName, 170 | $overwrite, 171 | true, 172 | true 173 | ); 174 | if ($result){ 175 | $tag = "info"; 176 | }else{ 177 | $tag = "comment"; 178 | } 179 | $this->output->writeln("<$tag>Generated fixture (lvl {$entity->level}) for {$entity->name}"); 180 | } 181 | } 182 | } else { 183 | $entity = Validators::validateEntityName($input->getOption('entity')); 184 | list($bundle, $entity) = $this->parseShortcutNotation($entity); 185 | $name = $input->getOption('name'); 186 | $ids = $this->parseIds($input->getOption('ids')); 187 | $order = $input->getOption('order'); 188 | 189 | $this->writeSection($output, 'Entity generation'); 190 | /** @var Kernel $kernel */ 191 | $kernel = $this->getContainer()->get('kernel'); 192 | // $bundle = $kernel->getBundle($bundle); 193 | 194 | $generator->generate('src', $entity, $name, array_values($ids), $order, $connectionName, $overwrite); 195 | 196 | $output->writeln('Generating the fixture code: OK'); 197 | } 198 | 199 | $output->writeln('DONE!'); 200 | 201 | //all fine. 202 | return 0; 203 | } 204 | 205 | /** 206 | * @param BundleInterface $bundle 207 | * 208 | * @return DoctrineFixtureGenerator 209 | */ 210 | protected function getGenerator(BundleInterface $bundle = null) 211 | { 212 | return parent::getGenerator($bundle); 213 | } 214 | 215 | /** 216 | * @param string $connectionName 217 | * 218 | * @return ClassMetadata[] 219 | */ 220 | protected function getEntitiesMetadata($connectionName = "default") 221 | { 222 | $classes = []; 223 | 224 | $em = $this->getContainer()->get('doctrine')->getManager($connectionName); 225 | $mf = $em->getMetadataFactory(); 226 | 227 | $metas = $mf->getAllMetadata(); 228 | 229 | /** @var ClassMetadata $meta */ 230 | foreach ($metas as $meta) { 231 | 232 | if ($meta->isMappedSuperclass 233 | //|| ($meta->isInheritanceTypeSingleTable() && $meta->name != $meta->rootEntityName) 234 | ) { 235 | $this->output->writeln("Skip mappedSuperClass entity ".$meta->getName().""); 236 | continue; 237 | } 238 | 239 | if ($this->skipEntity($meta)){ 240 | $this->output->writeln("Skip entity ".$meta->getName().""); 241 | continue; 242 | } 243 | 244 | //ignore vendor entities. 245 | // @todo data for entities in vendor directory should be created in a specified bundle container 246 | // in src folder. Maybe add an options in the command who user can specify which bundle should store those. 247 | $class = $meta->getReflectionClass(); 248 | if (strpos($class->getFileName(), "/vendor/")) { 249 | $this->output->writeln("Skip vendor entity ".$meta->getName().""); 250 | continue; 251 | } 252 | 253 | if ($meta->getReflectionClass()->isAbstract()) { 254 | $this->output->writeln("Skip abstract entity ".$meta->getName().""); 255 | continue; 256 | } 257 | 258 | $classes[] = $meta; 259 | } 260 | 261 | return $classes; 262 | } 263 | 264 | /** 265 | * @param ClassMetadata $meta 266 | * 267 | * @return bool 268 | */ 269 | protected function skipEntity($meta) 270 | { 271 | if ($meta->isMappedSuperclass 272 | //|| ($meta->isInheritanceTypeSingleTable() && $meta->name != $meta->rootEntityName) 273 | ) { 274 | return true; 275 | } 276 | 277 | //ignore vendor entities. 278 | // @todo data for entities in vendor directory should be created in a specified bundle container 279 | // in src folder. Maybe add an options in the command who user can specify which bundle should store those. 280 | $class = $meta->getReflectionClass(); 281 | if (strpos($class->getFileName(), "/vendor/")) { 282 | return true; 283 | } 284 | 285 | return false; 286 | } 287 | 288 | /** 289 | * 290 | * @param array $metadatas 291 | * 292 | * @return Entity[] 293 | */ 294 | protected function getOrderedEntities(array $metadatas, OutputInterface $output) 295 | { 296 | $level = 1; 297 | $countMeta = count($metadatas); 298 | $entities = []; 299 | 300 | $namespaces = $this->entityManager->getConfiguration()->getEntityNamespaces(); 301 | 302 | do { 303 | //reset current level entities list 304 | $entitiesCurrentOrder = []; 305 | //for each meta, 306 | /** 307 | * @var int $mkey 308 | * @var ClassMetadata $meta 309 | */ 310 | foreach ($metadatas as $mkey => $meta) { 311 | //check against last orders entities. 312 | if ($this->isEntityLevelReached($meta, $entities)) { 313 | if ($this->isIgnoredEntity($meta) === false) { 314 | $entity = new Entity(); 315 | $entity->level = $level; 316 | $entity->name = $meta->getName(); 317 | $entity->bundle = $this->findBundleInterface($namespaces, $meta->namespace);; 318 | $entity->meta = $meta; 319 | //add to temporary group of entities. 320 | $entitiesCurrentOrder[] = $entity; 321 | } 322 | //remove from meta to process. 323 | unset($metadatas[$mkey]); 324 | } 325 | } 326 | 327 | if (!empty($entitiesCurrentOrder)) { 328 | $entities = array_merge($entities, $entitiesCurrentOrder); 329 | } 330 | 331 | //repeat until all metadata are processed. 332 | //it can't have more level than number of entities so break if $level is superior to $countMeta. 333 | $level++; 334 | } while (!empty($metadatas) && $level <= $countMeta); 335 | 336 | //show entity who could not be ordered and get ignored. 337 | if (!empty($metadatas)){ 338 | foreach ($metadatas as $meta) { 339 | $output->writeln("Could not get ordered {$meta->getName()}"); 340 | } 341 | } 342 | 343 | return $entities; 344 | } 345 | 346 | /** 347 | * @param ClassMetadata $meta 348 | * @param array $entities 349 | * 350 | * @return bool 351 | */ 352 | protected function isEntityLevelReached(ClassMetadata $meta, array $entities) 353 | { 354 | $mappings = $meta->getAssociationMappings(); 355 | $reader = new AnnotationReader(); 356 | 357 | //if there is association, check if entity is already included to satisfy the requirement. 358 | if (count($mappings) > 0) { 359 | foreach ($mappings as $mapping) { 360 | $propertyReflection = $meta->getReflectionProperty($mapping['fieldName']); 361 | /** @var Property $propertyAnnotation */ 362 | $propertyAnnotation = $reader->getPropertyAnnotation( 363 | $propertyReflection, 364 | 'Webonaute\DoctrineFixturesGeneratorBundle\Annotation\Property' 365 | ); 366 | $annotations = $reader->getPropertyAnnotations($propertyReflection); 367 | 368 | if ($propertyAnnotation !== null && $propertyAnnotation->ignoreInSnapshot === true) { 369 | //ignore this mapping. (data will not be exported for that field.) 370 | continue; 371 | } 372 | 373 | //prevent self mapping loop. 374 | if ($mapping['targetEntity'] === $mapping['sourceEntity']) { 375 | continue; 376 | } 377 | 378 | if ($mapping['isOwningSide'] === true && $this->mappingSatisfied($mapping, $entities) === false) { 379 | // if mapping is made on abstract class with discriminator. ensure those are include before. 380 | if ($this->discriminatorSatisfied($mapping['targetEntity'], $entities)){ 381 | continue; 382 | } 383 | 384 | return false; 385 | } 386 | } 387 | } 388 | 389 | return true; 390 | 391 | } 392 | 393 | protected function discriminatorSatisfied($entity, $entities){ 394 | $entityMapping = $this->entityManager->getClassMetadata($entity); 395 | $reflectionClass = $entityMapping->getReflectionClass(); 396 | 397 | if ($reflectionClass->isAbstract()){ 398 | if (!empty($entityMapping->discriminatorMap)){ 399 | foreach ($entityMapping->discriminatorMap as $discriminator){ 400 | $found = false; 401 | if (!empty($entities)){ 402 | foreach ($entities as $checkEntity) { 403 | if ($checkEntity->name === $discriminator) { 404 | $found = true; 405 | break; 406 | } 407 | } 408 | } 409 | 410 | if (!$found){ 411 | return false; 412 | } 413 | } 414 | }else{ 415 | return false; 416 | } 417 | }else{ 418 | return false; 419 | } 420 | 421 | return true; 422 | } 423 | 424 | protected function mappingSatisfied($mapping, $entities) 425 | { 426 | foreach ($entities as $entity) { 427 | if ($entity->name === $mapping['targetEntity']) { 428 | return true; 429 | } 430 | } 431 | 432 | return false; 433 | } 434 | 435 | /** 436 | * Check if the entity should generate fixtures. 437 | * 438 | * @param ClassMetadata $meta 439 | * 440 | * @return bool 441 | */ 442 | protected function isIgnoredEntity(ClassMetadata $meta) 443 | { 444 | $result = false; 445 | 446 | $reader = new AnnotationReader(); 447 | $reflectionClass = $meta->getReflectionClass(); 448 | /** @var FixtureSnapshot $fixtureSnapshotAnnotation */ 449 | $fixtureSnapshotAnnotation = $reader->getClassAnnotation( 450 | $reflectionClass, 451 | 'Webonaute\DoctrineFixturesGeneratorBundle\Annotation\FixtureSnapshot' 452 | ); 453 | 454 | if ($fixtureSnapshotAnnotation !== null) { 455 | $result = $fixtureSnapshotAnnotation->ignore; 456 | } 457 | 458 | return $result; 459 | 460 | } 461 | 462 | /** 463 | * Return bundle name of entity namespace. 464 | * 465 | * @param $namespaces 466 | * @param $metaNamespace 467 | * 468 | * @return mixed 469 | */ 470 | protected function findBundleInterface($namespaces, $metaNamespace) 471 | { 472 | $namespaceParts = explode("\\", $metaNamespace); 473 | $bundle = null; 474 | 475 | if (count($namespaceParts) > 0) { 476 | /** @var Kernel $kernel */ 477 | $kernel = $this->getContainer()->get('kernel'); 478 | 479 | do { 480 | try { 481 | $find = array_search(implode("\\", $namespaceParts), $namespaces); 482 | if ($find !== false) { 483 | $bundle = $kernel->getBundle($find); 484 | } else { 485 | array_pop($namespaceParts); 486 | } 487 | } catch (\InvalidArgumentException $e) { 488 | array_pop($namespaceParts); 489 | } 490 | } while ($bundle == null && count($namespaceParts) > 1); 491 | 492 | } 493 | 494 | if ($bundle === null) { 495 | return self::SYMFONY_4_BUNDLE_ALIAS; 496 | } 497 | 498 | return $bundle; 499 | } 500 | 501 | /** 502 | * Parse Ids string list into an array. 503 | * 504 | * @param $input 505 | * 506 | * @return array 507 | */ 508 | private function parseIds($input) 509 | { 510 | $ids = []; 511 | 512 | if (is_array($input)) { 513 | return $input; 514 | } 515 | 516 | //check if the input is not empty. 517 | if (strlen($input) > 0) { 518 | $values = explode(' ', $input); 519 | //extract ids for each value found. 520 | foreach ($values as $value) { 521 | //filter any extra space. 522 | $value = trim($value); 523 | //filter empty values. 524 | if (strlen($value) > 0) { 525 | //check if the value is a range ids. 526 | if ($this->isRangeIds($value)) { 527 | $ids = array_merge($ids, $this->extractRangeIds($value)); 528 | } else { 529 | //make sure id is an integer. 530 | $value = intval($value); 531 | //make sure id are bigger than 0. 532 | if ($value > 0) { 533 | $ids[] = $value; 534 | } 535 | } 536 | } 537 | } 538 | } 539 | 540 | //make sure ids are unique. 541 | $ids = array_unique($ids); 542 | 543 | return $ids; 544 | } 545 | 546 | /** 547 | * Check if a string contain a range ids string. 548 | * 549 | * @param $string 550 | * 551 | * @return bool 552 | */ 553 | private function isRangeIds($string) 554 | { 555 | return (false !== strpos($string, '-')); 556 | } 557 | 558 | /** 559 | * extract ids from ranges. 560 | * 561 | * @param $string 562 | * 563 | * @return array 564 | */ 565 | private function extractRangeIds($string) 566 | { 567 | $rangesIds = explode('-', $string); 568 | $result = []; 569 | //validate array should have 2 values and those 2 values are integer. 570 | if (count($rangesIds) == 2) { 571 | $begin = intval($rangesIds[0]); 572 | $end = intval($rangesIds[1]); 573 | if ($begin > 0 && $end > 0) { 574 | $result = range($begin, $end); 575 | } 576 | } 577 | 578 | return $result; 579 | } 580 | 581 | public function writeSection(OutputInterface $output, $text, $style = 'bg=blue;fg=white') 582 | { 583 | $output->writeln( 584 | [ 585 | '', 586 | $this->getHelperSet()->get('formatter')->formatBlock($text, $style, true), 587 | '', 588 | ] 589 | ); 590 | } 591 | 592 | /** 593 | * Interactive mode. 594 | * 595 | * @param InputInterface $input 596 | * @param OutputInterface $output 597 | */ 598 | protected function interact(InputInterface $input, OutputInterface $output) 599 | { 600 | /** @var QuestionHelper $helper */ 601 | $helper = $this->getHelper('question'); 602 | $this->writeSection($output, 'Welcome to the Doctrine2 fixture generator'); 603 | 604 | // namespace 605 | $output->writeln( 606 | [ 607 | '', 608 | 'This command helps you generate Doctrine2 fixture.', 609 | '', 610 | ] 611 | ); 612 | 613 | $question = new ConfirmationQuestion('Do you want to create a full snapshot ? (y/N)', false); 614 | $snapshot = $helper->ask($input, $output, $question); 615 | 616 | if ($snapshot === false) { 617 | $bundle = ''; 618 | $entity = ''; 619 | 620 | /** @var Kernel $kernel */ 621 | $kernel = $this->getContainer()->get('kernel'); 622 | $bundleNames = array_keys($kernel->getBundles()); 623 | while (true) { 624 | 625 | $output->writeln( 626 | [ 627 | 'First, you need to give the entity name you want to generate fixture from.', 628 | 'You must use the shortcut notation like AcmeBlogBundle:Post.', 629 | '', 630 | ] 631 | ); 632 | 633 | $question = new Question( 634 | 'The Entity shortcut name'.($input->getOption('entity') != "" ? 635 | " (".$input->getOption('entity').")" : "").' : ', $input->getOption('entity') 636 | ); 637 | $question->setValidator(['Sensio\Bundle\GeneratorBundle\Command\Validators', 'validateEntityName']); 638 | $question->setMaxAttempts(5); 639 | $question->setAutocompleterValues($bundleNames); 640 | $entity = $helper->ask($input, $output, $question); 641 | 642 | list($bundle, $entity) = $this->parseShortcutNotation($entity); 643 | 644 | try { 645 | /** @var Kernel $kernel */ 646 | $kernel = $this->getContainer()->get('kernel'); 647 | //check if bundle exist. 648 | $kernel->getBundle($bundle); 649 | try { 650 | $connectionName = $input->getOption('connectionName'); 651 | //check if entity exist in the selected bundle. 652 | $this->getContainer() 653 | ->get("doctrine")->getManager($connectionName) 654 | ->getRepository($bundle.":".$entity); 655 | break; 656 | } catch (\Exception $e) { 657 | print $e->getMessage()."\n\n"; 658 | $output->writeln(sprintf('Entity "%s" does not exist.', $entity)); 659 | } 660 | 661 | } catch (\Exception $e) { 662 | $output->writeln(sprintf('Bundle "%s" does not exist.', $bundle)); 663 | } 664 | } 665 | $input->setOption('entity', $bundle.':'.$entity); 666 | 667 | // ids 668 | $input->setOption('ids', $this->addIds($input, $output, $helper)); 669 | 670 | // name 671 | $input->setOption('name', $this->getFixtureName($input, $output, $helper)); 672 | 673 | // Order 674 | $input->setOption('order', $this->getFixtureOrder($input, $output, $helper)); 675 | 676 | $count = count($input->getOption('ids')); 677 | 678 | // summary 679 | $output->writeln( 680 | [ 681 | '', 682 | $this->getHelper('formatter')->formatBlock('Summary before generation', 'bg=blue;fg=white', true), 683 | '', 684 | sprintf("You are going to generate \"%s:%s\" fixtures", $bundle, $entity), 685 | sprintf("using the \"%s\" ids.", $count), 686 | '', 687 | ] 688 | ); 689 | 690 | $this->confirmGeneration = false; 691 | $question = new ConfirmationQuestion('Do you confirm generation ? (y/N)', false); 692 | $this->confirmGeneration = $helper->ask($input, $output, $question); 693 | } else { 694 | $this->snapshot = true; 695 | } 696 | 697 | } 698 | 699 | /** 700 | * Interactive mode to add IDs list. 701 | * 702 | * @param InputInterface $input 703 | * @param OutputInterface $output 704 | * @param QuestionHelper $helper 705 | * 706 | * @return array 707 | */ 708 | private function addIds(InputInterface $input, OutputInterface $output, QuestionHelper $helper) 709 | { 710 | $ids = $this->parseIds($input->getOption('ids')); 711 | 712 | while (true) { 713 | $output->writeln(''); 714 | $question = new Question( 715 | 'New ID (press to stop adding ids)'.(!empty($ids) ? " (".implode(", ", $ids).")" : "") 716 | .' : ', null 717 | ); 718 | $question->setValidator( 719 | function ($id) use ($ids) { 720 | $inputIds = $this->parseIds($id); 721 | 722 | // If given id or range of ids are already present in defined range 723 | if ($duplicateIds = array_intersect($inputIds, $ids)) { 724 | // If input for example "5-9" 725 | if ($this->isRangeIds($id)) { 726 | // whether there is only one or more duplicate numbers from given range 727 | $idsWord = count($duplicateIds) > 1 ? 'Ids' : 'Id'; 728 | $duplicateIdsString = implode(', ', $duplicateIds); 729 | $msg = sprintf($idsWord.' "%s" from given range "%s" is already defined.', 730 | $duplicateIdsString, $id); 731 | } else { 732 | $msg = sprintf('Id "%s" is already defined.', $id); 733 | } 734 | throw new \InvalidArgumentException($msg); 735 | } 736 | 737 | return $inputIds; 738 | } 739 | ); 740 | 741 | $question->setMaxAttempts(5); 742 | $inputIds = $helper->ask($input, $output, $question); 743 | 744 | $id = $helper->ask($input, $output, $question); 745 | 746 | if (!$id) { 747 | break; 748 | } 749 | 750 | $ids = array_merge($ids, $inputIds); 751 | } 752 | 753 | return $ids; 754 | } 755 | 756 | /** 757 | * Interactive mode to add IDs list. 758 | * 759 | * @param InputInterface $input 760 | * @param OutputInterface $output 761 | * @param QuestionHelper $helper 762 | * 763 | * @return array 764 | */ 765 | private function getFixtureName(InputInterface $input, OutputInterface $output, QuestionHelper $helper) 766 | { 767 | $name = $input->getOption('name'); 768 | 769 | //should ask for the name. 770 | $output->writeln(''); 771 | 772 | $question = new Question('Fixture name'.($name != "" ? " (".$name.")" : "").' : ', $name); 773 | $question->setValidator( 774 | function ($name) use ($input) { 775 | if ($name == "" && count($input->getOption('ids')) > 1) { 776 | throw new \InvalidArgumentException('Name is require when using multiple IDs.'); 777 | } 778 | 779 | return $name; 780 | } 781 | ); 782 | $question->setMaxAttempts(5); 783 | $name = $helper->ask($input, $output, $question); 784 | 785 | if ($name == "") { 786 | //use default name. 787 | $name = null; 788 | } 789 | 790 | return $name; 791 | } 792 | 793 | /** 794 | * Interactive mode to add IDs list. 795 | * 796 | * @param InputInterface $input 797 | * @param OutputInterface $output 798 | * @param QuestionHelper $helper 799 | * 800 | * @return array 801 | */ 802 | private function getFixtureOrder(InputInterface $input, OutputInterface $output, QuestionHelper $helper) 803 | { 804 | $order = $input->getOption('order'); 805 | 806 | //should ask for the name. 807 | $output->writeln(''); 808 | 809 | $question = new Question('Fixture order'.($order != "" ? " (".$order.")" : "").' : ', $order); 810 | $question->setValidator( 811 | function ($order) { 812 | //allow numeric number including 0. but not 01 for example. 813 | if (!preg_match("/^[1-9][0-9]*|0$/", $order)) { 814 | throw new \InvalidArgumentException('Order should be an integer >= 0.'); 815 | } 816 | 817 | //ensure it return number. 818 | return intval($order); 819 | } 820 | ); 821 | $question->setMaxAttempts(5); 822 | $name = $helper->ask($input, $output, $question); 823 | 824 | if ($name == "") { 825 | //use default name. 826 | $name = 1; 827 | } 828 | 829 | return $name; 830 | } 831 | } 832 | -------------------------------------------------------------------------------- /Command/GeneratorCommand.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 Webonaute\DoctrineFixturesGeneratorBundle\Command; 13 | 14 | use Symfony\Component\HttpKernel\Bundle\BundleInterface; 15 | use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; 16 | use Webonaute\DoctrineFixturesGeneratorBundle\Generator\Generator; 17 | use Webonaute\DoctrineFixturesGeneratorBundle\Command\Helper\QuestionHelper; 18 | 19 | /** 20 | * Base class for generator commands. 21 | * 22 | * @author Fabien Potencier 23 | */ 24 | abstract class GeneratorCommand extends ContainerAwareCommand 25 | { 26 | /** 27 | * @var Generator 28 | */ 29 | private $generator; 30 | 31 | // only useful for unit tests 32 | public function setGenerator(Generator $generator) 33 | { 34 | $this->generator = $generator; 35 | } 36 | 37 | abstract protected function createGenerator(); 38 | 39 | protected function getGenerator(BundleInterface $bundle = null) 40 | { 41 | if (null === $this->generator) { 42 | $this->generator = $this->createGenerator(); 43 | $this->generator->setSkeletonDirs($this->getSkeletonDirs($bundle)); 44 | } 45 | 46 | return $this->generator; 47 | } 48 | 49 | protected function getSkeletonDirs(BundleInterface $bundle = null) 50 | { 51 | $skeletonDirs = array(); 52 | 53 | if (isset($bundle) && is_dir($dir = $bundle->getPath().'/Resources/SensioGeneratorBundle/skeleton')) { 54 | $skeletonDirs[] = $dir; 55 | } 56 | 57 | if (is_dir($dir = $this->getContainer()->get('kernel')->getRootdir().'/Resources/SensioGeneratorBundle/skeleton')) { 58 | $skeletonDirs[] = $dir; 59 | } 60 | 61 | $skeletonDirs[] = __DIR__.'/../Resources/skeleton'; 62 | $skeletonDirs[] = __DIR__.'/../Resources'; 63 | 64 | return $skeletonDirs; 65 | } 66 | 67 | protected function getQuestionHelper() 68 | { 69 | $question = $this->getHelperSet()->get('question'); 70 | if (!$question || get_class($question) !== 'Sensio\Bundle\GeneratorBundle\Command\Helper\QuestionHelper') { 71 | $this->getHelperSet()->set($question = new QuestionHelper()); 72 | } 73 | 74 | return $question; 75 | } 76 | 77 | /** 78 | * Tries to make a path relative to the project, which prints nicer. 79 | * 80 | * @param string $absolutePath 81 | * 82 | * @return string 83 | */ 84 | protected function makePathRelative($absolutePath) 85 | { 86 | $projectRootDir = dirname($this->getContainer()->getParameter('kernel.root_dir')); 87 | 88 | return str_replace($projectRootDir.'/', '', realpath($absolutePath) ?: $absolutePath); 89 | } 90 | } -------------------------------------------------------------------------------- /Command/Helper/QuestionHelper.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 Webonaute\DoctrineFixturesGeneratorBundle\Command\Helper; 13 | 14 | use Symfony\Component\Console\Helper\QuestionHelper as BaseQuestionHelper; 15 | use Symfony\Component\Console\Output\OutputInterface; 16 | 17 | /** 18 | * Generates bundles. 19 | * 20 | * @author Fabien Potencier 21 | */ 22 | class QuestionHelper extends BaseQuestionHelper 23 | { 24 | public function writeGeneratorSummary(OutputInterface $output, $errors) 25 | { 26 | if (!$errors) { 27 | $this->writeSection($output, 'Everything is OK! Now get to work :).'); 28 | } else { 29 | $this->writeSection($output, array( 30 | 'The command was not able to configure everything automatically.', 31 | 'You\'ll need to make the following changes manually.', 32 | ), 'error'); 33 | 34 | $output->writeln($errors); 35 | } 36 | } 37 | 38 | public function getRunner(OutputInterface $output, &$errors) 39 | { 40 | $runner = function ($err) use ($output, &$errors) { 41 | if ($err) { 42 | $output->writeln('FAILED'); 43 | $errors = array_merge($errors, $err); 44 | } else { 45 | $output->writeln('OK'); 46 | } 47 | }; 48 | 49 | return $runner; 50 | } 51 | 52 | public function writeSection(OutputInterface $output, $text, $style = 'bg=blue;fg=white') 53 | { 54 | $output->writeln(array( 55 | '', 56 | $this->getHelperSet()->get('formatter')->formatBlock($text, $style, true), 57 | '', 58 | )); 59 | } 60 | 61 | public function getQuestion($question, $default, $sep = ':') 62 | { 63 | return $default ? sprintf('%s [%s]%s ', $question, $default, $sep) : sprintf('%s%s ', $question, $sep); 64 | } 65 | } -------------------------------------------------------------------------------- /Command/Validators.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 Webonaute\DoctrineFixturesGeneratorBundle\Command; 13 | 14 | /** 15 | * Validator functions. 16 | * 17 | * @author Fabien Potencier 18 | */ 19 | class Validators 20 | { 21 | /** 22 | * Validates that the given namespace (e.g. Acme\FooBundle) is a valid format. 23 | * 24 | * If $requireVendorNamespace is true, then we require you to have a vendor 25 | * namespace (e.g. Acme). 26 | * 27 | * @param $namespace 28 | * @param bool $requireVendorNamespace 29 | * 30 | * @return string 31 | */ 32 | public static function validateBundleNamespace($namespace, $requireVendorNamespace = true) 33 | { 34 | if (!preg_match('/Bundle$/', $namespace)) { 35 | throw new \InvalidArgumentException('The namespace must end with Bundle.'); 36 | } 37 | 38 | $namespace = strtr($namespace, '/', '\\'); 39 | if (!preg_match('/^(?:[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*\\\?)+$/', $namespace)) { 40 | throw new \InvalidArgumentException('The namespace contains invalid characters.'); 41 | } 42 | 43 | // validate reserved keywords 44 | $reserved = self::getReservedWords(); 45 | foreach (explode('\\', $namespace) as $word) { 46 | if (in_array(strtolower($word), $reserved)) { 47 | throw new \InvalidArgumentException(sprintf('The namespace cannot contain PHP reserved words ("%s").', $word)); 48 | } 49 | } 50 | 51 | // validate that the namespace is at least one level deep 52 | if ($requireVendorNamespace && false === strpos($namespace, '\\')) { 53 | $msg = array(); 54 | $msg[] = sprintf('The namespace must contain a vendor namespace (e.g. "VendorName\%s" instead of simply "%s").', $namespace, $namespace); 55 | $msg[] = 'If you\'ve specified a vendor namespace, did you forget to surround it with quotes (init:bundle "Acme\BlogBundle")?'; 56 | 57 | throw new \InvalidArgumentException(implode("\n\n", $msg)); 58 | } 59 | 60 | return $namespace; 61 | } 62 | 63 | public static function validateBundleName($bundle) 64 | { 65 | if (!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $bundle)) { 66 | throw new \InvalidArgumentException(sprintf('The bundle name %s contains invalid characters.', $bundle)); 67 | } 68 | 69 | if (!preg_match('/Bundle$/', $bundle)) { 70 | throw new \InvalidArgumentException('The bundle name must end with Bundle.'); 71 | } 72 | 73 | return $bundle; 74 | } 75 | 76 | public static function validateControllerName($controller) 77 | { 78 | try { 79 | self::validateEntityName($controller); 80 | } catch (\InvalidArgumentException $e) { 81 | throw new \InvalidArgumentException( 82 | sprintf( 83 | 'The controller name must contain a : ("%s" given, expecting something like AcmeBlogBundle:Post)', 84 | $controller 85 | ) 86 | ); 87 | } 88 | 89 | return $controller; 90 | } 91 | 92 | public static function validateFormat($format) 93 | { 94 | if (!$format) { 95 | throw new \RuntimeException('Please enter a configuration format.'); 96 | } 97 | 98 | $format = strtolower($format); 99 | 100 | // in case they typed "yaml", but ok with that 101 | if ($format == 'yaml') { 102 | $format = 'yml'; 103 | } 104 | 105 | if (!in_array($format, array('php', 'xml', 'yml', 'annotation'))) { 106 | throw new \RuntimeException(sprintf('Format "%s" is not supported.', $format)); 107 | } 108 | 109 | return $format; 110 | } 111 | 112 | /** 113 | * Performs basic checks in entity name. 114 | * 115 | * @param string $entity 116 | * 117 | * @return string 118 | * 119 | * @throws \InvalidArgumentException 120 | */ 121 | public static function validateEntityName($entity) 122 | { 123 | if (!preg_match('{^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*:[a-zA-Z0-9_\x7f-\xff\\\/]+$}', $entity)) { 124 | throw new \InvalidArgumentException(sprintf('The entity name isn\'t valid ("%s" given, expecting something like AcmeBlogBundle:Blog/Post)', $entity)); 125 | } 126 | 127 | return $entity; 128 | } 129 | 130 | public static function getReservedWords() 131 | { 132 | return array( 133 | 'abstract', 134 | 'and', 135 | 'array', 136 | 'as', 137 | 'break', 138 | 'callable', 139 | 'case', 140 | 'catch', 141 | 'class', 142 | 'clone', 143 | 'const', 144 | 'continue', 145 | 'declare', 146 | 'default', 147 | 'do', 148 | 'else', 149 | 'elseif', 150 | 'enddeclare', 151 | 'endfor', 152 | 'endforeach', 153 | 'endif', 154 | 'endswitch', 155 | 'endwhile', 156 | 'extends', 157 | 'final', 158 | 'finally', 159 | 'for', 160 | 'foreach', 161 | 'function', 162 | 'global', 163 | 'goto', 164 | 'if', 165 | 'implements', 166 | 'interface', 167 | 'instanceof', 168 | 'insteadof', 169 | 'namespace', 170 | 'new', 171 | 'or', 172 | 'private', 173 | 'protected', 174 | 'public', 175 | 'static', 176 | 'switch', 177 | 'throw', 178 | 'trait', 179 | 'try', 180 | 'use', 181 | 'var', 182 | 'while', 183 | 'xor', 184 | 'yield', 185 | '__CLASS__', 186 | '__DIR__', 187 | '__FILE__', 188 | '__LINE__', 189 | '__FUNCTION__', 190 | '__METHOD__', 191 | '__NAMESPACE__', 192 | '__TRAIT__', 193 | '__halt_compiler', 194 | 'die', 195 | 'echo', 196 | 'empty', 197 | 'exit', 198 | 'eval', 199 | 'include', 200 | 'include_once', 201 | 'isset', 202 | 'list', 203 | 'require', 204 | 'require_once', 205 | 'return', 206 | 'print', 207 | 'unset', 208 | ); 209 | } 210 | } -------------------------------------------------------------------------------- /DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | load('services.yaml'); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Entity/Test.php: -------------------------------------------------------------------------------- 1 | id; 43 | } 44 | 45 | /** 46 | * @param int $id 47 | * 48 | * @return Test 49 | */ 50 | public function setId($id) 51 | { 52 | $this->id = $id; 53 | 54 | return $this; 55 | } 56 | 57 | /** 58 | * @return string 59 | */ 60 | public function getName() 61 | { 62 | return $this->name; 63 | } 64 | 65 | /** 66 | * @param string $name 67 | * 68 | * @return Test 69 | */ 70 | public function setName($name) 71 | { 72 | $this->name = $name; 73 | 74 | return $this; 75 | } 76 | 77 | /** 78 | * @return TestRelated 79 | */ 80 | public function getTestRelated() 81 | { 82 | return $this->testRelated; 83 | } 84 | 85 | /** 86 | * @param TestRelated $testRelated 87 | * 88 | * @return Test 89 | */ 90 | public function setTestRelated($testRelated) 91 | { 92 | $this->testRelated = $testRelated; 93 | 94 | return $this; 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /Entity/TestRelated.php: -------------------------------------------------------------------------------- 1 | id; 36 | } 37 | 38 | /** 39 | * @param int $id 40 | * 41 | * @return TestRelated 42 | */ 43 | public function setId($id) 44 | { 45 | $this->id = $id; 46 | 47 | return $this; 48 | } 49 | 50 | /** 51 | * @return string 52 | */ 53 | public function getName() 54 | { 55 | return $this->name; 56 | } 57 | 58 | /** 59 | * @param string $name 60 | * 61 | * @return TestRelated 62 | */ 63 | public function setName($name) 64 | { 65 | $this->name = $name; 66 | 67 | return $this; 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /Generator/DoctrineFixtureGenerator.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 Webonaute\DoctrineFixturesGeneratorBundle\Generator; 13 | 14 | use Doctrine\ORM\EntityManager; 15 | use Doctrine\ORM\EntityRepository; 16 | use Doctrine\ORM\Mapping\ClassMetadata; 17 | use Webonaute\DoctrineFixturesGeneratorBundle\Command\GenerateDoctrineFixtureCommand; 18 | use Webonaute\DoctrineFixturesGeneratorBundle\Generator\Generator; 19 | use Symfony\Bridge\Doctrine\RegistryInterface; 20 | use Symfony\Component\Filesystem\Filesystem; 21 | use Symfony\Component\HttpKernel\Bundle\BundleInterface; 22 | use Webonaute\DoctrineFixturesGeneratorBundle\Tool\FixtureGenerator; 23 | 24 | /** 25 | * Generates a Doctrine Fixture class based on entity name, ids and custom name. 26 | * 27 | * @author Mathieu Delisle 28 | */ 29 | class DoctrineFixtureGenerator extends Generator 30 | { 31 | 32 | /** 33 | * @var Filesystem 34 | */ 35 | private $filesystem; 36 | 37 | /** 38 | * @var RegistryInterface 39 | */ 40 | private $registry; 41 | 42 | /** 43 | * Constructor 44 | * 45 | * @param Filesystem $filesystem 46 | * @param RegistryInterface $registry 47 | */ 48 | public function __construct(Filesystem $filesystem, RegistryInterface $registry) 49 | { 50 | $this->filesystem = $filesystem; 51 | $this->registry = $registry; 52 | } 53 | 54 | /** 55 | * Generate Fixture from bundle name, entity name, fixture name and ids 56 | * 57 | * @param BundleInterface $bundle 58 | * @param string $entity 59 | * @param string $name 60 | * @param array $ids 61 | * @param string|null $connectionName 62 | * @return bool 63 | */ 64 | public function generate($bundle, $entity, $name, array $ids, $order, $connectionName = null, $overwrite = false, $isFqcnEntity = false, bool $skipEmptyFixture = false) 65 | { 66 | // configure the bundle (needed if the bundle does not contain any Entities yet) 67 | $config = $this->registry->getManager($connectionName)->getConfiguration(); 68 | $config->setEntityNamespaces( 69 | array_merge( 70 | array('App' . '\\Entity'), 71 | $config->getEntityNamespaces() 72 | ) 73 | ); 74 | 75 | $fixtureFileName = $this->getFixtureFileName($entity, $name, $ids); 76 | $entityClass = $this->getFqcnEntityClass($entity, 'App', $isFqcnEntity); 77 | 78 | $fixturePath = ($bundle === GenerateDoctrineFixtureCommand::SYMFONY_4_BUNDLE_ALIAS ? 'src' : $bundle) . '/DataFixtures/ORM/' . $fixtureFileName . '.php'; 79 | $bundleNameSpace = $bundle; 80 | if ($overwrite === false && file_exists($fixturePath)) { 81 | throw new \RuntimeException(sprintf('Fixture "%s" already exists.', $fixtureFileName)); 82 | } 83 | 84 | /** @var EntityManager $entityManager */ 85 | $entityManager = $this->registry->getManager($connectionName); 86 | $class = $entityManager->getClassMetadata($entityClass); 87 | 88 | $fixtureGenerator = $this->getFixtureGenerator(); 89 | $fixtureGenerator->setFixtureName($fixtureFileName); 90 | $fixtureGenerator->setBundleNameSpace($bundleNameSpace); 91 | $fixtureGenerator->setMetadata($class); 92 | $fixtureGenerator->setFixtureOrder($order); 93 | $fixtureGenerator->setEntityManager($entityManager); 94 | 95 | /** @var EntityManager $em */ 96 | $em = $this->registry->getManager($connectionName); 97 | 98 | /** @var EntityRepository $repo */ 99 | $repo = $em->getRepository($class->name); 100 | if (empty($ids)) { 101 | $items = $repo->findAll(); 102 | $items = array_filter($items, function($item) use ($entityClass){ 103 | if (get_class($item) === $entityClass){ 104 | return true; 105 | } else{ 106 | return false; 107 | } 108 | }); 109 | } else { 110 | $items = $repo->{$this->getFindByIdentifier($class)}($ids); 111 | } 112 | 113 | $fixtureGenerator->setItems($items); 114 | 115 | //skip fixture who dont have data to import. 116 | if ($skipEmptyFixture === true && count($items) === 0){ 117 | return false; 118 | } 119 | 120 | $fixtureCode = $fixtureGenerator->generateFixtureClass(); 121 | 122 | $this->filesystem->mkdir(dirname($fixturePath)); 123 | file_put_contents($fixturePath, $fixtureCode); 124 | return true; 125 | } 126 | 127 | /** 128 | * Return the method name to get item by identifier of the entity. 129 | * 130 | * @param ClassMetadata $class 131 | * 132 | * @return string 133 | * @throws \Exception 134 | * @throws \LogicException 135 | */ 136 | protected function getFindByIdentifier(ClassMetadata $class) 137 | { 138 | $identifiers = $class->getIdentifier(); 139 | if (count($identifiers) > 1){ 140 | throw new \Exception("Multiple identifiers is not supported."); 141 | } 142 | 143 | if (count($identifiers) === 0){ 144 | throw new \LogicException("This entity have no identifier."); 145 | } 146 | 147 | return "findBy".ucfirst($identifiers[0]); 148 | } 149 | 150 | /** 151 | * Return fixture file name 152 | * 153 | * @param $entity 154 | * @param $name 155 | * @param array $ids 156 | * 157 | * @return string 158 | */ 159 | public function getFixtureFileName($entity, $name, array $ids) 160 | { 161 | 162 | $fixtureFileName = "Load"; 163 | //if name params is set. 164 | if (strlen($name) > 0) { 165 | $fixtureFileName .= ucfirst($name); 166 | } else { 167 | //esle use entity name 168 | 169 | //ids with more than one entry should have --name set. 170 | if (count($ids) > 1) { 171 | throw new \RuntimeException('Fixture with multiple IDs should have the --name set.'); 172 | } 173 | 174 | $fixtureFileName = $this->getFixtureNameFromEntityName($entity, $ids); 175 | } 176 | 177 | return $fixtureFileName; 178 | } 179 | 180 | /** 181 | * Transform Entity name into a compatible filename. 182 | * 183 | * @param string $entity 184 | * @param array $ids 185 | * @param string $prefix 186 | * @return string 187 | */ 188 | public function getFixtureNameFromEntityName(string $entity, array $ids = [], string $prefix = null) 189 | { 190 | //noBackSlash 191 | $name = str_replace('\\', '', $entity); 192 | 193 | //add prefix. 194 | if (strlen($prefix) > 0) { 195 | $name = $prefix . ucfirst($name); 196 | } 197 | 198 | //add first ID in the name. 199 | if (isset($ids[0])) { 200 | $name .= $ids[0]; 201 | } 202 | 203 | return $name; 204 | } 205 | 206 | protected function getFqcnEntityClass($entity, $bundle, $isFqcnEntity = false) 207 | { 208 | if ($isFqcnEntity) { 209 | return $entity; 210 | } else { 211 | return $this->registry->getAliasNamespace($bundle) . '\\' . $entity; 212 | } 213 | } 214 | 215 | /** 216 | * Return the fixture generator object 217 | * 218 | * @return FixtureGenerator 219 | */ 220 | protected function getFixtureGenerator() 221 | { 222 | $fixtureGenerator = new FixtureGenerator(); 223 | $fixtureGenerator->setNumSpaces(4); 224 | 225 | return $fixtureGenerator; 226 | } 227 | 228 | } 229 | -------------------------------------------------------------------------------- /Generator/Entity.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 Webonaute\DoctrineFixturesGeneratorBundle\Generator; 13 | 14 | use Symfony\Component\Console\Output\ConsoleOutput; 15 | 16 | /** 17 | * Generator is the base class for all generators. 18 | * 19 | * @author Fabien Potencier 20 | */ 21 | class Generator 22 | { 23 | private $skeletonDirs; 24 | private static $output; 25 | 26 | /** 27 | * Sets an array of directories to look for templates. 28 | * 29 | * The directories must be sorted from the most specific to the most 30 | * directory. 31 | * 32 | * @param array $skeletonDirs An array of skeleton dirs 33 | */ 34 | public function setSkeletonDirs($skeletonDirs) 35 | { 36 | $this->skeletonDirs = is_array($skeletonDirs) ? $skeletonDirs : array($skeletonDirs); 37 | } 38 | 39 | protected function render($template, $parameters) 40 | { 41 | $twig = $this->getTwigEnvironment(); 42 | 43 | return $twig->render($template, $parameters); 44 | } 45 | 46 | /** 47 | * Gets the twig environment that will render skeletons. 48 | * 49 | * @return \Twig_Environment 50 | */ 51 | protected function getTwigEnvironment() 52 | { 53 | return new \Twig_Environment(new \Twig_Loader_Filesystem($this->skeletonDirs), array( 54 | 'debug' => true, 55 | 'cache' => false, 56 | 'strict_variables' => true, 57 | 'autoescape' => false, 58 | )); 59 | } 60 | 61 | protected function renderFile($template, $target, $parameters) 62 | { 63 | self::mkdir(dirname($target)); 64 | 65 | return self::dump($target, $this->render($template, $parameters)); 66 | } 67 | 68 | /** 69 | * @internal 70 | */ 71 | public static function mkdir($dir, $mode = 0777, $recursive = true) 72 | { 73 | if (!is_dir($dir)) { 74 | mkdir($dir, $mode, $recursive); 75 | self::writeln(sprintf(' created %s', self::relativizePath($dir))); 76 | } 77 | } 78 | 79 | /** 80 | * @internal 81 | */ 82 | public static function dump($filename, $content) 83 | { 84 | if (file_exists($filename)) { 85 | self::writeln(sprintf(' updated %s', self::relativizePath($filename))); 86 | } else { 87 | self::writeln(sprintf(' created %s', self::relativizePath($filename))); 88 | } 89 | 90 | return file_put_contents($filename, $content); 91 | } 92 | 93 | private static function writeln($message) 94 | { 95 | if (null === self::$output) { 96 | self::$output = new ConsoleOutput(); 97 | } 98 | 99 | self::$output->writeln($message); 100 | } 101 | 102 | private static function relativizePath($absolutePath) 103 | { 104 | $relativePath = str_replace(getcwd(), '.', $absolutePath); 105 | 106 | return is_dir($absolutePath) ? rtrim($relativePath, '/').'/' : $relativePath; 107 | } 108 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | DoctrineFixturesGeneratorBundle 2 | =============================== 3 | [![Latest Stable Version](https://poser.pugx.org/webonaute/doctrine-fixtures-generator-bundle/v/stable.svg)](https://packagist.org/packages/webonaute/doctrine-fixtures-generator-bundle) [![Total Downloads](https://poser.pugx.org/webonaute/doctrine-fixtures-generator-bundle/downloads.svg)](https://packagist.org/packages/webonaute/doctrine-fixtures-generator-bundle) [![Latest Unstable Version](https://poser.pugx.org/webonaute/doctrine-fixtures-generator-bundle/v/unstable.svg)](https://packagist.org/packages/webonaute/doctrine-fixtures-generator-bundle) [![License](https://poser.pugx.org/webonaute/doctrine-fixtures-generator-bundle/license.svg)](https://packagist.org/packages/webonaute/doctrine-fixtures-generator-bundle) 4 | 5 | Generate Fixture from your existing data in your database. You can specify the Entity name and the IDs you want to import in your fixture. 6 | 7 | Features include: 8 | 9 | - Create fixture from existing entity data. 10 | - Option to specify the exact IDs to import in your fixture. 11 | - Option to specify a ranges of ids to import in your fixture. (Thanks to [andreyserdjuk](https://github.com/andreyserdjuk)) 12 | - Manually set load order for a fixture from command line. (Thanks to [ioniks](https://github.com/ioniks)) 13 | - Allow to specify in the command line the specific load order we want for the generated fixture. 14 | - Snapshot : Create a full sets of fixtures from your current database. 15 | - Automatically set the load order in the snapshot context. (**NEW** Now support many to many relationship.) 16 | - Generate fixture reference when any other entity is link to this entity in the snapshot context. 17 | 18 | Version note 19 | ------------- 20 | 21 | - For symfony 2.3 and 2.4, use the version v1.0.* 22 | - For symfony 2.5 and to 3.4, use the version v1.3.* 23 | - For symfony 4.x and over, use version v2.x or 2.0-dev (master) . 24 | 25 | Documentation 26 | ------------- 27 | 28 | The bulk of the documentation is stored in the `Resources/doc/index.md` 29 | file in this bundle: 30 | 31 | [Read the Documentation for master](https://github.com/Webonaute/DoctrineFixturesGeneratorBundle/blob/master/Resources/doc/index.md) 32 | 33 | Installation 34 | ------------ 35 | 36 | All the installation instructions are located in the documentation. 37 | 38 | License 39 | ------- 40 | 41 | This bundle is under the MIT license. See the complete license in the bundle: 42 | 43 | Resources/meta/LICENSE 44 | 45 | About 46 | ----- 47 | 48 | DoctrineFixturesGeneratorBundle is a [Webonaute] initiative. 49 | See also the list of [contributors](https://github.com/Webonaute/DoctrineFixturesGeneratorBundle/contributors). 50 | 51 | Reporting an issue or a feature request 52 | --------------------------------------- 53 | 54 | Issues and feature requests are tracked in the [Github issue tracker](https://github.com/Webonaute/DoctrineFixturesGeneratorBundle/issues). 55 | 56 | When reporting a bug, it may be a good idea to reproduce it in a basic project 57 | built using the [Symfony Standard Edition](https://github.com/symfony/symfony-standard) 58 | to allow developers of the bundle to reproduce the issue by simply cloning it 59 | and following some steps. 60 | 61 | Help development 62 | ---------------- 63 | 64 | If you like this bundle, you can donate bitcoin to this address : 13zeEE6qdWJfSpNWwtWUuMoKTYGWU6jNwc 65 | 66 | Thank you! 67 | -------------------------------------------------------------------------------- /Resources/config/services.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | 3 | Webonaute\DoctrineFixturesGeneratorBundle\Command\GenerateDoctrineFixtureCommand: 4 | tags: 5 | - { name: 'console.command', command: 'doctrine:generate:fixture' } -------------------------------------------------------------------------------- /Resources/doc/index.md: -------------------------------------------------------------------------------- 1 | Getting Started With DoctrineFixturesGeneratorBundle 2 | ================================== 3 | 4 | ## Prerequisites 5 | 6 | This version of the bundle requires Symfony 2.8.x OR 3.x. 7 | 8 | ## Installation 9 | 10 | Installation is a quick (I promise!) 3 step process: 11 | 12 | 1. Download Webonaute\DoctrineFixturesGeneratorBundle using composer 13 | 2. Enable the Bundle 14 | 3. Generate your fixtures 15 | 16 | ### Step 1: Download Webonaute\DoctrineFixturesGeneratorBundle using composer 17 | 18 | Add Webonaute\DoctrineFixturesGeneratorBundle by running the command: 19 | 20 | ``` bash 21 | $ php composer.phar require webonaute/doctrine-fixtures-generator-bundle dev-master 22 | ``` 23 | 24 | Composer will install the bundle to your project's `vendor/Webonaute` directory. 25 | 26 | ### Step 2: Enable the bundle 27 | 28 | Enable the bundle in the kernel: 29 | 30 | Be sure to use it only in your dev or test environement. 31 | 32 | ``` php 33 | getEnvironment(), array('dev', 'test'))) { 39 | // ... 40 | $bundles[] = new Webonaute\DoctrineFixturesGeneratorBundle\DoctrineFixturesGeneratorBundle(); 41 | // ... 42 | } 43 | } 44 | ``` 45 | 46 | ### Step 3: Generate your fixture. 47 | ``` bash 48 | $ php bin/console doctrine:generate:fixture --entity=Blog:BlogPost --ids="12 534 124" --name="bug43" --order="1" 49 | ``` 50 | 51 | Then edit your new fixture BlogBundle/DataFixture/Orm/LoadBug43.php. 52 | 53 | Voila! 54 | 55 | ## Snapshot 56 | 57 | Since version 1.3, you can do a full snapshot of your existing database. 58 | 59 | To do so, run this command : 60 | ``` bash 61 | php app/console doctrine:generate:fixture --snapshot --overwrite 62 | ``` 63 | It will create one file per entity you have in your project, it will create it in ```src//DataFixtures/ORM/Load<Entity.php``` 64 | 65 | If you have entity relation, the load order will be automatically set according to that. 66 | 67 | ## Property Annotation 68 | You can set a column to not be imported at all into your fixture. 69 | To do so, you can add this annotation to any property of your entity. 70 | ``` 71 | @Webonaute\DoctrineFixturesGeneratorBundle\Annotation\Property(ignoreInSnapshot=true) 72 | ``` 73 | 74 | Entity Example : 75 | ``` 76 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | namespace FOS\TwitterBundle\Tests\DependencyInjection; 10 | 11 | use PHPUnit\Framework\TestCase; 12 | 13 | class DoctrineFixturesGeneratorTest extends TestCase 14 | { 15 | 16 | /** 17 | * @todo. 18 | */ 19 | public function testLoadFailed() 20 | { 21 | $this->assertTrue(true); 22 | } 23 | 24 | 25 | } 26 | -------------------------------------------------------------------------------- /Tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | if ( ! is_file($autoloadFile = __DIR__ . '/../vendor/autoload.php')) { 10 | throw new \LogicException('Could not find autoload.php in vendor/. Did you run "composer install --dev"?'); 11 | } 12 | 13 | require $autoloadFile; 14 | -------------------------------------------------------------------------------- /Tool/FixtureGenerator.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Webonaute\DoctrineFixturesGeneratorBundle\Tool; 11 | 12 | use Doctrine\Common\Annotations\AnnotationReader; 13 | use Doctrine\Common\Util\ClassUtils; 14 | use Doctrine\ORM\EntityManagerInterface; 15 | use Doctrine\ORM\Mapping\ClassMetadataInfo; 16 | use Doctrine\ORM\PersistentCollection; 17 | use Webonaute\DoctrineFixturesGeneratorBundle\Annotation\Property; 18 | 19 | /** 20 | * Generic class used to generate PHP5 fixture classes from existing data. 21 | * [php] 22 | * $classes = $em->getClassMetadataFactory()->getAllMetadata(); 23 | * $generator = new \Doctrine\ORM\Tools\EntityGenerator(); 24 | * $generator->setGenerateAnnotations(true); 25 | * $generator->setGenerateStubMethods(true); 26 | * $generator->setRegenerateEntityIfExists(false); 27 | * $generator->setUpdateEntityIfExists(true); 28 | * $generator->generate($classes, '/path/to/generate/entities'); 29 | * 30 | * @author Mathieu Delisle 31 | */ 32 | class FixtureGenerator 33 | { 34 | 35 | /** 36 | * @var string 37 | */ 38 | protected static $classTemplate 39 | = ' 42 | 43 | use Doctrine\Bundle\FixturesBundle\Fixture; 44 | use Doctrine\Common\DataFixtures\OrderedFixtureInterface; 45 | use Doctrine\Common\Persistence\ObjectManager; 46 | use Doctrine\ORM\Mapping\ClassMetadata; 47 | 48 | 49 | /** 50 | * Generated by Webonaute\DoctrineFixtureGenerator. 51 | */ 52 | 53 | { 54 | 55 | /** 56 | * Set loading order. 57 | * 58 | * @return int 59 | */ 60 | public function getOrder() 61 | { 62 | return ; 63 | } 64 | 65 | 66 | } 67 | '; 68 | 69 | /** 70 | * @var string 71 | */ 72 | protected static $getItemFixtureTemplate 73 | = ' 74 | $item = new (); 75 | $manager->persist($item); 76 | '; 77 | 78 | /** 79 | * @var string 80 | */ 81 | protected static $getLoadMethodTemplate 82 | = ' 83 | /** 84 | * {@inheritDoc} 85 | */ 86 | public function load(ObjectManager $manager) 87 | { 88 | $manager->getClassMetadata(::class)->setIdGeneratorType(ClassMetadata::GENERATOR_TYPE_NONE); 89 | 90 | 91 | $manager->flush(); 92 | } 93 | '; 94 | 95 | /** 96 | * @var bool 97 | */ 98 | protected $backupExisting = true; 99 | 100 | /** 101 | * @var string 102 | */ 103 | protected $bundleNameSpace = ""; 104 | 105 | /** 106 | * The class all generated entities should extend. 107 | * 108 | * @var string 109 | */ 110 | protected $classToExtend = "Fixture implements OrderedFixtureInterface"; 111 | 112 | /** 113 | * The extension to use for written php files. 114 | * 115 | * @var string 116 | */ 117 | protected $extension = '.php'; 118 | 119 | /** 120 | * @var string 121 | */ 122 | protected $fixtureName = ""; 123 | 124 | /** 125 | * Whether or not the current ClassMetadataInfo instance is new or old. 126 | * 127 | * @var boolean 128 | */ 129 | protected $isNew = true; 130 | 131 | /** 132 | * Array of data to generate item stubs. 133 | * 134 | * @var array 135 | */ 136 | protected $items = array(); 137 | 138 | /** 139 | * @var ClassMetadataInfo 140 | * @return FixtureGenerator 141 | */ 142 | protected $metadata = null; 143 | 144 | /** 145 | * Number of spaces to use for indention in generated code. 146 | */ 147 | protected $numSpaces = 4; 148 | 149 | /** 150 | * Order of the fixture execution. 151 | */ 152 | protected $fixtureorder = 1; 153 | 154 | /** 155 | * The actual spaces to use for indention. 156 | * 157 | * @var string 158 | */ 159 | protected $spaces = ' '; 160 | 161 | /** 162 | * @var array 163 | */ 164 | protected $staticReflection = array(); 165 | 166 | /** 167 | * @var string 168 | */ 169 | protected $referencePrefix = '_reference_'; 170 | /** 171 | * @var EntityManagerInterface 172 | */ 173 | protected $entityManager; 174 | 175 | /** 176 | * Constructor. 177 | */ 178 | public function __construct() 179 | { 180 | 181 | } 182 | 183 | /** 184 | * Generates and writes entity classes for the given array of ClassMetadataInfo instances. 185 | * 186 | * @param string $outputDirectory 187 | * 188 | * @return void 189 | */ 190 | public function generate($outputDirectory) 191 | { 192 | $this->writeFixtureClass($outputDirectory); 193 | } 194 | 195 | /** 196 | * Generates and writes entity class to disk for the given ClassMetadataInfo instance. 197 | * 198 | * @param string $outputDirectory 199 | * 200 | * @return void 201 | * @throws \RuntimeException 202 | */ 203 | public function writeFixtureClass($outputDirectory) 204 | { 205 | $path = $outputDirectory . '/' . str_replace( 206 | '\\', 207 | DIRECTORY_SEPARATOR, 208 | $this->getFixtureName() 209 | ) . $this->extension; 210 | $dir = dirname($path); 211 | 212 | if (!is_dir($dir)) { 213 | mkdir($dir, 0777, true); 214 | } 215 | 216 | $this->isNew = !file_exists($path) || (file_exists($path) && $this->regenerateEntityIfExists); 217 | 218 | if ($this->backupExisting && file_exists($path)) { 219 | $backupPath = dirname($path) . DIRECTORY_SEPARATOR . basename($path) . "~"; 220 | if (!copy($path, $backupPath)) { 221 | throw new \RuntimeException("Attempt to backup overwritten entity file but copy operation failed."); 222 | } 223 | } 224 | 225 | file_put_contents($path, $this->generateFixtureClass()); 226 | } 227 | 228 | /** 229 | * @return string 230 | */ 231 | public function getFixtureName() 232 | { 233 | return $this->fixtureName; 234 | } 235 | 236 | /** 237 | * @param string $fixtureName 238 | * 239 | * @return FixtureGenerator 240 | */ 241 | public function setFixtureName($fixtureName) 242 | { 243 | $this->fixtureName = $fixtureName; 244 | 245 | return $this; 246 | } 247 | 248 | /** 249 | * Generates a PHP5 Doctrine 2 entity class from the given ClassMetadataInfo instance. 250 | * 251 | * @return string 252 | */ 253 | public function generateFixtureClass() 254 | { 255 | 256 | if (is_null($this->getMetadata())) { 257 | throw new \RuntimeException("No metadata set."); 258 | } 259 | 260 | $placeHolders = array( 261 | '', 262 | '', 263 | '', 264 | '', 265 | '', 266 | ); 267 | 268 | $replacements = array( 269 | $this->generateFixtureNamespace(), 270 | $this->generateFixtureClassName(), 271 | $this->generateFixtureBody(), 272 | $this->generateUse(), 273 | $this->generateOrder(), 274 | ); 275 | 276 | $code = str_replace($placeHolders, $replacements, self::$classTemplate); 277 | 278 | return str_replace('', $this->spaces, $code); 279 | } 280 | 281 | /** 282 | * @return ClassMetadataInfo 283 | */ 284 | public function getMetadata() 285 | { 286 | return $this->metadata; 287 | } 288 | 289 | public function setMetadata(ClassMetadataInfo $metadata) 290 | { 291 | $this->metadata = $metadata; 292 | 293 | return $this; 294 | } 295 | 296 | /** 297 | * @return string 298 | */ 299 | public function getBundleNameSpace() 300 | { 301 | return $this->bundleNameSpace; 302 | } 303 | 304 | /** 305 | * @param $namespace 306 | * 307 | * @return FixtureGenerator 308 | */ 309 | public function setBundleNameSpace($namespace) 310 | { 311 | $this->bundleNameSpace = $namespace; 312 | 313 | return $this; 314 | } 315 | 316 | /** 317 | * @param object $item 318 | * 319 | * @return string 320 | */ 321 | public function generateFixtureItemStub($item) 322 | { 323 | $class = get_class($item); 324 | $ids = $this->getRelatedIdsForReference($class, $item); 325 | 326 | $code = ""; 327 | $reflexion = new \ReflectionClass($item); 328 | $properties = $this->getRecursiveProperties($reflexion); 329 | $newInstance = $this->getNewInstance($item,$reflexion); 330 | 331 | $code .= "\n\$this->addReference('{$this->referencePrefix}{$this->getEntityNameForRef($class)}{$ids}', \$item{$ids});"; 332 | 333 | foreach ($properties as $property) { 334 | $setValue = null; 335 | $property->setAccessible(true); 336 | $name = $property->getName(); 337 | if (strpos($name, '_')) { 338 | $_names = explode('_', $property->getName()); 339 | foreach ($_names as $k => $_name) { 340 | $_names[$k] = ucfirst($_name); 341 | } 342 | $name = implode('', $_names); 343 | } 344 | $setter = "set" . ucfirst($name); 345 | $getter = "get" . ucfirst($name); 346 | $comment = ""; 347 | if (method_exists($item, $setter)) { 348 | $value = $property->getValue($item); 349 | $defaultValue = $property->getValue($newInstance); 350 | if ($value === $defaultValue) { 351 | continue; 352 | } elseif (is_integer($value)) { 353 | $setValue = $value; 354 | } elseif ($value === false || $value === true) { 355 | if ($value === true) { 356 | $setValue = "true"; 357 | } else { 358 | $setValue = "false"; 359 | } 360 | } elseif ($value instanceof \DateTime) { 361 | $setValue = "new \\DateTime(\"" . $value->format("Y-m-d H:i:s") . "\")"; 362 | } elseif (is_object($value) && get_class($value) != "Doctrine\\ORM\\PersistentCollection") { 363 | if ($this->hasIgnoreProperty($property) === false) { 364 | //check reference. 365 | $relatedClass = get_class($value); 366 | $relatedEntity = ClassUtils::getRealClass($relatedClass); 367 | $identifiersIdsString = $this->getRelatedIdsForReference($relatedEntity, $value); 368 | $setValue = "\$this->getReference('{$this->referencePrefix}{$this->getEntityNameForRef($relatedEntity)}$identifiersIdsString')"; 369 | $comment = ""; 370 | 371 | } else { 372 | //ignore data for this property. 373 | continue; 374 | } 375 | } elseif (is_object($value) && get_class($value) == "Doctrine\\ORM\\PersistentCollection") { 376 | /** @var PersistentCollection $value */ 377 | $meta = $this->metadata->getAssociationMapping($property->getName()); 378 | 379 | if ($meta['isOwningSide'] === true && $value->isEmpty() === false) { 380 | $setValue = "[\n"; 381 | foreach ($value as $object) { 382 | $relatedClass = get_class($object); 383 | $relatedEntity = ClassUtils::getRealClass($relatedClass); 384 | $identifiersIdsString = $this->getRelatedIdsForReference($relatedEntity, $object); 385 | $setValue .= $this->spaces.$this->spaces.$this->spaces."\$this->getReference('{$this->referencePrefix}{$this->getEntityNameForRef($relatedEntity)}$identifiersIdsString'),\n"; 386 | $comment = ""; 387 | } 388 | $setValue .= $this->spaces.$this->spaces."]"; 389 | }else{ 390 | //nothing to add. 391 | continue; 392 | } 393 | 394 | } elseif (is_array($value)) { 395 | $setValue = "unserialize('" . str_replace(['\''], ['\\\''], serialize($value)) . "')"; 396 | } elseif (is_null($value)) { 397 | $setValue = "NULL"; 398 | } else { 399 | $setValue = '"' . str_replace(['"', '$'], ['\"', '\$'], $value) . '"'; 400 | } 401 | 402 | $code .= "\n{$comment}\$item{$ids}->{$setter}({$setValue});"; 403 | } 404 | } 405 | 406 | $code .= "\n"; 407 | 408 | return $code; 409 | } 410 | 411 | protected function getEntityNameForRef($entityFQN){ 412 | return str_replace("\\", "", $entityFQN); 413 | } 414 | 415 | protected function getRecursiveProperties(\ReflectionClass $reflection){ 416 | $properties = $reflection->getProperties(); 417 | $parentReflection = $reflection->getParentClass(); 418 | if ($parentReflection !== false){ 419 | $parentProperties = $this->getRecursiveProperties($parentReflection); 420 | //only get private property. 421 | $parentProperties = array_filter($parentProperties, function(\ReflectionProperty $property){ 422 | if ($property->isPrivate()){ 423 | return true; 424 | }else{ 425 | return false; 426 | } 427 | }); 428 | $properties = array_merge($properties, $parentProperties); 429 | } 430 | 431 | return $properties; 432 | } 433 | 434 | /** 435 | * @return string 436 | */ 437 | public function getFixtureOrder() 438 | { 439 | return $this->fixtureorder; 440 | } 441 | 442 | /** 443 | * @param string $fixtureOrder 444 | * 445 | * @return FixtureGenerator 446 | */ 447 | public function setFixtureOrder($fixtureOrder) 448 | { 449 | $this->fixtureorder = $fixtureOrder; 450 | 451 | return $this; 452 | } 453 | 454 | /** 455 | * @param EntityManagerInterface $entityManager 456 | * 457 | * @return FixtureGenerator 458 | */ 459 | public function setEntityManager(EntityManagerInterface $entityManager) 460 | { 461 | $this->entityManager = $entityManager; 462 | 463 | return $this; 464 | } 465 | 466 | /** 467 | * @return array 468 | */ 469 | public function getItems() 470 | { 471 | return $this->items; 472 | } 473 | 474 | /** 475 | * @param array $items 476 | */ 477 | public function setItems(array $items) 478 | { 479 | $this->items = $items; 480 | } 481 | 482 | /** 483 | * Sets the extension to use when writing php files to disk. 484 | * 485 | * @param string $extension 486 | * 487 | * @return void 488 | */ 489 | public function setExtension($extension) 490 | { 491 | $this->extension = $extension; 492 | } 493 | 494 | /** 495 | * Sets the number of spaces the exported class should have. 496 | * 497 | * @param integer $numSpaces 498 | * 499 | * @return void 500 | */ 501 | public function setNumSpaces($numSpaces) 502 | { 503 | $this->spaces = str_repeat(' ', $numSpaces); 504 | $this->numSpaces = $numSpaces; 505 | } 506 | 507 | /** 508 | * @return string 509 | */ 510 | protected function generateFixtureNamespace() 511 | { 512 | $namespace = 'namespace '.$this->getNamespace(); 513 | if (strpos($namespace, ';') === false) { 514 | $namespace.';'; 515 | } 516 | 517 | return $namespace; 518 | } 519 | 520 | /** 521 | * @return string 522 | */ 523 | protected function getNamespace() 524 | { 525 | return $this->getBundleNameSpace().'\DataFixtures\ORM;'; 526 | } 527 | 528 | /** 529 | * @return string 530 | */ 531 | protected function generateFixtureClassName() 532 | { 533 | return 'class '.$this->getClassName().' extends '.$this->getClassToExtend(); 534 | } 535 | 536 | /** 537 | * @return string 538 | */ 539 | protected function getClassName() 540 | { 541 | return $this->fixtureName; 542 | } 543 | 544 | /** 545 | * @return string 546 | */ 547 | protected function getClassToExtend() 548 | { 549 | return $this->classToExtend; 550 | } 551 | 552 | /** 553 | * Sets the name of the class the generated classes should extend from. 554 | * 555 | * @param string $classToExtend 556 | * 557 | * @return void 558 | */ 559 | public function setClassToExtend($classToExtend) 560 | { 561 | $this->classToExtend = $classToExtend; 562 | } 563 | 564 | /** 565 | * @return string 566 | */ 567 | protected function generateFixtureBody() 568 | { 569 | $code = self::$getLoadMethodTemplate; 570 | $classpath = $this->getMetadata()->getName(); 571 | $pos = strrpos($classpath, "\\"); 572 | 573 | $code = str_replace("", substr($classpath, $pos + 1), $code); 574 | $code = str_replace("", $this->generateFixtures(), $code); 575 | 576 | return $code; 577 | } 578 | 579 | protected function generateFixtures() 580 | { 581 | $code = ""; 582 | 583 | foreach ($this->items as $item) { 584 | $code .= $this->generateFixture($item); 585 | } 586 | 587 | return $code; 588 | } 589 | 590 | /** 591 | * @param $item 592 | * 593 | * @return string 594 | */ 595 | protected function generateFixture($item) 596 | { 597 | 598 | $placeHolders = [ 599 | '', 600 | '', 601 | '', 602 | '', 603 | ]; 604 | 605 | $reflexionClass = new \ReflectionClass($item); 606 | $constructorParams = $this->getConstructorParams($item, $reflexionClass); 607 | $constructorParamString = ''; 608 | if (!empty($constructorParams)) { 609 | $constructorParamString = "'".implode("', '", $constructorParams)."'"; 610 | } 611 | 612 | $replacements = [ 613 | $this->getRelatedIdsForReference(get_class($item), $item), 614 | $reflexionClass->getShortName(), 615 | $this->generateFixtureItemStub($item), 616 | $constructorParamString 617 | ]; 618 | 619 | $code = str_replace($placeHolders, $replacements, self::$getItemFixtureTemplate); 620 | 621 | return $code; 622 | } 623 | 624 | /** 625 | * @param string $fqcn 626 | * 627 | * @return string 628 | */ 629 | protected function getRelatedIdsForReference(string $fqcn, $value) 630 | { 631 | $relatedClassMeta = $this->entityManager->getClassMetadata($fqcn); 632 | $identifiers = $relatedClassMeta->getIdentifier(); 633 | $ret = ""; 634 | if (!empty($identifiers)) { 635 | foreach ($identifiers as $identifier) { 636 | $method = "get".ucfirst($identifier); 637 | if (method_exists($value, $method)){ 638 | //change all - for _ in case identifier use UUID as '-' is not a permitted symbol 639 | $ret .= $this->sanitizeSuspiciousSymbols($value->$method()); 640 | }else{ 641 | $ret .= $this->sanitizeSuspiciousSymbols($value->$identifier); 642 | } 643 | } 644 | } 645 | 646 | return $ret; 647 | } 648 | 649 | protected function hasIgnoreProperty($propertyReflection) 650 | { 651 | $reader = new AnnotationReader(); 652 | 653 | /** @var Property $propertyAnnotation */ 654 | $propertyAnnotation = $reader->getPropertyAnnotation( 655 | $propertyReflection, 656 | 'Webonaute\DoctrineFixturesGeneratorBundle\Annotation\Property' 657 | ); 658 | 659 | if ($propertyAnnotation !== null && $propertyAnnotation->ignoreInSnapshot === true) { 660 | //ignore this mapping. (data will not be exported for that field.) 661 | return true; 662 | } 663 | 664 | return false; 665 | } 666 | 667 | protected function generateUse() 668 | { 669 | return "use ".$this->getMetadata()->name.";"; 670 | } 671 | 672 | /** 673 | * @return int 674 | */ 675 | protected function generateOrder() 676 | { 677 | return $this->fixtureorder; 678 | } 679 | 680 | protected function generateFixtureLoadMethod(ClassMetadataInfo $metadata) 681 | { 682 | 683 | } 684 | 685 | /** 686 | * sanitize illegal symbols in variable name suffix 687 | * @param string $string 688 | * @return string 689 | */ 690 | private function sanitizeSuspiciousSymbols($string) 691 | { 692 | $sanitizedString = preg_replace('/[^a-zA-Z0-9_]/', '_', $string); 693 | 694 | return $sanitizedString; 695 | } 696 | 697 | /** 698 | * @param $item 699 | * @param \ReflectionClass $reflexion 700 | * 701 | * @return mixed 702 | */ 703 | private function getNewInstance($item, \ReflectionClass $reflexion) 704 | { 705 | if (null === $reflexion->getConstructor()) { 706 | return $reflexion->newInstance(); 707 | } 708 | $constructorParams = $this->getConstructorParams($item, $reflexion); 709 | if (empty($constructorParams)) { 710 | return $reflexion->newInstance(); 711 | } 712 | 713 | return call_user_func_array(array($reflexion, 'newInstance'), $constructorParams); 714 | } 715 | 716 | /** 717 | * @param $item 718 | * @param \ReflectionClass $reflexion 719 | * 720 | * @return array 721 | */ 722 | private function getConstructorParams($item, \ReflectionClass $reflexion): array 723 | { 724 | if (is_null($reflexion->getConstructor())) { 725 | return []; 726 | } 727 | 728 | $constructorParams = []; 729 | foreach ($reflexion->getConstructor()->getParameters() as $parameter) { 730 | if ($parameter->isDefaultValueAvailable()) { 731 | continue; 732 | } 733 | $paramName = $parameter->getName(); 734 | if ($reflexion->hasMethod('get'.ucfirst($paramName))) { 735 | $constructorParams[] = $item->{'get'.ucfirst($paramName)}(); 736 | } elseif ($reflexion->hasProperty($paramName)) { 737 | $reflectionProperty = $reflexion->getProperty($paramName); 738 | $reflectionProperty->setAccessible(true); 739 | $constructorParams[] = $reflectionProperty->getValue($item); 740 | } 741 | } 742 | 743 | return $constructorParams; 744 | } 745 | } 746 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webonaute/doctrine-fixtures-generator-bundle", 3 | "description": "Generate Fixture from your existing data in your database. You can specify the Entity name and the IDs you want to import in your fixture.", 4 | "type": "symfony-bundle", 5 | "keywords": [ 6 | "development", 7 | "database", 8 | "fixture", 9 | "symfony", 10 | "symfony2", 11 | "symfony4", 12 | "generator", 13 | "dumper", 14 | "doctrine", 15 | "doctrine2", 16 | "tests", 17 | "migration" 18 | ], 19 | "license": "MIT", 20 | "authors": [ 21 | { 22 | "name": "Mathieu Delisle", 23 | "email": "mdelisle@webonaute.ca" 24 | } 25 | ], 26 | "support": { 27 | "email": "mdelisle@webonaute.ca", 28 | "issue": "https://github.com/Webonaute/DoctrineFixturesGeneratorBundle/issues", 29 | "source": "https://github.com/Webonaute/DoctrineFixturesGeneratorBundle" 30 | }, 31 | "require": { 32 | "php": ">=7.0", 33 | "symfony/framework-bundle": "~3.4|~4", 34 | "doctrine/orm": ">=2.2", 35 | "doctrine/doctrine-bundle": ">=1.2", 36 | "doctrine/doctrine-fixtures-bundle": ">=2.2" 37 | }, 38 | "require-dev": { 39 | "phpunit/phpunit": "^6.0", 40 | "symfony/phpunit-bridge": "dev-master", 41 | "phpstan/phpstan": "^0.9.2" 42 | }, 43 | "autoload": { 44 | "psr-0": {"Webonaute\\DoctrineFixturesGeneratorBundle": ""} 45 | }, 46 | "target-dir": "Webonaute/DoctrineFixturesGeneratorBundle", 47 | "extra": { 48 | "branch-alias": { 49 | "dev-master": "2.0-dev" 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | excludes_analyse: 3 | - */Tests/* 4 | - */DataFixtures/* 5 | - */parameters.php 6 | - */GroupsFilterQueryTrait.php 7 | # ignoreErrors: 8 | # - '#Call to an undefined method Predis\\Client::[a-zA-Z\\\.]+#' -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ./Tests 8 | 9 | 10 | 11 | 12 | 13 | ./ 14 | 15 | ./Resources 16 | ./Tests 17 | ./vendor 18 | 19 | 20 | 21 | 22 | --------------------------------------------------------------------------------