ヤミRoot VoidGate
User / IP
:
216.73.216.81
Host / Server
:
146.88.233.70 / dev.loger.cm
System
:
Linux hybrid1120.fr.ns.planethoster.net 3.10.0-957.21.2.el7.x86_64 #1 SMP Wed Jun 5 14:26:44 UTC 2019 x86_64
Command
|
Upload
|
Create
Mass Deface
|
Jumping
|
Symlink
|
Reverse Shell
Ping
|
Port Scan
|
DNS Lookup
|
Whois
|
Header
|
cURL
:
/
home
/
logercm
/
dev.loger.cm
/
fixtures
/
assert
/
Viewing: maker-bundle.tar
src/Command/MakerCommand.php 0000644 00000010704 15120141001 0011743 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Command; use Symfony\Bundle\MakerBundle\ApplicationAwareMakerInterface; use Symfony\Bundle\MakerBundle\ConsoleStyle; use Symfony\Bundle\MakerBundle\DependencyBuilder; use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException; use Symfony\Bundle\MakerBundle\FileManager; use Symfony\Bundle\MakerBundle\Generator; use Symfony\Bundle\MakerBundle\InputConfiguration; use Symfony\Bundle\MakerBundle\MakerInterface; use Symfony\Bundle\MakerBundle\Validator; use Symfony\Component\Console\Application; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; /** * Used as the Command class for the makers. * * @internal */ final class MakerCommand extends Command { private $maker; private $fileManager; private $inputConfig; /** @var ConsoleStyle */ private $io; private $checkDependencies = true; private $generator; public function __construct(MakerInterface $maker, FileManager $fileManager, Generator $generator) { $this->maker = $maker; $this->fileManager = $fileManager; $this->inputConfig = new InputConfiguration(); $this->generator = $generator; parent::__construct(); } protected function configure(): void { $this->maker->configureCommand($this, $this->inputConfig); } protected function initialize(InputInterface $input, OutputInterface $output): void { $this->io = new ConsoleStyle($input, $output); $this->fileManager->setIO($this->io); if ($this->checkDependencies) { $dependencies = new DependencyBuilder(); $this->maker->configureDependencies($dependencies, $input); if (!$dependencies->isPhpVersionSatisfied()) { throw new RuntimeCommandException('The make:entity command requires that you use PHP 7.1 or higher.'); } if ($missingPackagesMessage = $dependencies->getMissingPackagesMessage($this->getName())) { throw new RuntimeCommandException($missingPackagesMessage); } } } protected function interact(InputInterface $input, OutputInterface $output): void { if (!$this->fileManager->isNamespaceConfiguredToAutoload($this->generator->getRootNamespace())) { $this->io->note([ sprintf('It looks like your app may be using a namespace other than "%s".', $this->generator->getRootNamespace()), 'To configure this and make your life easier, see: https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html#configuration', ]); } foreach ($this->getDefinition()->getArguments() as $argument) { if ($input->getArgument($argument->getName())) { continue; } if (\in_array($argument->getName(), $this->inputConfig->getNonInteractiveArguments(), true)) { continue; } $value = $this->io->ask($argument->getDescription(), $argument->getDefault(), [Validator::class, 'notBlank']); $input->setArgument($argument->getName(), $value); } $this->maker->interact($input, $this->io, $this); } protected function execute(InputInterface $input, OutputInterface $output): int { $this->maker->generate($input, $this->io, $this->generator); // sanity check for custom makers if ($this->generator->hasPendingOperations()) { throw new \LogicException('Make sure to call the writeChanges() method on the generator.'); } return 0; } public function setApplication(Application $application = null): void { parent::setApplication($application); if ($this->maker instanceof ApplicationAwareMakerInterface) { if (null === $application) { throw new \RuntimeException('Application cannot be null.'); } $this->maker->setApplication($application); } } /** * @internal Used for testing commands */ public function setCheckDependencies(bool $checkDeps): void { $this->checkDependencies = $checkDeps; } } src/Console/MigrationDiffFilteredOutput.php 0000644 00000007707 15120141001 0015064 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Console; use Symfony\Component\Console\Formatter\OutputFormatterInterface; use Symfony\Component\Console\Output\OutputInterface; if (\PHP_VERSION_ID < 80000 // look for the "string|iterable" type on OutputInterface::write() || !(new \ReflectionMethod(OutputInterface::class, 'write'))->getParameters()[0]->getType()) { class MigrationDiffFilteredOutput implements OutputInterface { use BaseMakerMigrationDiffFilteredOuputTrait; public function write($messages, $newline = false, $options = 0) { $this->_write($messages, $newline, $options); } public function writeln($messages, $options = 0) { $this->_writeln($messages, $options); } public function setVerbosity($level) { $this->output->setVerbosity($level); } public function setDecorated($decorated) { $this->output->setDecorated($decorated); } } } else { require __DIR__.'/MigrationDiffFilteredOutput_php8'; } trait BaseMakerMigrationDiffFilteredOuputTrait { private $output; private $buffer = ''; private $previousLineWasRemoved = false; public function __construct(OutputInterface $output) { $this->output = $output; } public function _write($messages, bool $newline = false, $options = 0) { $messages = $this->filterMessages($messages, $newline); $this->output->write($messages, $newline, $options); } public function _writeln($messages, int $options = 0) { $messages = $this->filterMessages($messages, true); $this->output->writeln($messages, $options); } public function getVerbosity(): int { return $this->output->getVerbosity(); } public function isQuiet(): bool { return $this->output->isQuiet(); } public function isVerbose(): bool { return $this->output->isVerbose(); } public function isVeryVerbose(): bool { return $this->output->isVeryVerbose(); } public function isDebug(): bool { return $this->output->isDebug(); } public function isDecorated(): bool { return $this->output->isDecorated(); } public function setFormatter(OutputFormatterInterface $formatter) { $this->output->setFormatter($formatter); } public function getFormatter(): OutputFormatterInterface { return $this->output->getFormatter(); } public function fetch(): string { return $this->buffer; } private function filterMessages($messages, bool $newLine) { if (!is_iterable($messages)) { $messages = [$messages]; } $hiddenPhrases = [ 'Generated new migration class', 'To run just this migration', 'To revert the migration you', ]; foreach ($messages as $key => $message) { $this->buffer .= $message; if ($newLine) { $this->buffer .= \PHP_EOL; } if ($this->previousLineWasRemoved && !trim($message)) { // hide a blank line after a filtered line unset($messages[$key]); $this->previousLineWasRemoved = false; continue; } $this->previousLineWasRemoved = false; foreach ($hiddenPhrases as $hiddenPhrase) { if (false !== strpos($message, $hiddenPhrase)) { $this->previousLineWasRemoved = true; unset($messages[$key]); break; } } } return array_values($messages); } } src/Console/MigrationDiffFilteredOutput_php8 0000644 00000001653 15120141001 0015227 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Console; use Symfony\Component\Console\Output\OutputInterface; class MigrationDiffFilteredOutput implements OutputInterface { use BaseMakerMigrationDiffFilteredOuputTrait; public function write($messages, bool $newline = false, $options = 0) { $this->_write($messages, $newline, $options); } public function writeln($messages, int $options = 0) { $this->_writeln($messages, $options); } public function setVerbosity(int $level) { $this->output->setVerbosity($level); } public function setDecorated(bool $decorated) { $this->output->setDecorated($decorated); } } src/DependencyInjection/CompilerPass/DoctrineAttributesCheckPass.php 0000644 00000001431 15120141001 0021751 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\DependencyInjection\CompilerPass; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; class DoctrineAttributesCheckPass implements CompilerPassInterface { public function process(ContainerBuilder $container): void { $container->setParameter( 'maker.compatible_check.doctrine.supports_attributes', $container->hasParameter('doctrine.orm.metadata.attribute.class') ); } } src/DependencyInjection/CompilerPass/MakeCommandRegistrationPass.php 0000644 00000005464 15120141001 0021756 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\DependencyInjection\CompilerPass; use Symfony\Bundle\MakerBundle\Command\MakerCommand; use Symfony\Bundle\MakerBundle\MakerInterface; use Symfony\Bundle\MakerBundle\Str; use Symfony\Component\Console\Command\LazyCommand; use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Reference; class MakeCommandRegistrationPass implements CompilerPassInterface { public const MAKER_TAG = 'maker.command'; public function process(ContainerBuilder $container): void { foreach ($container->findTaggedServiceIds(self::MAKER_TAG) as $id => $tags) { $def = $container->getDefinition($id); if ($def->isDeprecated()) { continue; } $class = $container->getParameterBag()->resolveValue($def->getClass()); if (!is_subclass_of($class, MakerInterface::class)) { throw new InvalidArgumentException(sprintf('Service "%s" must implement interface "%s".', $id, MakerInterface::class)); } $commandDefinition = new ChildDefinition('maker.auto_command.abstract'); $commandDefinition->setClass(MakerCommand::class); $commandDefinition->replaceArgument(0, new Reference($id)); $tagAttributes = ['command' => $class::getCommandName()]; if (!method_exists($class, 'getCommandDescription')) { // no-op } elseif (class_exists(LazyCommand::class)) { $tagAttributes['description'] = $class::getCommandDescription(); } else { $commandDefinition->addMethodCall('setDescription', [$class::getCommandDescription()]); } $commandDefinition->addTag('console.command', $tagAttributes); /* * @deprecated remove this block when removing make:unit-test and make:functional-test */ if (method_exists($class, 'getCommandAliases')) { foreach ($class::getCommandAliases() as $alias) { $commandDefinition->addTag('console.command', ['command' => $alias, 'description' => 'Deprecated alias of "make:test"']); } } $container->setDefinition(sprintf('maker.auto_command.%s', Str::asTwigVariable($class::getCommandName())), $commandDefinition); } } } src/DependencyInjection/CompilerPass/RemoveMissingParametersPass.php 0000644 00000001616 15120141001 0022015 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\DependencyInjection\CompilerPass; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; /** * Removes injected parameter arguments if they don't exist in this app. * * @author Ryan Weaver <ryan@symfonycasts.com> */ class RemoveMissingParametersPass implements CompilerPassInterface { public function process(ContainerBuilder $container): void { if (!$container->hasParameter('twig.default_path')) { $container->getDefinition('maker.file_manager') ->replaceArgument(4, null); } } } src/DependencyInjection/CompilerPass/SetDoctrineAnnotatedPrefixesPass.php 0000644 00000004725 15120141001 0022775 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\DependencyInjection\CompilerPass; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; class SetDoctrineAnnotatedPrefixesPass implements CompilerPassInterface { public function process(ContainerBuilder $container): void { $annotatedPrefixes = null; foreach ($container->findTaggedServiceIds('doctrine.orm.configuration') as $id => $tags) { $metadataDriverImpl = null; foreach ($container->getDefinition($id)->getMethodCalls() as [$method, $arguments]) { if ('setMetadataDriverImpl' === $method) { $metadataDriverImpl = $container->getDefinition($arguments[0]); break; } } if (null === $metadataDriverImpl || !preg_match('/^doctrine\.orm\.(.+)_configuration$/D', $id, $m)) { continue; } $managerName = $m[1]; $methodCalls = $metadataDriverImpl->getMethodCalls(); foreach ($methodCalls as $i => [$method, $arguments]) { if ('addDriver' !== $method) { continue; } if ($arguments[0] instanceof Definition) { $class = $arguments[0]->getClass(); $namespace = substr($class, 0, strrpos($class, '\\')); $id = sprintf('.%d_doctrine_metadata_driver~%s', $i, ContainerBuilder::hash($arguments)); $container->setDefinition($id, $arguments[0]); $arguments[0] = new Reference($id); $methodCalls[$i] = [$method, $arguments]; } $annotatedPrefixes[$managerName][] = [ $arguments[1], new Reference($arguments[0]), ]; } $metadataDriverImpl->setMethodCalls($methodCalls); } if (null !== $annotatedPrefixes) { $container->getDefinition('maker.doctrine_helper')->setArgument(4, $annotatedPrefixes); } } } src/DependencyInjection/Configuration.php 0000644 00000002062 15120141001 0014555 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\DependencyInjection; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; class Configuration implements ConfigurationInterface { /** * {@inheritdoc} */ public function getConfigTreeBuilder(): TreeBuilder { $treeBuilder = new TreeBuilder('maker'); if (method_exists($treeBuilder, 'getRootNode')) { $rootNode = $treeBuilder->getRootNode(); } else { // BC layer for symfony/config 4.1 and older $rootNode = $treeBuilder->root('maker'); } $rootNode ->children() ->scalarNode('root_namespace')->defaultValue('App')->end() ->end() ; return $treeBuilder; } } src/DependencyInjection/MakerExtension.php 0000644 00000005460 15120141001 0014707 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\DependencyInjection; use Symfony\Bundle\MakerBundle\DependencyInjection\CompilerPass\MakeCommandRegistrationPass; use Symfony\Bundle\MakerBundle\MakerInterface; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Loader; use Symfony\Component\HttpKernel\DependencyInjection\Extension; /** * This is the class that loads and manages your bundle configuration. * * @see http://symfony.com/doc/current/cookbook/bundles/extension.html */ class MakerExtension extends Extension { /** * @deprecated remove this block when removing make:unit-test and make:functional-test */ private const TEST_MAKER_DEPRECATION_MESSAGE = 'The "%service_id%" service is deprecated, use "maker.maker.make_test" instead.'; /** * {@inheritdoc} */ public function load(array $configs, ContainerBuilder $container): void { $loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); $loader->load('services.xml'); $loader->load('makers.xml'); /** * @deprecated remove this block when removing make:unit-test and make:functional-test */ $deprecParams = method_exists(Definition::class, 'getDeprecation') ? ['symfony/maker-bundle', '1.29', self::TEST_MAKER_DEPRECATION_MESSAGE] : [true, self::TEST_MAKER_DEPRECATION_MESSAGE]; $container ->getDefinition('maker.maker.make_unit_test') ->setDeprecated(...$deprecParams); $container ->getDefinition('maker.maker.make_functional_test') ->setDeprecated(...$deprecParams); $configuration = $this->getConfiguration($configs, $container); $config = $this->processConfiguration($configuration, $configs); $rootNamespace = trim($config['root_namespace'], '\\'); $autoloaderFinderDefinition = $container->getDefinition('maker.autoloader_finder'); $autoloaderFinderDefinition->replaceArgument(0, $rootNamespace); $makeCommandDefinition = $container->getDefinition('maker.generator'); $makeCommandDefinition->replaceArgument(1, $rootNamespace); $doctrineHelperDefinition = $container->getDefinition('maker.doctrine_helper'); $doctrineHelperDefinition->replaceArgument(0, $rootNamespace.'\\Entity'); $container->registerForAutoconfiguration(MakerInterface::class) ->addTag(MakeCommandRegistrationPass::MAKER_TAG); } } src/Docker/DockerDatabaseServices.php 0000644 00000005666 15120141001 0013611 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Docker; use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException; /** * @author Jesse Rushlow <jr@rushlow.dev> * * @internal */ final class DockerDatabaseServices { /** * @throws RuntimeCommandException */ public static function getDatabaseSkeleton(string $name, string $version): array { switch ($name) { case 'mariadb': return [ 'image' => sprintf('mariadb:%s', $version), 'environment' => [ 'MYSQL_ROOT_PASSWORD' => 'password', 'MYSQL_DATABASE' => 'main', ], ]; case 'mysql': return [ 'image' => sprintf('mysql:%s', $version), 'environment' => [ 'MYSQL_ROOT_PASSWORD' => 'password', 'MYSQL_DATABASE' => 'main', ], ]; case 'postgres': return [ 'image' => sprintf('postgres:%s', $version), 'environment' => [ 'POSTGRES_PASSWORD' => 'main', 'POSTGRES_USER' => 'main', 'POSTGRES_DB' => 'main', ], ]; } self::throwInvalidDatabase($name); } /** * @throws RuntimeCommandException */ public static function getDefaultPorts(string $name): array { switch ($name) { case 'mariadb': case 'mysql': return ['3306']; case 'postgres': return ['5432']; } self::throwInvalidDatabase($name); } public static function getSuggestedServiceVersion(string $name): string { if ('postgres' === $name) { return 'alpine'; } return 'latest'; } public static function getMissingExtensionName(string $name): ?string { switch ($name) { case 'mariadb': case 'mysql': $driver = 'mysql'; break; case 'postgres': $driver = 'pgsql'; break; default: self::throwInvalidDatabase($name); } if (!\in_array($driver, \PDO::getAvailableDrivers(), true)) { return $driver; } return null; } /** * @throws RuntimeCommandException */ private static function throwInvalidDatabase(string $name): void { throw new RuntimeCommandException(sprintf('%s is not a valid / supported docker database type.', $name)); } } src/Doctrine/BaseCollectionRelation.php 0000644 00000001540 15120141001 0014160 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Doctrine; use Symfony\Bundle\MakerBundle\Str; /** * @internal */ abstract class BaseCollectionRelation extends BaseRelation { abstract public function getOrphanRemoval(): bool; abstract public function getTargetSetterMethodName(): string; public function getAdderMethodName(): string { return 'add'.Str::asCamelCase(Str::pluralCamelCaseToSingular($this->getPropertyName())); } public function getRemoverMethodName(): string { return 'remove'.Str::asCamelCase(Str::pluralCamelCaseToSingular($this->getPropertyName())); } } src/Doctrine/BaseRelation.php 0000644 00000005112 15120141001 0012143 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Doctrine; /** * @internal */ abstract class BaseRelation { private $propertyName; private $targetClassName; private $targetPropertyName; private $customReturnType; private $isSelfReferencing = false; private $mapInverseRelation = true; private $avoidSetter = false; abstract public function isOwning(): bool; public function getPropertyName(): string { return $this->propertyName; } public function setPropertyName(string $propertyName): self { $this->propertyName = $propertyName; return $this; } public function getTargetClassName(): string { return $this->targetClassName; } public function setTargetClassName(string $targetClassName): self { $this->targetClassName = $targetClassName; return $this; } public function getTargetPropertyName(): ?string { return $this->targetPropertyName; } public function setTargetPropertyName(?string $targetPropertyName): self { $this->targetPropertyName = $targetPropertyName; return $this; } public function isSelfReferencing(): bool { return $this->isSelfReferencing; } public function setIsSelfReferencing(bool $isSelfReferencing): self { $this->isSelfReferencing = $isSelfReferencing; return $this; } public function getMapInverseRelation(): bool { return $this->mapInverseRelation; } public function setMapInverseRelation(bool $mapInverseRelation): self { $this->mapInverseRelation = $mapInverseRelation; return $this; } public function shouldAvoidSetter(): bool { return $this->avoidSetter; } public function avoidSetter(bool $avoidSetter = true): self { $this->avoidSetter = $avoidSetter; return $this; } public function getCustomReturnType(): ?string { return $this->customReturnType; } public function isCustomReturnTypeNullable(): bool { return $this->isCustomReturnTypeNullable; } public function setCustomReturnType(string $customReturnType, bool $isNullable) { $this->customReturnType = $customReturnType; $this->isCustomReturnTypeNullable = $isNullable; return $this; } } src/Doctrine/BaseSingleRelation.php 0000644 00000001266 15120141001 0013313 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Doctrine; /** * @internal */ abstract class BaseSingleRelation extends BaseRelation { private $isNullable; public function isNullable(): bool { if ($this->isNullable) { return $this->isNullable; } return false; } public function setIsNullable(bool $isNullable): self { $this->isNullable = $isNullable; return $this; } } src/Doctrine/DoctrineHelper.php 0000644 00000026751 15120141001 0012516 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Doctrine; use Doctrine\DBAL\Connection; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\Driver\AnnotationDriver; use Doctrine\ORM\Mapping\Driver\AttributeDriver; use Doctrine\ORM\Mapping\MappingException as ORMMappingException; use Doctrine\ORM\Mapping\NamingStrategy; use Doctrine\ORM\Tools\DisconnectedClassMetadataFactory; use Doctrine\Persistence\ManagerRegistry; use Doctrine\Persistence\Mapping\AbstractClassMetadataFactory; use Doctrine\Persistence\Mapping\ClassMetadata; use Doctrine\Persistence\Mapping\Driver\MappingDriver; use Doctrine\Persistence\Mapping\Driver\MappingDriverChain; use Doctrine\Persistence\Mapping\MappingException as PersistenceMappingException; use Symfony\Bundle\MakerBundle\Util\ClassNameDetails; use Symfony\Bundle\MakerBundle\Util\PhpCompatUtil; /** * @author Fabien Potencier <fabien@symfony.com> * @author Ryan Weaver <ryan@knpuniversity.com> * @author Sadicov Vladimir <sadikoff@gmail.com> * * @internal */ final class DoctrineHelper { /** * @var string */ private $entityNamespace; private $phpCompatUtil; private $registry; /** * @var array|null */ private $mappingDriversByPrefix; private $attributeMappingSupport; public function __construct(string $entityNamespace, PhpCompatUtil $phpCompatUtil, ManagerRegistry $registry = null, bool $attributeMappingSupport = false, array $annotatedPrefixes = null) { $this->entityNamespace = trim($entityNamespace, '\\'); $this->phpCompatUtil = $phpCompatUtil; $this->registry = $registry; $this->attributeMappingSupport = $attributeMappingSupport; $this->mappingDriversByPrefix = $annotatedPrefixes; } public function getRegistry(): ManagerRegistry { // this should never happen: we will have checked for the // DoctrineBundle dependency before calling this if (null === $this->registry) { throw new \Exception('Somehow the doctrine service is missing. Is DoctrineBundle installed?'); } return $this->registry; } private function isDoctrineInstalled(): bool { return null !== $this->registry; } public function getEntityNamespace(): string { return $this->entityNamespace; } public function doesClassUseDriver(string $className, string $driverClass): bool { try { /** @var EntityManagerInterface $em */ $em = $this->getRegistry()->getManagerForClass($className); } catch (\ReflectionException $exception) { // this exception will be thrown by the registry if the class isn't created yet. // an example case is the "make:entity" command, which needs to know which driver is used for the class to determine // if the class should be generated with attributes or annotations. If this exception is thrown, we will check based on the // namespaces for the given $className and compare it with the doctrine configuration to get the correct MappingDriver. // extract the new class's namespace from the full $className to check the namespace of the new class against the doctrine configuration. $classNameComponents = explode('\\', $className); if (1 < \count($classNameComponents)) { array_pop($classNameComponents); } $classNamespace = implode('\\', $classNameComponents); return $this->isInstanceOf($this->getMappingDriverForNamespace($classNamespace), $driverClass); } if (null === $em) { throw new \InvalidArgumentException(sprintf('Cannot find the entity manager for class "%s"', $className)); } if (null === $this->mappingDriversByPrefix) { // doctrine-bundle <= 2.2 $metadataDriver = $em->getConfiguration()->getMetadataDriverImpl(); if (!$this->isInstanceOf($metadataDriver, MappingDriverChain::class)) { return $this->isInstanceOf($metadataDriver, $driverClass); } foreach ($metadataDriver->getDrivers() as $namespace => $driver) { if (0 === strpos($className, $namespace)) { return $this->isInstanceOf($driver, $driverClass); } } return $this->isInstanceOf($metadataDriver->getDefaultDriver(), $driverClass); } $managerName = array_search($em, $this->getRegistry()->getManagers(), true); foreach ($this->mappingDriversByPrefix[$managerName] as [$prefix, $prefixDriver]) { if (0 === strpos($className, $prefix)) { return $this->isInstanceOf($prefixDriver, $driverClass); } } return false; } public function isClassAnnotated(string $className): bool { return $this->doesClassUseDriver($className, AnnotationDriver::class); } public function doesClassUsesAttributes(string $className): bool { return $this->doesClassUseDriver($className, AttributeDriver::class); } public function isDoctrineSupportingAttributes(): bool { return $this->isDoctrineInstalled() && $this->attributeMappingSupport && $this->phpCompatUtil->canUseAttributes(); } public function getEntitiesForAutocomplete(): array { $entities = []; if ($this->isDoctrineInstalled()) { $allMetadata = $this->getMetadata(); foreach (array_keys($allMetadata) as $classname) { $entityClassDetails = new ClassNameDetails($classname, $this->entityNamespace); $entities[] = $entityClassDetails->getRelativeName(); } } sort($entities); return $entities; } /** * @return array|ClassMetadata */ public function getMetadata(string $classOrNamespace = null, bool $disconnected = false) { // Invalidating the cached AnnotationDriver::$classNames to find new Entity classes foreach ($this->mappingDriversByPrefix ?? [] as $managerName => $prefixes) { foreach ($prefixes as [$prefix, $annotationDriver]) { if (null !== $annotationDriver) { if ($annotationDriver instanceof AnnotationDriver) { $classNames = (new \ReflectionClass(AnnotationDriver::class))->getProperty('classNames'); } else { $classNames = (new \ReflectionClass(AttributeDriver::class))->getProperty('classNames'); } $classNames->setAccessible(true); $classNames->setValue($annotationDriver, null); } } } $metadata = []; /** @var EntityManagerInterface $em */ foreach ($this->getRegistry()->getManagers() as $em) { $cmf = $em->getMetadataFactory(); if ($disconnected) { try { $loaded = $cmf->getAllMetadata(); } catch (ORMMappingException|PersistenceMappingException $e) { $loaded = $this->isInstanceOf($cmf, AbstractClassMetadataFactory::class) ? $cmf->getLoadedMetadata() : []; } $cmf = new DisconnectedClassMetadataFactory(); $cmf->setEntityManager($em); foreach ($loaded as $m) { $cmf->setMetadataFor($m->getName(), $m); } if (null === $this->mappingDriversByPrefix) { // Invalidating the cached AnnotationDriver::$classNames to find new Entity classes $metadataDriver = $em->getConfiguration()->getMetadataDriverImpl(); if ($this->isInstanceOf($metadataDriver, MappingDriverChain::class)) { foreach ($metadataDriver->getDrivers() as $driver) { if ($this->isInstanceOf($driver, AnnotationDriver::class)) { $classNames->setValue($driver, null); } if ($this->isInstanceOf($driver, AttributeDriver::class)) { $classNames->setValue($driver, null); } } } } } foreach ($cmf->getAllMetadata() as $m) { if (null === $classOrNamespace) { $metadata[$m->getName()] = $m; } else { if ($m->getName() === $classOrNamespace) { return $m; } if (0 === strpos($m->getName(), $classOrNamespace)) { $metadata[$m->getName()] = $m; } } } } return $metadata; } public function createDoctrineDetails(string $entityClassName): ?EntityDetails { $metadata = $this->getMetadata($entityClassName); if ($this->isInstanceOf($metadata, ClassMetadata::class)) { return new EntityDetails($metadata); } return null; } public function isClassAMappedEntity(string $className): bool { if (!$this->isDoctrineInstalled()) { return false; } return (bool) $this->getMetadata($className); } private function isInstanceOf($object, string $class): bool { if (!\is_object($object)) { return false; } return $object instanceof $class; } public function getPotentialTableName(string $className): string { $entityManager = $this->getRegistry()->getManager(); if (!$entityManager instanceof EntityManagerInterface) { throw new \RuntimeException('ObjectManager is not an EntityManagerInterface.'); } /** @var NamingStrategy $namingStrategy */ $namingStrategy = $entityManager->getConfiguration()->getNamingStrategy(); return $namingStrategy->classToTableName($className); } public function isKeyword(string $name): bool { /** @var Connection $connection */ $connection = $this->getRegistry()->getConnection(); return $connection->getDatabasePlatform()->getReservedKeywordsList()->isKeyword($name); } /** * this method tries to find the correct MappingDriver for the given namespace/class * To determine which MappingDriver belongs to the class we check the prefixes configured in Doctrine and use the * prefix that has the closest match to the given $namespace. * * this helper function is needed to create entities with the configuration of doctrine if they are not yet been registered * in the ManagerRegistry */ private function getMappingDriverForNamespace(string $namespace): ?MappingDriver { $lowestCharacterDiff = null; $foundDriver = null; foreach ($this->mappingDriversByPrefix ?? [] as $mappings) { foreach ($mappings as [$prefix, $driver]) { $diff = substr_compare($namespace, $prefix, 0); if ($diff >= 0 && (null === $lowestCharacterDiff || $diff < $lowestCharacterDiff)) { $lowestCharacterDiff = $diff; $foundDriver = $driver; } } } return $foundDriver; } } src/Doctrine/EntityClassGenerator.php 0000644 00000011542 15120141001 0013710 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Doctrine; use ApiPlatform\Metadata\ApiResource; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\ORM\Mapping; use Doctrine\Persistence\ManagerRegistry; use Symfony\Bundle\MakerBundle\Generator; use Symfony\Bundle\MakerBundle\Str; use Symfony\Bundle\MakerBundle\Util\ClassNameDetails; use Symfony\Bundle\MakerBundle\Util\UseStatementGenerator; use Symfony\Component\Security\Core\Exception\UnsupportedUserException; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\UX\Turbo\Attribute\Broadcast; /** * @internal */ final class EntityClassGenerator { private $generator; private $doctrineHelper; public function __construct(Generator $generator, DoctrineHelper $doctrineHelper) { $this->generator = $generator; $this->doctrineHelper = $doctrineHelper; } public function generateEntityClass(ClassNameDetails $entityClassDetails, bool $apiResource, bool $withPasswordUpgrade = false, bool $generateRepositoryClass = true, bool $broadcast = false): string { $repoClassDetails = $this->generator->createClassNameDetails( $entityClassDetails->getRelativeName(), 'Repository\\', 'Repository' ); $tableName = $this->doctrineHelper->getPotentialTableName($entityClassDetails->getFullName()); $useStatements = new UseStatementGenerator([ $repoClassDetails->getFullName(), [Mapping::class => 'ORM'], ]); if ($broadcast) { $useStatements->addUseStatement(Broadcast::class); } if ($apiResource) { // @legacy Drop annotation class when annotations are no longer supported. $useStatements->addUseStatement(class_exists(ApiResource::class) ? ApiResource::class : \ApiPlatform\Core\Annotation\ApiResource::class); } $entityPath = $this->generator->generateClass( $entityClassDetails->getFullName(), 'doctrine/Entity.tpl.php', [ 'use_statements' => $useStatements, 'repository_class_name' => $repoClassDetails->getShortName(), 'api_resource' => $apiResource, 'broadcast' => $broadcast, 'should_escape_table_name' => $this->doctrineHelper->isKeyword($tableName), 'table_name' => $tableName, 'doctrine_use_attributes' => $this->doctrineHelper->isDoctrineSupportingAttributes() && $this->doctrineHelper->doesClassUsesAttributes($entityClassDetails->getFullName()), ] ); if ($generateRepositoryClass) { $this->generateRepositoryClass( $repoClassDetails->getFullName(), $entityClassDetails->getFullName(), $withPasswordUpgrade, true ); } return $entityPath; } public function generateRepositoryClass(string $repositoryClass, string $entityClass, bool $withPasswordUpgrade, bool $includeExampleComments = true): void { $shortEntityClass = Str::getShortClassName($entityClass); $entityAlias = strtolower($shortEntityClass[0]); $passwordUserInterfaceName = UserInterface::class; if (interface_exists(PasswordAuthenticatedUserInterface::class)) { $passwordUserInterfaceName = PasswordAuthenticatedUserInterface::class; } $interfaceClassNameDetails = new ClassNameDetails($passwordUserInterfaceName, 'Symfony\Component\Security\Core\User'); $useStatements = new UseStatementGenerator([ $entityClass, ManagerRegistry::class, ServiceEntityRepository::class, ]); if ($withPasswordUpgrade) { $useStatements->addUseStatement([ $interfaceClassNameDetails->getFullName(), PasswordUpgraderInterface::class, UnsupportedUserException::class, ]); } $this->generator->generateClass( $repositoryClass, 'doctrine/Repository.tpl.php', [ 'use_statements' => $useStatements, 'entity_class_name' => $shortEntityClass, 'entity_alias' => $entityAlias, 'with_password_upgrade' => $withPasswordUpgrade, 'password_upgrade_user_interface' => $interfaceClassNameDetails, 'include_example_comments' => $includeExampleComments, ] ); } } src/Doctrine/EntityDetails.php 0000644 00000004141 15120141001 0012356 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Doctrine; use Doctrine\Common\Persistence\Mapping\ClassMetadata as LegacyClassMetadata; use Doctrine\Persistence\Mapping\ClassMetadata; /** * @author Sadicov Vladimir <sadikoff@gmail.com> * * @internal */ final class EntityDetails { private $metadata; /** * @param ClassMetadata|LegacyClassMetadata $metadata */ public function __construct($metadata) { $this->metadata = $metadata; } public function getRepositoryClass(): ?string { return $this->metadata->customRepositoryClassName; } public function getIdentifier() { return $this->metadata->identifier[0]; } public function getDisplayFields(): array { return $this->metadata->fieldMappings; } public function getFormFields(): array { $fields = (array) $this->metadata->fieldNames; // Remove the primary key field if it's not managed manually if (!$this->metadata->isIdentifierNatural()) { $fields = array_diff($fields, $this->metadata->identifier); } $fields = array_values($fields); if (!empty($this->metadata->embeddedClasses)) { foreach (array_keys($this->metadata->embeddedClasses) as $embeddedClassKey) { $fields = array_filter($fields, function ($v) use ($embeddedClassKey) { return 0 !== strpos($v, $embeddedClassKey.'.'); }); } } foreach ($this->metadata->associationMappings as $fieldName => $relation) { if (\Doctrine\ORM\Mapping\ClassMetadata::ONE_TO_MANY !== $relation['type']) { $fields[] = $fieldName; } } $fieldsWithTypes = []; foreach ($fields as $field) { $fieldsWithTypes[$field] = null; } return $fieldsWithTypes; } } src/Doctrine/EntityRegenerator.php 0000644 00000024262 15120141001 0013254 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Doctrine; use Doctrine\Common\Persistence\Mapping\MappingException as LegacyCommonMappingException; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Mapping\MappingException; use Doctrine\Persistence\Mapping\MappingException as PersistenceMappingException; use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException; use Symfony\Bundle\MakerBundle\FileManager; use Symfony\Bundle\MakerBundle\Generator; use Symfony\Bundle\MakerBundle\Util\ClassSourceManipulator; /** * @internal */ final class EntityRegenerator { private $doctrineHelper; private $fileManager; private $generator; private $entityClassGenerator; private $overwrite; public function __construct(DoctrineHelper $doctrineHelper, FileManager $fileManager, Generator $generator, EntityClassGenerator $entityClassGenerator, bool $overwrite) { $this->doctrineHelper = $doctrineHelper; $this->fileManager = $fileManager; $this->generator = $generator; $this->entityClassGenerator = $entityClassGenerator; $this->overwrite = $overwrite; } public function regenerateEntities(string $classOrNamespace): void { try { $metadata = $this->doctrineHelper->getMetadata($classOrNamespace); } catch (MappingException|LegacyCommonMappingException|PersistenceMappingException $mappingException) { $metadata = $this->doctrineHelper->getMetadata($classOrNamespace, true); } if ($metadata instanceof ClassMetadata) { $metadata = [$metadata]; } elseif (class_exists($classOrNamespace)) { throw new RuntimeCommandException(sprintf('Could not find Doctrine metadata for "%s". Is it mapped as an entity?', $classOrNamespace)); } elseif (empty($metadata)) { throw new RuntimeCommandException(sprintf('No entities were found in the "%s" namespace.', $classOrNamespace)); } /** @var ClassSourceManipulator[] $operations */ $operations = []; foreach ($metadata as $classMetadata) { if (!class_exists($classMetadata->name)) { // the class needs to be generated for the first time! $classPath = $this->generateClass($classMetadata); } else { $classPath = $this->getPathOfClass($classMetadata->name); } $mappedFields = $this->getMappedFieldsInEntity($classMetadata); if ($classMetadata->customRepositoryClassName) { $this->generateRepository($classMetadata); } $manipulator = $this->createClassManipulator($classPath); $operations[$classPath] = $manipulator; $embeddedClasses = []; foreach ($classMetadata->embeddedClasses as $fieldName => $mapping) { if (false !== strpos($fieldName, '.')) { continue; } $className = $mapping['class']; $embeddedClasses[$fieldName] = $this->getPathOfClass($className); $operations[$embeddedClasses[$fieldName]] = $this->createClassManipulator($embeddedClasses[$fieldName]); if (!\in_array($fieldName, $mappedFields)) { continue; } $manipulator->addEmbeddedEntity($fieldName, $className); } foreach ($classMetadata->fieldMappings as $fieldName => $mapping) { // skip embedded fields if (false !== strpos($fieldName, '.')) { list($fieldName, $embeddedFiledName) = explode('.', $fieldName); $operations[$embeddedClasses[$fieldName]]->addEntityField($embeddedFiledName, $mapping); continue; } if (!\in_array($fieldName, $mappedFields)) { continue; } $manipulator->addEntityField($fieldName, $mapping); } $getIsNullable = function (array $mapping) { if (!isset($mapping['joinColumns'][0]['nullable'])) { // the default for relationships IS nullable return true; } return $mapping['joinColumns'][0]['nullable']; }; foreach ($classMetadata->associationMappings as $fieldName => $mapping) { if (!\in_array($fieldName, $mappedFields)) { continue; } switch ($mapping['type']) { case ClassMetadata::MANY_TO_ONE: $relation = (new RelationManyToOne()) ->setPropertyName($mapping['fieldName']) ->setIsNullable($getIsNullable($mapping)) ->setTargetClassName($mapping['targetEntity']) ->setTargetPropertyName($mapping['inversedBy']) ->setMapInverseRelation(null !== $mapping['inversedBy']) ; $manipulator->addManyToOneRelation($relation); break; case ClassMetadata::ONE_TO_MANY: $relation = (new RelationOneToMany()) ->setPropertyName($mapping['fieldName']) ->setTargetClassName($mapping['targetEntity']) ->setTargetPropertyName($mapping['mappedBy']) ->setOrphanRemoval($mapping['orphanRemoval']) ; $manipulator->addOneToManyRelation($relation); break; case ClassMetadata::MANY_TO_MANY: $relation = (new RelationManyToMany()) ->setPropertyName($mapping['fieldName']) ->setTargetClassName($mapping['targetEntity']) ->setTargetPropertyName($mapping['mappedBy']) ->setIsOwning($mapping['isOwningSide']) ->setMapInverseRelation($mapping['isOwningSide'] ? (null !== $mapping['inversedBy']) : true) ; $manipulator->addManyToManyRelation($relation); break; case ClassMetadata::ONE_TO_ONE: $relation = (new RelationOneToOne()) ->setPropertyName($mapping['fieldName']) ->setTargetClassName($mapping['targetEntity']) ->setTargetPropertyName($mapping['isOwningSide'] ? $mapping['inversedBy'] : $mapping['mappedBy']) ->setIsOwning($mapping['isOwningSide']) ->setMapInverseRelation($mapping['isOwningSide'] ? (null !== $mapping['inversedBy']) : true) ->setIsNullable($getIsNullable($mapping)) ; $manipulator->addOneToOneRelation($relation); break; default: throw new \Exception('Unknown association type.'); } } } foreach ($operations as $filename => $manipulator) { $this->fileManager->dumpFile( $filename, $manipulator->getSourceCode() ); } } private function generateClass(ClassMetadata $metadata): string { $path = $this->generator->generateClass( $metadata->name, 'Class.tpl.php', [] ); $this->generator->writeChanges(); return $path; } private function createClassManipulator(string $classPath): ClassSourceManipulator { return new ClassSourceManipulator( $this->fileManager->getFileContents($classPath), $this->overwrite, // use annotations // if properties need to be generated then, by definition, // some non-annotation config is being used, and so, the // properties should not have annotations added to them false ); } private function getPathOfClass(string $class): string { return (new \ReflectionClass($class))->getFileName(); } private function generateRepository(ClassMetadata $metadata): void { if (!$metadata->customRepositoryClassName) { return; } if (class_exists($metadata->customRepositoryClassName)) { // repository already exists return; } $this->entityClassGenerator->generateRepositoryClass( $metadata->customRepositoryClassName, $metadata->name, false ); $this->generator->writeChanges(); } private function getMappedFieldsInEntity(ClassMetadata $classMetadata): array { /** @var \ReflectionClass $classReflection */ $classReflection = $classMetadata->reflClass; $targetFields = array_merge( array_keys($classMetadata->fieldMappings), array_keys($classMetadata->associationMappings), array_keys($classMetadata->embeddedClasses) ); if ($classReflection) { // exclude traits $traitProperties = []; foreach ($classReflection->getTraits() as $trait) { foreach ($trait->getProperties() as $property) { $traitProperties[] = $property->getName(); } } $targetFields = array_diff($targetFields, $traitProperties); // exclude inherited properties $targetFields = array_filter($targetFields, function ($field) use ($classReflection) { return $classReflection->hasProperty($field) && $classReflection->getProperty($field)->getDeclaringClass()->getName() == $classReflection->getName(); }); } return $targetFields; } } src/Doctrine/EntityRelation.php 0000644 00000014745 15120141001 0012561 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Doctrine; /** * @internal */ final class EntityRelation { public const MANY_TO_ONE = 'ManyToOne'; public const ONE_TO_MANY = 'OneToMany'; public const MANY_TO_MANY = 'ManyToMany'; public const ONE_TO_ONE = 'OneToOne'; private $type; private $owningClass; private $inverseClass; private $owningProperty; private $inverseProperty; private $isNullable = false; private $isSelfReferencing = false; private $orphanRemoval = false; private $mapInverseRelation = true; public function __construct(string $type, string $owningClass, string $inverseClass) { if (!\in_array($type, self::getValidRelationTypes())) { throw new \Exception(sprintf('Invalid relation type "%s"', $type)); } if (self::ONE_TO_MANY === $type) { throw new \Exception('Use ManyToOne instead of OneToMany'); } $this->type = $type; $this->owningClass = $owningClass; $this->inverseClass = $inverseClass; $this->isSelfReferencing = $owningClass === $inverseClass; } public function setOwningProperty(string $owningProperty): void { $this->owningProperty = $owningProperty; } public function setInverseProperty(string $inverseProperty): void { if (!$this->mapInverseRelation) { throw new \Exception('Cannot call setInverseProperty() when the inverse relation will not be mapped.'); } $this->inverseProperty = $inverseProperty; } public function setIsNullable(bool $isNullable): void { $this->isNullable = $isNullable; } public function setOrphanRemoval(bool $orphanRemoval): void { $this->orphanRemoval = $orphanRemoval; } public static function getValidRelationTypes(): array { return [ self::MANY_TO_ONE, self::ONE_TO_MANY, self::MANY_TO_MANY, self::ONE_TO_ONE, ]; } public function getOwningRelation() { switch ($this->getType()) { case self::MANY_TO_ONE: return (new RelationManyToOne()) ->setPropertyName($this->owningProperty) ->setTargetClassName($this->inverseClass) ->setTargetPropertyName($this->inverseProperty) ->setIsNullable($this->isNullable) ->setIsSelfReferencing($this->isSelfReferencing) ->setMapInverseRelation($this->mapInverseRelation) ; break; case self::MANY_TO_MANY: return (new RelationManyToMany()) ->setPropertyName($this->owningProperty) ->setTargetClassName($this->inverseClass) ->setTargetPropertyName($this->inverseProperty) ->setIsOwning(true)->setMapInverseRelation($this->mapInverseRelation) ->setIsSelfReferencing($this->isSelfReferencing) ; break; case self::ONE_TO_ONE: return (new RelationOneToOne()) ->setPropertyName($this->owningProperty) ->setTargetClassName($this->inverseClass) ->setTargetPropertyName($this->inverseProperty) ->setIsNullable($this->isNullable) ->setIsOwning(true) ->setIsSelfReferencing($this->isSelfReferencing) ->setMapInverseRelation($this->mapInverseRelation) ; break; default: throw new \InvalidArgumentException('Invalid type'); } } public function getInverseRelation() { switch ($this->getType()) { case self::MANY_TO_ONE: return (new RelationOneToMany()) ->setPropertyName($this->inverseProperty) ->setTargetClassName($this->owningClass) ->setTargetPropertyName($this->owningProperty) ->setOrphanRemoval($this->orphanRemoval) ->setIsSelfReferencing($this->isSelfReferencing) ; break; case self::MANY_TO_MANY: return (new RelationManyToMany()) ->setPropertyName($this->inverseProperty) ->setTargetClassName($this->owningClass) ->setTargetPropertyName($this->owningProperty) ->setIsOwning(false) ->setIsSelfReferencing($this->isSelfReferencing) ; break; case self::ONE_TO_ONE: return (new RelationOneToOne()) ->setPropertyName($this->inverseProperty) ->setTargetClassName($this->owningClass) ->setTargetPropertyName($this->owningProperty) ->setIsNullable($this->isNullable) ->setIsOwning(false) ->setIsSelfReferencing($this->isSelfReferencing) ; break; default: throw new \InvalidArgumentException('Invalid type'); } } public function getType(): string { return $this->type; } public function getOwningClass(): string { return $this->owningClass; } public function getInverseClass(): string { return $this->inverseClass; } public function getOwningProperty() { return $this->owningProperty; } public function getInverseProperty(): string { return $this->inverseProperty; } public function isNullable(): bool { return $this->isNullable; } public function isSelfReferencing(): bool { return $this->isSelfReferencing; } public function getMapInverseRelation(): bool { return $this->mapInverseRelation; } public function setMapInverseRelation(bool $mapInverseRelation) { if ($mapInverseRelation && $this->inverseProperty) { throw new \Exception('Cannot set setMapInverseRelation() to true when the inverse relation property is set.'); } $this->mapInverseRelation = $mapInverseRelation; } } src/Doctrine/ORMDependencyBuilder.php 0000644 00000001737 15120141001 0013547 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Doctrine; use Doctrine\Bundle\DoctrineBundle\DoctrineBundle; use Doctrine\ORM\Mapping\Column; use Symfony\Bundle\MakerBundle\DependencyBuilder; /** * @internal */ final class ORMDependencyBuilder { /** * Central method to add dependencies needed for Doctrine ORM. */ public static function buildDependencies(DependencyBuilder $dependencies): void { $classes = [ // guarantee DoctrineBundle DoctrineBundle::class, // guarantee ORM Column::class, ]; foreach ($classes as $class) { $dependencies->addClassDependency( $class, 'orm' ); } } } src/Doctrine/RelationManyToMany.php 0000644 00000002065 15120141001 0013331 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Doctrine; use Symfony\Bundle\MakerBundle\Str; /** * @internal */ final class RelationManyToMany extends BaseCollectionRelation { private $isOwning; public function isOwning(): bool { return $this->isOwning; } public function setIsOwning($isOwning): self { $this->isOwning = $isOwning; return $this; } public function getOrphanRemoval(): bool { return false; } public function getTargetSetterMethodName(): string { return 'add'.Str::asCamelCase(Str::pluralCamelCaseToSingular($this->getTargetPropertyName())); } public function getTargetRemoverMethodName(): string { return 'remove'.Str::asCamelCase(Str::pluralCamelCaseToSingular($this->getTargetPropertyName())); } } src/Doctrine/RelationManyToOne.php 0000644 00000000702 15120141001 0013142 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Doctrine; /** * @internal */ final class RelationManyToOne extends BaseSingleRelation { public function isOwning(): bool { return true; } } src/Doctrine/RelationOneToMany.php 0000644 00000002214 15120141001 0013142 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Doctrine; use Symfony\Bundle\MakerBundle\Str; /** * @internal */ final class RelationOneToMany extends BaseCollectionRelation { private $orphanRemoval; public function getOrphanRemoval(): bool { return $this->orphanRemoval; } public function setOrphanRemoval($orphanRemoval): self { $this->orphanRemoval = $orphanRemoval; return $this; } public function getTargetGetterMethodName(): string { return 'get'.Str::asCamelCase($this->getTargetPropertyName()); } public function getTargetSetterMethodName(): string { return 'set'.Str::asCamelCase($this->getTargetPropertyName()); } public function isOwning(): bool { return false; } public function isMapInverseRelation(): bool { throw new \Exception('OneToMany IS the inverse side!'); } } src/Doctrine/RelationOneToOne.php 0000644 00000001633 15120141001 0012763 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Doctrine; use Symfony\Bundle\MakerBundle\Str; /** * @internal */ final class RelationOneToOne extends BaseSingleRelation { private $isOwning; public function isOwning(): bool { return $this->isOwning; } public function setIsOwning($isOwning): self { $this->isOwning = $isOwning; return $this; } public function getTargetGetterMethodName(): string { return 'get'.Str::asCamelCase($this->getTargetPropertyName()); } public function getTargetSetterMethodName(): string { return 'set'.Str::asCamelCase($this->getTargetPropertyName()); } } src/Event/ConsoleErrorSubscriber.php 0000644 00000003477 15120141001 0013561 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Event; use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException; use Symfony\Component\Console\ConsoleEvents; use Symfony\Component\Console\Event\ConsoleErrorEvent; use Symfony\Component\Console\Event\ConsoleTerminateEvent; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\EventDispatcher\EventSubscriberInterface; /** * Prints certain exceptions in a pretty way and silences normal exception handling. * * @author Ryan Weaver <ryan@knpuniversity.com> */ final class ConsoleErrorSubscriber implements EventSubscriberInterface { private $setExitCode = false; public function onConsoleError(ConsoleErrorEvent $event): void { if (!$event->getError() instanceof RuntimeCommandException) { return; } // prevent any visual logging from appearing $event->stopPropagation(); // prevent the exception from actually being thrown $event->setExitCode(0); $this->setExitCode = true; $io = new SymfonyStyle($event->getInput(), $event->getOutput()); $io->error($event->getError()->getMessage()); } public function onConsoleTerminate(ConsoleTerminateEvent $event): void { if (!$this->setExitCode) { return; } // finally set a non-zero exit code $event->setExitCode(1); } public static function getSubscribedEvents(): array { return [ ConsoleEvents::ERROR => 'onConsoleError', ConsoleEvents::TERMINATE => 'onConsoleTerminate', ]; } } src/Exception/RuntimeCommandException.php 0000644 00000001077 15120141001 0014571 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Exception; use Symfony\Component\Console\Exception\ExceptionInterface; /** * An exception whose output is displayed as a clean error. * * @author Ryan Weaver <ryan@knpuniversity.com> */ final class RuntimeCommandException extends \RuntimeException implements ExceptionInterface { } src/Maker/AbstractMaker.php 0000644 00000002672 15120141001 0011616 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Maker; use Symfony\Bundle\MakerBundle\ConsoleStyle; use Symfony\Bundle\MakerBundle\DependencyBuilder; use Symfony\Bundle\MakerBundle\MakerInterface; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; /** * Convenient abstract class for makers. */ abstract class AbstractMaker implements MakerInterface { public function interact(InputInterface $input, ConsoleStyle $io, Command $command) { } protected function writeSuccessMessage(ConsoleStyle $io) { $io->newLine(); $io->writeln(' <bg=green;fg=white> </>'); $io->writeln(' <bg=green;fg=white> Success! </>'); $io->writeln(' <bg=green;fg=white> </>'); $io->newLine(); } protected function addDependencies(array $dependencies, string $message = null): string { $dependencyBuilder = new DependencyBuilder(); foreach ($dependencies as $class => $name) { $dependencyBuilder->addClassDependency($class, $name); } return $dependencyBuilder->getMissingPackagesMessage( $this->getCommandName(), $message ); } } src/Maker/MakeAuthenticator.php 0000644 00000043017 15120141001 0012501 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Maker; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\MakerBundle\ConsoleStyle; use Symfony\Bundle\MakerBundle\DependencyBuilder; use Symfony\Bundle\MakerBundle\Doctrine\DoctrineHelper; use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException; use Symfony\Bundle\MakerBundle\FileManager; use Symfony\Bundle\MakerBundle\Generator; use Symfony\Bundle\MakerBundle\InputConfiguration; use Symfony\Bundle\MakerBundle\Security\InteractiveSecurityHelper; use Symfony\Bundle\MakerBundle\Security\SecurityConfigUpdater; use Symfony\Bundle\MakerBundle\Security\SecurityControllerBuilder; use Symfony\Bundle\MakerBundle\Str; use Symfony\Bundle\MakerBundle\Util\ClassSourceManipulator; use Symfony\Bundle\MakerBundle\Util\UseStatementGenerator; use Symfony\Bundle\MakerBundle\Util\YamlManipulationFailedException; use Symfony\Bundle\MakerBundle\Util\YamlSourceManipulator; use Symfony\Bundle\MakerBundle\Validator; use Symfony\Bundle\SecurityBundle\SecurityBundle; use Symfony\Bundle\TwigBundle\TwigBundle; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Question\Question; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Guard\AuthenticatorInterface as GuardAuthenticatorInterface; use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator; use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; use Symfony\Component\Security\Http\Authenticator\Passport\Passport; use Symfony\Component\Security\Http\Util\TargetPathTrait; use Symfony\Component\Yaml\Yaml; /** * @author Ryan Weaver <ryan@symfonycasts.com> * @author Jesse Rushlow <jr@rushlow.dev> * * @internal */ final class MakeAuthenticator extends AbstractMaker { private const AUTH_TYPE_EMPTY_AUTHENTICATOR = 'empty-authenticator'; private const AUTH_TYPE_FORM_LOGIN = 'form-login'; private $fileManager; private $configUpdater; private $generator; private $doctrineHelper; private $securityControllerBuilder; public function __construct(FileManager $fileManager, SecurityConfigUpdater $configUpdater, Generator $generator, DoctrineHelper $doctrineHelper, SecurityControllerBuilder $securityControllerBuilder) { $this->fileManager = $fileManager; $this->configUpdater = $configUpdater; $this->generator = $generator; $this->doctrineHelper = $doctrineHelper; $this->securityControllerBuilder = $securityControllerBuilder; } public static function getCommandName(): string { return 'make:auth'; } public static function getCommandDescription(): string { return 'Creates a Guard authenticator of different flavors'; } public function configureCommand(Command $command, InputConfiguration $inputConfig): void { $command ->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeAuth.txt')); } public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void { if (!$this->fileManager->fileExists($path = 'config/packages/security.yaml')) { throw new RuntimeCommandException('The file "config/packages/security.yaml" does not exist. This command requires that file to exist so that it can be updated.'); } $manipulator = new YamlSourceManipulator($this->fileManager->getFileContents($path)); $securityData = $manipulator->getData(); // @legacy - Can be removed when Symfony 5.4 support is dropped if (interface_exists(GuardAuthenticatorInterface::class) && !($securityData['security']['enable_authenticator_manager'] ?? false)) { throw new RuntimeCommandException('MakerBundle only supports the new authenticator based security system. See https://symfony.com/doc/current/security.html'); } // authenticator type $authenticatorTypeValues = [ 'Empty authenticator' => self::AUTH_TYPE_EMPTY_AUTHENTICATOR, 'Login form authenticator' => self::AUTH_TYPE_FORM_LOGIN, ]; $command->addArgument('authenticator-type', InputArgument::REQUIRED); $authenticatorType = $io->choice( 'What style of authentication do you want?', array_keys($authenticatorTypeValues), key($authenticatorTypeValues) ); $input->setArgument( 'authenticator-type', $authenticatorTypeValues[$authenticatorType] ); if (self::AUTH_TYPE_FORM_LOGIN === $input->getArgument('authenticator-type')) { $neededDependencies = [TwigBundle::class => 'twig']; $missingPackagesMessage = $this->addDependencies($neededDependencies, 'Twig must be installed to display the login form.'); if ($missingPackagesMessage) { throw new RuntimeCommandException($missingPackagesMessage); } if (!isset($securityData['security']['providers']) || !$securityData['security']['providers']) { throw new RuntimeCommandException('To generate a form login authentication, you must configure at least one entry under "providers" in "security.yaml".'); } } // authenticator class $command->addArgument('authenticator-class', InputArgument::REQUIRED); $questionAuthenticatorClass = new Question('The class name of the authenticator to create (e.g. <fg=yellow>AppCustomAuthenticator</>)'); $questionAuthenticatorClass->setValidator( function ($answer) { Validator::notBlank($answer); return Validator::classDoesNotExist( $this->generator->createClassNameDetails($answer, 'Security\\', 'Authenticator')->getFullName() ); } ); $input->setArgument('authenticator-class', $io->askQuestion($questionAuthenticatorClass)); $interactiveSecurityHelper = new InteractiveSecurityHelper(); $command->addOption('firewall-name', null, InputOption::VALUE_OPTIONAL); $input->setOption('firewall-name', $firewallName = $interactiveSecurityHelper->guessFirewallName($io, $securityData)); $command->addOption('entry-point', null, InputOption::VALUE_OPTIONAL); if (self::AUTH_TYPE_FORM_LOGIN === $input->getArgument('authenticator-type')) { $command->addArgument('controller-class', InputArgument::REQUIRED); $input->setArgument( 'controller-class', $io->ask( 'Choose a name for the controller class (e.g. <fg=yellow>SecurityController</>)', 'SecurityController', [Validator::class, 'validateClassName'] ) ); $command->addArgument('user-class', InputArgument::REQUIRED); $input->setArgument( 'user-class', $userClass = $interactiveSecurityHelper->guessUserClass($io, $securityData['security']['providers']) ); $command->addArgument('username-field', InputArgument::REQUIRED); $input->setArgument( 'username-field', $interactiveSecurityHelper->guessUserNameField($io, $userClass, $securityData['security']['providers']) ); $command->addArgument('logout-setup', InputArgument::REQUIRED); $input->setArgument( 'logout-setup', $io->confirm( 'Do you want to generate a \'/logout\' URL?', true ) ); } } public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void { $manipulator = new YamlSourceManipulator($this->fileManager->getFileContents('config/packages/security.yaml')); $securityData = $manipulator->getData(); $this->generateAuthenticatorClass( $securityData, $input->getArgument('authenticator-type'), $input->getArgument('authenticator-class'), $input->hasArgument('user-class') ? $input->getArgument('user-class') : null, $input->hasArgument('username-field') ? $input->getArgument('username-field') : null ); // update security.yaml with guard config $securityYamlUpdated = false; $entryPoint = $input->getOption('entry-point'); if (self::AUTH_TYPE_FORM_LOGIN !== $input->getArgument('authenticator-type')) { $entryPoint = false; } try { $newYaml = $this->configUpdater->updateForAuthenticator( $this->fileManager->getFileContents($path = 'config/packages/security.yaml'), $input->getOption('firewall-name'), $entryPoint, $input->getArgument('authenticator-class'), $input->hasArgument('logout-setup') ? $input->getArgument('logout-setup') : false ); $generator->dumpFile($path, $newYaml); $securityYamlUpdated = true; } catch (YamlManipulationFailedException $e) { } if (self::AUTH_TYPE_FORM_LOGIN === $input->getArgument('authenticator-type')) { $this->generateFormLoginFiles( $input->getArgument('controller-class'), $input->getArgument('username-field'), $input->getArgument('logout-setup') ); } $generator->writeChanges(); $this->writeSuccessMessage($io); $io->text( $this->generateNextMessage( $securityYamlUpdated, $input->getArgument('authenticator-type'), $input->getArgument('authenticator-class'), $securityData, $input->hasArgument('user-class') ? $input->getArgument('user-class') : null, $input->hasArgument('logout-setup') ? $input->getArgument('logout-setup') : false ) ); } private function generateAuthenticatorClass(array $securityData, string $authenticatorType, string $authenticatorClass, $userClass, $userNameField): void { $useStatements = new UseStatementGenerator([ Request::class, Response::class, TokenInterface::class, Passport::class, ]); // generate authenticator class if (self::AUTH_TYPE_EMPTY_AUTHENTICATOR === $authenticatorType) { $useStatements->addUseStatement([ AuthenticationException::class, AbstractAuthenticator::class, ]); $this->generator->generateClass( $authenticatorClass, 'authenticator/EmptyAuthenticator.tpl.php', ['use_statements' => $useStatements] ); return; } $useStatements->addUseStatement([ RedirectResponse::class, UrlGeneratorInterface::class, Security::class, AbstractLoginFormAuthenticator::class, CsrfTokenBadge::class, UserBadge::class, PasswordCredentials::class, TargetPathTrait::class, ]); $userClassNameDetails = $this->generator->createClassNameDetails( '\\'.$userClass, 'Entity\\' ); $this->generator->generateClass( $authenticatorClass, 'authenticator/LoginFormAuthenticator.tpl.php', [ 'use_statements' => $useStatements, 'user_fully_qualified_class_name' => trim($userClassNameDetails->getFullName(), '\\'), 'user_class_name' => $userClassNameDetails->getShortName(), 'username_field' => $userNameField, 'username_field_label' => Str::asHumanWords($userNameField), 'username_field_var' => Str::asLowerCamelCase($userNameField), 'user_needs_encoder' => $this->userClassHasEncoder($securityData, $userClass), 'user_is_entity' => $this->doctrineHelper->isClassAMappedEntity($userClass), ] ); } private function generateFormLoginFiles(string $controllerClass, string $userNameField, bool $logoutSetup): void { $controllerClassNameDetails = $this->generator->createClassNameDetails( $controllerClass, 'Controller\\', 'Controller' ); if (!class_exists($controllerClassNameDetails->getFullName())) { $useStatements = new UseStatementGenerator([ AbstractController::class, Route::class, AuthenticationUtils::class, ]); $controllerPath = $this->generator->generateController( $controllerClassNameDetails->getFullName(), 'authenticator/EmptySecurityController.tpl.php', ['use_statements' => $useStatements] ); $controllerSourceCode = $this->generator->getFileContentsForPendingOperation($controllerPath); } else { $controllerPath = $this->fileManager->getRelativePathForFutureClass($controllerClassNameDetails->getFullName()); $controllerSourceCode = $this->fileManager->getFileContents($controllerPath); } if (method_exists($controllerClassNameDetails->getFullName(), 'login')) { throw new RuntimeCommandException(sprintf('Method "login" already exists on class %s', $controllerClassNameDetails->getFullName())); } $manipulator = new ClassSourceManipulator($controllerSourceCode, true); $this->securityControllerBuilder->addLoginMethod($manipulator); if ($logoutSetup) { $this->securityControllerBuilder->addLogoutMethod($manipulator); } $this->generator->dumpFile($controllerPath, $manipulator->getSourceCode()); // create login form template $this->generator->generateTemplate( 'security/login.html.twig', 'authenticator/login_form.tpl.php', [ 'username_field' => $userNameField, 'username_is_email' => false !== stripos($userNameField, 'email'), 'username_label' => ucfirst(Str::asHumanWords($userNameField)), 'logout_setup' => $logoutSetup, ] ); } private function generateNextMessage(bool $securityYamlUpdated, string $authenticatorType, string $authenticatorClass, array $securityData, $userClass, bool $logoutSetup): array { $nextTexts = ['Next:']; $nextTexts[] = '- Customize your new authenticator.'; if (!$securityYamlUpdated) { $yamlExample = $this->configUpdater->updateForAuthenticator( 'security: {}', 'main', null, $authenticatorClass, $logoutSetup ); $nextTexts[] = "- Your <info>security.yaml</info> could not be updated automatically. You'll need to add the following config manually:\n\n".$yamlExample; } if (self::AUTH_TYPE_FORM_LOGIN === $authenticatorType) { $nextTexts[] = sprintf('- Finish the redirect "TODO" in the <info>%s::onAuthenticationSuccess()</info> method.', $authenticatorClass); if (!$this->doctrineHelper->isClassAMappedEntity($userClass)) { $nextTexts[] = sprintf('- Review <info>%s::getUser()</info> to make sure it matches your needs.', $authenticatorClass); } $nextTexts[] = '- Review & adapt the login template: <info>'.$this->fileManager->getPathForTemplate('security/login.html.twig').'</info>.'; } return $nextTexts; } private function userClassHasEncoder(array $securityData, string $userClass): bool { $userNeedsEncoder = false; $hashersData = $securityData['security']['encoders'] ?? $securityData['security']['encoders'] ?? []; foreach ($hashersData as $userClassWithEncoder => $encoder) { if ($userClass === $userClassWithEncoder || is_subclass_of($userClass, $userClassWithEncoder) || class_implements($userClass, $userClassWithEncoder)) { $userNeedsEncoder = true; } } return $userNeedsEncoder; } public function configureDependencies(DependencyBuilder $dependencies, InputInterface $input = null): void { $dependencies->addClassDependency( SecurityBundle::class, 'security' ); // needed to update the YAML files $dependencies->addClassDependency( Yaml::class, 'yaml' ); } } src/Maker/MakeCommand.php 0000644 00000007505 15120141001 0011247 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Maker; use Symfony\Bundle\MakerBundle\ConsoleStyle; use Symfony\Bundle\MakerBundle\DependencyBuilder; use Symfony\Bundle\MakerBundle\Generator; use Symfony\Bundle\MakerBundle\InputConfiguration; use Symfony\Bundle\MakerBundle\Str; use Symfony\Bundle\MakerBundle\Util\PhpCompatUtil; use Symfony\Bundle\MakerBundle\Util\UseStatementGenerator; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\LazyCommand; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; /** * @author Javier Eguiluz <javier.eguiluz@gmail.com> * @author Ryan Weaver <weaverryan@gmail.com> */ final class MakeCommand extends AbstractMaker { private $phpCompatUtil; public function __construct(PhpCompatUtil $phpCompatUtil) { $this->phpCompatUtil = $phpCompatUtil; } public static function getCommandName(): string { return 'make:command'; } public static function getCommandDescription(): string { return 'Creates a new console command class'; } public function configureCommand(Command $command, InputConfiguration $inputConfig): void { $command ->addArgument('name', InputArgument::OPTIONAL, sprintf('Choose a command name (e.g. <fg=yellow>app:%s</>)', Str::asCommand(Str::getRandomTerm()))) ->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeCommand.txt')) ; } public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void { $commandName = trim($input->getArgument('name')); $commandNameHasAppPrefix = 0 === strpos($commandName, 'app:'); $commandClassNameDetails = $generator->createClassNameDetails( $commandNameHasAppPrefix ? substr($commandName, 4) : $commandName, 'Command\\', 'Command', sprintf('The "%s" command name is not valid because it would be implemented by "%s" class, which is not valid as a PHP class name (it must start with a letter or underscore, followed by any number of letters, numbers, or underscores).', $commandName, Str::asClassName($commandName, 'Command')) ); $useStatements = new UseStatementGenerator([ Command::class, InputArgument::class, InputInterface::class, InputOption::class, OutputInterface::class, SymfonyStyle::class, ]); if ($this->phpCompatUtil->canUseAttributes()) { $useStatements->addUseStatement(AsCommand::class); } $generator->generateClass( $commandClassNameDetails->getFullName(), 'command/Command.tpl.php', [ 'use_statements' => $useStatements, 'command_name' => $commandName, 'set_description' => !class_exists(LazyCommand::class), ] ); $generator->writeChanges(); $this->writeSuccessMessage($io); $io->text([ 'Next: open your new command class and customize it!', 'Find the documentation at <fg=yellow>https://symfony.com/doc/current/console.html</>', ]); } public function configureDependencies(DependencyBuilder $dependencies): void { $dependencies->addClassDependency( Command::class, 'console' ); } } src/Maker/MakeController.php 0000644 00000011361 15120141001 0012007 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Maker; use Doctrine\Common\Annotations\Annotation; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\MakerBundle\ConsoleStyle; use Symfony\Bundle\MakerBundle\DependencyBuilder; use Symfony\Bundle\MakerBundle\Generator; use Symfony\Bundle\MakerBundle\InputConfiguration; use Symfony\Bundle\MakerBundle\Str; use Symfony\Bundle\MakerBundle\Util\PhpCompatUtil; use Symfony\Bundle\MakerBundle\Util\UseStatementGenerator; use Symfony\Bundle\TwigBundle\TwigBundle; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\Routing\Annotation\Route; /** * @author Javier Eguiluz <javier.eguiluz@gmail.com> * @author Ryan Weaver <weaverryan@gmail.com> */ final class MakeController extends AbstractMaker { private $phpCompatUtil; public function __construct(PhpCompatUtil $phpCompatUtil = null) { if (null === $phpCompatUtil) { @trigger_error(sprintf('Passing a "%s" instance is mandatory since version 1.42.0', PhpCompatUtil::class), \E_USER_DEPRECATED); } $this->phpCompatUtil = $phpCompatUtil; } public static function getCommandName(): string { return 'make:controller'; } public static function getCommandDescription(): string { return 'Creates a new controller class'; } public function configureCommand(Command $command, InputConfiguration $inputConfig): void { $command ->addArgument('controller-class', InputArgument::OPTIONAL, sprintf('Choose a name for your controller class (e.g. <fg=yellow>%sController</>)', Str::asClassName(Str::getRandomTerm()))) ->addOption('no-template', null, InputOption::VALUE_NONE, 'Use this option to disable template generation') ->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeController.txt')) ; } public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void { $controllerClassNameDetails = $generator->createClassNameDetails( $input->getArgument('controller-class'), 'Controller\\', 'Controller' ); $withTemplate = $this->isTwigInstalled() && !$input->getOption('no-template'); $useStatements = new UseStatementGenerator([ AbstractController::class, $withTemplate ? Response::class : JsonResponse::class, Route::class, ]); $templateName = Str::asFilePath($controllerClassNameDetails->getRelativeNameWithoutSuffix()).'/index.html.twig'; $controllerPath = $generator->generateController( $controllerClassNameDetails->getFullName(), 'controller/Controller.tpl.php', [ 'use_statements' => $useStatements, 'route_path' => Str::asRoutePath($controllerClassNameDetails->getRelativeNameWithoutSuffix()), 'route_name' => Str::asRouteName($controllerClassNameDetails->getRelativeNameWithoutSuffix()), 'with_template' => $withTemplate, 'template_name' => $templateName, ] ); if ($withTemplate) { $generator->generateTemplate( $templateName, 'controller/twig_template.tpl.php', [ 'controller_path' => $controllerPath, 'root_directory' => $generator->getRootDirectory(), 'class_name' => $controllerClassNameDetails->getShortName(), ] ); } $generator->writeChanges(); $this->writeSuccessMessage($io); $io->text('Next: Open your new controller class and add some pages!'); } public function configureDependencies(DependencyBuilder $dependencies): void { // @legacy - Remove method when support for Symfony 5.4 is dropped if (null !== $this->phpCompatUtil && 60000 <= Kernel::VERSION_ID && $this->phpCompatUtil->canUseAttributes()) { return; } $dependencies->addClassDependency( Annotation::class, 'doctrine/annotations' ); } private function isTwigInstalled(): bool { return class_exists(TwigBundle::class); } } src/Maker/MakeCrud.php 0000644 00000031462 15120141001 0010565 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Maker; use Doctrine\Bundle\DoctrineBundle\DoctrineBundle; use Doctrine\Inflector\InflectorFactory; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\KernelBrowser; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Symfony\Bundle\MakerBundle\ConsoleStyle; use Symfony\Bundle\MakerBundle\DependencyBuilder; use Symfony\Bundle\MakerBundle\Doctrine\DoctrineHelper; use Symfony\Bundle\MakerBundle\Generator; use Symfony\Bundle\MakerBundle\InputConfiguration; use Symfony\Bundle\MakerBundle\Renderer\FormTypeRenderer; use Symfony\Bundle\MakerBundle\Str; use Symfony\Bundle\MakerBundle\Util\UseStatementGenerator; use Symfony\Bundle\MakerBundle\Validator; use Symfony\Bundle\TwigBundle\TwigBundle; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Question\Question; use Symfony\Component\Form\AbstractType; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Security\Csrf\CsrfTokenManager; use Symfony\Component\Validator\Validation; /** * @author Sadicov Vladimir <sadikoff@gmail.com> */ final class MakeCrud extends AbstractMaker { private $doctrineHelper; private $formTypeRenderer; private $inflector; private $controllerClassName; private $generateTests = false; public function __construct(DoctrineHelper $doctrineHelper, FormTypeRenderer $formTypeRenderer) { $this->doctrineHelper = $doctrineHelper; $this->formTypeRenderer = $formTypeRenderer; $this->inflector = InflectorFactory::create()->build(); } public static function getCommandName(): string { return 'make:crud'; } public static function getCommandDescription(): string { return 'Creates CRUD for Doctrine entity class'; } public function configureCommand(Command $command, InputConfiguration $inputConfig): void { $command ->addArgument('entity-class', InputArgument::OPTIONAL, sprintf('The class name of the entity to create CRUD (e.g. <fg=yellow>%s</>)', Str::asClassName(Str::getRandomTerm()))) ->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeCrud.txt')) ; $inputConfig->setArgumentAsNonInteractive('entity-class'); } public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void { if (null === $input->getArgument('entity-class')) { $argument = $command->getDefinition()->getArgument('entity-class'); $entities = $this->doctrineHelper->getEntitiesForAutocomplete(); $question = new Question($argument->getDescription()); $question->setAutocompleterValues($entities); $value = $io->askQuestion($question); $input->setArgument('entity-class', $value); } $defaultControllerClass = Str::asClassName(sprintf('%s Controller', $input->getArgument('entity-class'))); $this->controllerClassName = $io->ask( sprintf('Choose a name for your controller class (e.g. <fg=yellow>%s</>)', $defaultControllerClass), $defaultControllerClass ); $this->generateTests = $io->confirm('Do you want to generate tests for the controller?. [Experimental]', false); } public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void { $entityClassDetails = $generator->createClassNameDetails( Validator::entityExists($input->getArgument('entity-class'), $this->doctrineHelper->getEntitiesForAutocomplete()), 'Entity\\' ); $entityDoctrineDetails = $this->doctrineHelper->createDoctrineDetails($entityClassDetails->getFullName()); $repositoryVars = []; $repositoryClassName = EntityManagerInterface::class; if (null !== $entityDoctrineDetails->getRepositoryClass()) { $repositoryClassDetails = $generator->createClassNameDetails( '\\'.$entityDoctrineDetails->getRepositoryClass(), 'Repository\\', 'Repository' ); $repositoryClassName = $repositoryClassDetails->getFullName(); $repositoryVars = [ 'repository_full_class_name' => $repositoryClassName, 'repository_class_name' => $repositoryClassDetails->getShortName(), 'repository_var' => lcfirst($this->inflector->singularize($repositoryClassDetails->getShortName())), ]; } $controllerClassDetails = $generator->createClassNameDetails( $this->controllerClassName, 'Controller\\', 'Controller' ); $iter = 0; do { $formClassDetails = $generator->createClassNameDetails( $entityClassDetails->getRelativeNameWithoutSuffix().($iter ?: '').'Type', 'Form\\', 'Type' ); ++$iter; } while (class_exists($formClassDetails->getFullName())); $entityVarPlural = lcfirst($this->inflector->pluralize($entityClassDetails->getShortName())); $entityVarSingular = lcfirst($this->inflector->singularize($entityClassDetails->getShortName())); $entityTwigVarPlural = Str::asTwigVariable($entityVarPlural); $entityTwigVarSingular = Str::asTwigVariable($entityVarSingular); $routeName = Str::asRouteName($controllerClassDetails->getRelativeNameWithoutSuffix()); $templatesPath = Str::asFilePath($controllerClassDetails->getRelativeNameWithoutSuffix()); $useStatements = new UseStatementGenerator([ $entityClassDetails->getFullName(), $formClassDetails->getFullName(), $repositoryClassName, AbstractController::class, Request::class, Response::class, Route::class, ]); $generator->generateController( $controllerClassDetails->getFullName(), 'crud/controller/Controller.tpl.php', array_merge([ 'use_statements' => $useStatements, 'entity_class_name' => $entityClassDetails->getShortName(), 'form_class_name' => $formClassDetails->getShortName(), 'route_path' => Str::asRoutePath($controllerClassDetails->getRelativeNameWithoutSuffix()), 'route_name' => $routeName, 'templates_path' => $templatesPath, 'entity_var_plural' => $entityVarPlural, 'entity_twig_var_plural' => $entityTwigVarPlural, 'entity_var_singular' => $entityVarSingular, 'entity_twig_var_singular' => $entityTwigVarSingular, 'entity_identifier' => $entityDoctrineDetails->getIdentifier(), 'use_render_form' => method_exists(AbstractController::class, 'renderForm'), ], $repositoryVars ) ); $this->formTypeRenderer->render( $formClassDetails, $entityDoctrineDetails->getFormFields(), $entityClassDetails ); $templates = [ '_delete_form' => [ 'route_name' => $routeName, 'entity_twig_var_singular' => $entityTwigVarSingular, 'entity_identifier' => $entityDoctrineDetails->getIdentifier(), ], '_form' => [], 'edit' => [ 'entity_class_name' => $entityClassDetails->getShortName(), 'entity_twig_var_singular' => $entityTwigVarSingular, 'entity_identifier' => $entityDoctrineDetails->getIdentifier(), 'route_name' => $routeName, 'templates_path' => $templatesPath, ], 'index' => [ 'entity_class_name' => $entityClassDetails->getShortName(), 'entity_twig_var_plural' => $entityTwigVarPlural, 'entity_twig_var_singular' => $entityTwigVarSingular, 'entity_identifier' => $entityDoctrineDetails->getIdentifier(), 'entity_fields' => $entityDoctrineDetails->getDisplayFields(), 'route_name' => $routeName, ], 'new' => [ 'entity_class_name' => $entityClassDetails->getShortName(), 'route_name' => $routeName, 'templates_path' => $templatesPath, ], 'show' => [ 'entity_class_name' => $entityClassDetails->getShortName(), 'entity_twig_var_singular' => $entityTwigVarSingular, 'entity_identifier' => $entityDoctrineDetails->getIdentifier(), 'entity_fields' => $entityDoctrineDetails->getDisplayFields(), 'route_name' => $routeName, 'templates_path' => $templatesPath, ], ]; foreach ($templates as $template => $variables) { $generator->generateTemplate( $templatesPath.'/'.$template.'.html.twig', 'crud/templates/'.$template.'.tpl.php', $variables ); } if ($this->generateTests) { $testClassDetails = $generator->createClassNameDetails( $entityClassDetails->getRelativeNameWithoutSuffix(), 'Test\\Controller\\', 'ControllerTest' ); $useStatements = new UseStatementGenerator([ $entityClassDetails->getFullName(), WebTestCase::class, KernelBrowser::class, $repositoryClassName, ]); $usesEntityManager = EntityManagerInterface::class === $repositoryClassName; if ($usesEntityManager) { $useStatements->addUseStatement(EntityRepository::class); } $generator->generateFile( 'tests/Controller/'.$testClassDetails->getShortName().'.php', $usesEntityManager ? 'crud/test/Test.EntityManager.tpl.php' : 'crud/test/Test.tpl.php', [ 'use_statements' => $useStatements, 'entity_full_class_name' => $entityClassDetails->getFullName(), 'entity_class_name' => $entityClassDetails->getShortName(), 'entity_var_singular' => $entityVarSingular, 'route_path' => Str::asRoutePath($controllerClassDetails->getRelativeNameWithoutSuffix()), 'route_name' => $routeName, 'class_name' => Str::getShortClassName($testClassDetails->getFullName()), 'namespace' => Str::getNamespace($testClassDetails->getFullName()), 'form_fields' => $entityDoctrineDetails->getFormFields(), 'repository_class_name' => $usesEntityManager ? EntityManagerInterface::class : $repositoryVars['repository_class_name'], 'form_field_prefix' => strtolower(Str::asSnakeCase($entityTwigVarSingular)), ] ); if (!class_exists(WebTestCase::class)) { $io->caution('You\'ll need to install the `symfony/test-pack` to execute the tests for your new controller.'); } } $generator->writeChanges(); $this->writeSuccessMessage($io); $io->text(sprintf('Next: Check your new CRUD by going to <fg=yellow>%s/</>', Str::asRoutePath($controllerClassDetails->getRelativeNameWithoutSuffix()))); } public function configureDependencies(DependencyBuilder $dependencies): void { $dependencies->addClassDependency( Route::class, 'router' ); $dependencies->addClassDependency( AbstractType::class, 'form' ); $dependencies->addClassDependency( Validation::class, 'validator' ); $dependencies->addClassDependency( TwigBundle::class, 'twig-bundle' ); $dependencies->addClassDependency( DoctrineBundle::class, 'orm' ); $dependencies->addClassDependency( CsrfTokenManager::class, 'security-csrf' ); $dependencies->addClassDependency( ParamConverter::class, 'annotations' ); } } src/Maker/MakeDockerDatabase.php 0000644 00000017060 15120141001 0012522 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Maker; use Symfony\Bundle\MakerBundle\ConsoleStyle; use Symfony\Bundle\MakerBundle\DependencyBuilder; use Symfony\Bundle\MakerBundle\Docker\DockerDatabaseServices; use Symfony\Bundle\MakerBundle\FileManager; use Symfony\Bundle\MakerBundle\Generator; use Symfony\Bundle\MakerBundle\InputConfiguration; use Symfony\Bundle\MakerBundle\Util\ComposeFileManipulator; use Symfony\Bundle\MakerBundle\Validator; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Yaml\Yaml; /** * @author Jesse Rushlow <jr@rushlow.dev> * * @internal */ final class MakeDockerDatabase extends AbstractMaker { private $fileManager; private $composeFilePath; /** * @var ComposeFileManipulator */ private $composeFileManipulator; /** * @var string type of database selected by the user */ private $databaseChoice; /** * @var string Service identifier to be set in docker-compose.yaml */ private $serviceName = 'database'; /** * @var string Version set in docker-compose.yaml for the service. e.g. latest */ private $serviceVersion = 'latest'; public function __construct(FileManager $fileManager) { $this->fileManager = $fileManager; } public static function getCommandName(): string { return 'make:docker:database'; } public static function getCommandDescription(): string { return 'Adds a database container to your docker-compose.yaml file'; } public function configureCommand(Command $command, InputConfiguration $inputConfig): void { $command ->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeDockerDatabase.txt')) ; } public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void { $io->section('- Docker Compose Setup-'); $this->composeFileManipulator = new ComposeFileManipulator($this->getComposeFileContents($io)); $io->newLine(); $this->databaseChoice = strtolower($io->choice( 'Which database service will you be creating?', ['MySQL', 'MariaDB', 'Postgres'] )); $io->text([sprintf( 'For a list of supported versions, check out https://hub.docker.com/_/%s', $this->databaseChoice )]); $this->serviceVersion = $io->ask('What version would you like to use?', DockerDatabaseServices::getSuggestedServiceVersion($this->databaseChoice)); if ($this->composeFileManipulator->serviceExists($this->serviceName)) { $io->comment(sprintf('A <fg=yellow>"%s"</> service is already defined.', $this->serviceName)); $io->newLine(); $serviceNameMsg[] = 'If you are using the Symfony Binary, it will expose the connection config for'; $serviceNameMsg[] = 'this service as environment variables. The name of the service determines the'; $serviceNameMsg[] = 'name of those environment variables.'; $serviceNameMsg[] = ''; $serviceNameMsg[] = 'For example, if you name the service <fg=yellow>database_alt</>, the binary will expose a'; $serviceNameMsg[] = '<fg=yellow>DATABASE_ALT_URL</> environment variable.'; $io->text($serviceNameMsg); $this->serviceName = $io->ask(sprintf('What name should we call the new %s service? (e.g. <fg=yellow>database</>)', $this->serviceName), null, [Validator::class, 'notBlank']); } $this->checkForPDOSupport($this->databaseChoice, $io); } public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void { $io->newLine(); $service = DockerDatabaseServices::getDatabaseSkeleton($this->databaseChoice, $this->serviceVersion); $this->composeFileManipulator->addDockerService($this->serviceName, $service); $this->composeFileManipulator->exposePorts($this->serviceName, DockerDatabaseServices::getDefaultPorts($this->databaseChoice)); $generator->dumpFile($this->composeFilePath, $this->composeFileManipulator->getDataString()); $generator->writeChanges(); $this->writeSuccessMessage($io); $io->text(sprintf('The new <fg=yellow>"%s"</> service is now ready!', $this->serviceName)); $io->newLine(); $ports = DockerDatabaseServices::getDefaultPorts($this->databaseChoice); $closing[] = 'Next:'; $closing[] = sprintf(' A) Run <fg=yellow>docker-compose up -d %s</> to start your database container', $this->serviceName); $closing[] = ' or <fg=yellow>docker-compose up -d</> to start all of them.'; $closing[] = ''; $closing[] = ' B) If you are using the Symfony Binary, it will detect the new service automatically.'; $closing[] = ' Run <fg=yellow>symfony var:export --multiline</> to see the environment variables the binary is exposing.'; $closing[] = ' These will override any values you have in your .env files.'; $closing[] = ''; $closing[] = ' C) Run <fg=yellow>docker-compose stop</> will stop all the containers in docker-compose.yaml.'; $closing[] = ' <fg=yellow>docker-compose down</> will stop and destroy the containers.'; $closing[] = ''; $closing[] = sprintf( 'Port%s %s will be exposed to %s random port%s on your host machine.', 1 === \count($ports) ? '' : 's', implode(' ', $ports), 1 === \count($ports) ? 'a' : '', 1 === \count($ports) ? '' : 's' ); $io->text($closing); $io->newLine(); } public function configureDependencies(DependencyBuilder $dependencies): void { $dependencies->addClassDependency( Yaml::class, 'yaml' ); } private function checkForPDOSupport(string $databaseType, ConsoleStyle $io): void { $extension = DockerDatabaseServices::getMissingExtensionName($databaseType); if (null !== $extension) { $io->note( sprintf('Cannot find PHP\'s pdo_%s extension. Be sure it\'s installed & enabled to talk to the database.', $extension) ); } } /** * Determines and sets the correct Compose File Path and retrieves its contents * if the file exists else an empty string. */ private function getComposeFileContents(ConsoleStyle $io): string { $this->composeFilePath = sprintf('%s/docker-compose.yaml', $this->fileManager->getRootDirectory()); $composeFileExists = false; $statusMessage = 'Existing docker-compose.yaml not found: a new one will be generated!'; $contents = ''; foreach (['.yml', '.yaml'] as $extension) { $composeFilePath = sprintf('%s/docker-compose%s', $this->fileManager->getRootDirectory(), $extension); if (!$composeFileExists && $this->fileManager->fileExists($composeFilePath)) { $composeFileExists = true; $statusMessage = sprintf('We found your existing docker-compose%s: Let\'s update it!', $extension); $this->composeFilePath = $composeFilePath; $contents = $this->fileManager->getFileContents($composeFilePath); } } $io->text($statusMessage); return $contents; } } src/Maker/MakeEntity.php 0000644 00000115317 15120141001 0011146 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Maker; use ApiPlatform\Core\Annotation\ApiResource; use Doctrine\DBAL\Types\Type; use Symfony\Bundle\MakerBundle\ConsoleStyle; use Symfony\Bundle\MakerBundle\DependencyBuilder; use Symfony\Bundle\MakerBundle\Doctrine\DoctrineHelper; use Symfony\Bundle\MakerBundle\Doctrine\EntityClassGenerator; use Symfony\Bundle\MakerBundle\Doctrine\EntityRegenerator; use Symfony\Bundle\MakerBundle\Doctrine\EntityRelation; use Symfony\Bundle\MakerBundle\Doctrine\ORMDependencyBuilder; use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException; use Symfony\Bundle\MakerBundle\FileManager; use Symfony\Bundle\MakerBundle\Generator; use Symfony\Bundle\MakerBundle\InputAwareMakerInterface; use Symfony\Bundle\MakerBundle\InputConfiguration; use Symfony\Bundle\MakerBundle\Str; use Symfony\Bundle\MakerBundle\Util\ClassDetails; use Symfony\Bundle\MakerBundle\Util\ClassSourceManipulator; use Symfony\Bundle\MakerBundle\Util\PhpCompatUtil; use Symfony\Bundle\MakerBundle\Validator; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Question\Question; use Symfony\UX\Turbo\Attribute\Broadcast; /** * @author Javier Eguiluz <javier.eguiluz@gmail.com> * @author Ryan Weaver <weaverryan@gmail.com> * @author Kévin Dunglas <dunglas@gmail.com> */ final class MakeEntity extends AbstractMaker implements InputAwareMakerInterface { private $fileManager; private $doctrineHelper; private $generator; private $entityClassGenerator; private $phpCompatUtil; public function __construct( FileManager $fileManager, DoctrineHelper $doctrineHelper, string $projectDirectory = null, Generator $generator = null, EntityClassGenerator $entityClassGenerator = null, PhpCompatUtil $phpCompatUtil = null ) { $this->fileManager = $fileManager; $this->doctrineHelper = $doctrineHelper; if (null !== $projectDirectory) { @trigger_error('The $projectDirectory constructor argument is no longer used since 1.41.0', \E_USER_DEPRECATED); } if (null === $generator) { @trigger_error(sprintf('Passing a "%s" instance as 4th argument is mandatory since version 1.5.', Generator::class), \E_USER_DEPRECATED); $this->generator = new Generator($fileManager, 'App\\'); } else { $this->generator = $generator; } if (null === $entityClassGenerator) { @trigger_error(sprintf('Passing a "%s" instance as 5th argument is mandatory since version 1.15.1', EntityClassGenerator::class), \E_USER_DEPRECATED); $this->entityClassGenerator = new EntityClassGenerator($generator, $this->doctrineHelper); } else { $this->entityClassGenerator = $entityClassGenerator; } if (null === $phpCompatUtil) { @trigger_error(sprintf('Passing a "%s" instance as 6th argument is mandatory since version 1.41.0', PhpCompatUtil::class), \E_USER_DEPRECATED); $this->phpCompatUtil = new PhpCompatUtil($this->fileManager); } else { $this->phpCompatUtil = $phpCompatUtil; } } public static function getCommandName(): string { return 'make:entity'; } public static function getCommandDescription(): string { return 'Creates or updates a Doctrine entity class, and optionally an API Platform resource'; } public function configureCommand(Command $command, InputConfiguration $inputConfig): void { $command ->addArgument('name', InputArgument::OPTIONAL, sprintf('Class name of the entity to create or update (e.g. <fg=yellow>%s</>)', Str::asClassName(Str::getRandomTerm()))) ->addOption('api-resource', 'a', InputOption::VALUE_NONE, 'Mark this class as an API Platform resource (expose a CRUD API for it)') ->addOption('broadcast', 'b', InputOption::VALUE_NONE, 'Add the ability to broadcast entity updates using Symfony UX Turbo?') ->addOption('regenerate', null, InputOption::VALUE_NONE, 'Instead of adding new fields, simply generate the methods (e.g. getter/setter) for existing fields') ->addOption('overwrite', null, InputOption::VALUE_NONE, 'Overwrite any existing getter/setter methods') ->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeEntity.txt')) ; $inputConfig->setArgumentAsNonInteractive('name'); } public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void { if ($input->getArgument('name')) { return; } if ($input->getOption('regenerate')) { $io->block([ 'This command will generate any missing methods (e.g. getters & setters) for a class or all classes in a namespace.', 'To overwrite any existing methods, re-run this command with the --overwrite flag', ], null, 'fg=yellow'); $classOrNamespace = $io->ask('Enter a class or namespace to regenerate', $this->getEntityNamespace(), [Validator::class, 'notBlank']); $input->setArgument('name', $classOrNamespace); return; } $argument = $command->getDefinition()->getArgument('name'); $question = $this->createEntityClassQuestion($argument->getDescription()); $entityClassName = $io->askQuestion($question); $input->setArgument('name', $entityClassName); if ( !$input->getOption('api-resource') && class_exists(ApiResource::class) && !class_exists($this->generator->createClassNameDetails($entityClassName, 'Entity\\')->getFullName()) ) { $description = $command->getDefinition()->getOption('api-resource')->getDescription(); $question = new ConfirmationQuestion($description, false); $isApiResource = $io->askQuestion($question); $input->setOption('api-resource', $isApiResource); } if ( !$input->getOption('broadcast') && class_exists(Broadcast::class) && !class_exists($this->generator->createClassNameDetails($entityClassName, 'Entity\\')->getFullName()) ) { $description = $command->getDefinition()->getOption('broadcast')->getDescription(); $question = new ConfirmationQuestion($description, false); $isBroadcast = $io->askQuestion($question); $input->setOption('broadcast', $isBroadcast); } } public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void { $overwrite = $input->getOption('overwrite'); // the regenerate option has entirely custom behavior if ($input->getOption('regenerate')) { $this->regenerateEntities($input->getArgument('name'), $overwrite, $generator); $this->writeSuccessMessage($io); return; } $entityClassDetails = $generator->createClassNameDetails( $input->getArgument('name'), 'Entity\\' ); if (!$this->doctrineHelper->isDoctrineSupportingAttributes() && $this->doctrineHelper->doesClassUsesAttributes($entityClassDetails->getFullName())) { throw new RuntimeCommandException('To use Doctrine entity attributes you\'ll need PHP 8, doctrine/orm 2.9, doctrine/doctrine-bundle 2.4 and symfony/framework-bundle 5.2.'); } $classExists = class_exists($entityClassDetails->getFullName()); if (!$classExists) { $broadcast = $input->getOption('broadcast'); $entityPath = $this->entityClassGenerator->generateEntityClass( $entityClassDetails, $input->getOption('api-resource'), false, true, $broadcast ); if ($broadcast) { $shortName = $entityClassDetails->getShortName(); $generator->generateTemplate( sprintf('broadcast/%s.stream.html.twig', $shortName), 'doctrine/broadcast_twig_template.tpl.php', [ 'class_name' => Str::asSnakeCase($shortName), 'class_name_plural' => Str::asSnakeCase(Str::singularCamelCaseToPluralCamelCase($shortName)), ] ); } $generator->writeChanges(); } if ( !$this->doesEntityUseAnnotationMapping($entityClassDetails->getFullName()) && !$this->doesEntityUseAttributeMapping($entityClassDetails->getFullName()) ) { throw new RuntimeCommandException(sprintf('Only annotation or attribute mapping is supported by make:entity, but the <info>%s</info> class uses a different format. If you would like this command to generate the properties & getter/setter methods, add your mapping configuration, and then re-run this command with the <info>--regenerate</info> flag.', $entityClassDetails->getFullName())); } if ($classExists) { $entityPath = $this->getPathOfClass($entityClassDetails->getFullName()); $io->text([ 'Your entity already exists! So let\'s add some new fields!', ]); } else { $io->text([ '', 'Entity generated! Now let\'s add some fields!', 'You can always add more fields later manually or by re-running this command.', ]); } $currentFields = $this->getPropertyNames($entityClassDetails->getFullName()); $manipulator = $this->createClassManipulator($entityPath, $io, $overwrite, $entityClassDetails->getFullName()); $isFirstField = true; while (true) { $newField = $this->askForNextField($io, $currentFields, $entityClassDetails->getFullName(), $isFirstField); $isFirstField = false; if (null === $newField) { break; } $fileManagerOperations = []; $fileManagerOperations[$entityPath] = $manipulator; if (\is_array($newField)) { $annotationOptions = $newField; unset($annotationOptions['fieldName']); $manipulator->addEntityField($newField['fieldName'], $annotationOptions); $currentFields[] = $newField['fieldName']; } elseif ($newField instanceof EntityRelation) { // both overridden below for OneToMany $newFieldName = $newField->getOwningProperty(); if ($newField->isSelfReferencing()) { $otherManipulatorFilename = $entityPath; $otherManipulator = $manipulator; } else { $otherManipulatorFilename = $this->getPathOfClass($newField->getInverseClass()); $otherManipulator = $this->createClassManipulator($otherManipulatorFilename, $io, $overwrite, $entityClassDetails->getFullName()); } switch ($newField->getType()) { case EntityRelation::MANY_TO_ONE: if ($newField->getOwningClass() === $entityClassDetails->getFullName()) { // THIS class will receive the ManyToOne $manipulator->addManyToOneRelation($newField->getOwningRelation()); if ($newField->getMapInverseRelation()) { $otherManipulator->addOneToManyRelation($newField->getInverseRelation()); } } else { // the new field being added to THIS entity is the inverse $newFieldName = $newField->getInverseProperty(); $otherManipulatorFilename = $this->getPathOfClass($newField->getOwningClass()); $otherManipulator = $this->createClassManipulator($otherManipulatorFilename, $io, $overwrite, $entityClassDetails->getFullName()); // The *other* class will receive the ManyToOne $otherManipulator->addManyToOneRelation($newField->getOwningRelation()); if (!$newField->getMapInverseRelation()) { throw new \Exception('Somehow a OneToMany relationship is being created, but the inverse side will not be mapped?'); } $manipulator->addOneToManyRelation($newField->getInverseRelation()); } break; case EntityRelation::MANY_TO_MANY: $manipulator->addManyToManyRelation($newField->getOwningRelation()); if ($newField->getMapInverseRelation()) { $otherManipulator->addManyToManyRelation($newField->getInverseRelation()); } break; case EntityRelation::ONE_TO_ONE: $manipulator->addOneToOneRelation($newField->getOwningRelation()); if ($newField->getMapInverseRelation()) { $otherManipulator->addOneToOneRelation($newField->getInverseRelation()); } break; default: throw new \Exception('Invalid relation type'); } // save the inverse side if it's being mapped if ($newField->getMapInverseRelation()) { $fileManagerOperations[$otherManipulatorFilename] = $otherManipulator; } $currentFields[] = $newFieldName; } else { throw new \Exception('Invalid value'); } foreach ($fileManagerOperations as $path => $manipulatorOrMessage) { if (\is_string($manipulatorOrMessage)) { $io->comment($manipulatorOrMessage); } else { $this->fileManager->dumpFile($path, $manipulatorOrMessage->getSourceCode()); } } } $this->writeSuccessMessage($io); $io->text([ 'Next: When you\'re ready, create a migration with <info>php bin/console make:migration</info>', '', ]); } public function configureDependencies(DependencyBuilder $dependencies, InputInterface $input = null): void { if (null !== $input && $input->getOption('api-resource')) { $dependencies->addClassDependency( ApiResource::class, 'api' ); } if (null !== $input && $input->getOption('broadcast')) { $dependencies->addClassDependency( Broadcast::class, 'ux-turbo-mercure' ); } ORMDependencyBuilder::buildDependencies($dependencies); } private function askForNextField(ConsoleStyle $io, array $fields, string $entityClass, bool $isFirstField) { $io->writeln(''); if ($isFirstField) { $questionText = 'New property name (press <return> to stop adding fields)'; } else { $questionText = 'Add another property? Enter the property name (or press <return> to stop adding fields)'; } $fieldName = $io->ask($questionText, null, function ($name) use ($fields) { // allow it to be empty if (!$name) { return $name; } if (\in_array($name, $fields)) { throw new \InvalidArgumentException(sprintf('The "%s" property already exists.', $name)); } return Validator::validateDoctrineFieldName($name, $this->doctrineHelper->getRegistry()); }); if (!$fieldName) { return null; } $defaultType = 'string'; // try to guess the type by the field name prefix/suffix // convert to snake case for simplicity $snakeCasedField = Str::asSnakeCase($fieldName); if ('_at' === $suffix = substr($snakeCasedField, -3)) { $defaultType = 'datetime_immutable'; } elseif ('_id' === $suffix) { $defaultType = 'integer'; } elseif (0 === strpos($snakeCasedField, 'is_')) { $defaultType = 'boolean'; } elseif (0 === strpos($snakeCasedField, 'has_')) { $defaultType = 'boolean'; } elseif ('uuid' === $snakeCasedField) { $defaultType = 'uuid'; } elseif ('guid' === $snakeCasedField) { $defaultType = 'guid'; } $type = null; $types = $this->getTypesMap(); $allValidTypes = array_merge( array_keys($types), EntityRelation::getValidRelationTypes(), ['relation'] ); while (null === $type) { $question = new Question('Field type (enter <comment>?</comment> to see all types)', $defaultType); $question->setAutocompleterValues($allValidTypes); $type = $io->askQuestion($question); if ('?' === $type) { $this->printAvailableTypes($io); $io->writeln(''); $type = null; } elseif (!\in_array($type, $allValidTypes)) { $this->printAvailableTypes($io); $io->error(sprintf('Invalid type "%s".', $type)); $io->writeln(''); $type = null; } } if ('relation' === $type || \in_array($type, EntityRelation::getValidRelationTypes())) { return $this->askRelationDetails($io, $entityClass, $type, $fieldName); } // this is a normal field $data = ['fieldName' => $fieldName, 'type' => $type]; if ('string' === $type) { // default to 255, avoid the question $data['length'] = $io->ask('Field length', 255, [Validator::class, 'validateLength']); } elseif ('decimal' === $type) { // 10 is the default value given in \Doctrine\DBAL\Schema\Column::$_precision $data['precision'] = $io->ask('Precision (total number of digits stored: 100.00 would be 5)', 10, [Validator::class, 'validatePrecision']); // 0 is the default value given in \Doctrine\DBAL\Schema\Column::$_scale $data['scale'] = $io->ask('Scale (number of decimals to store: 100.00 would be 2)', 0, [Validator::class, 'validateScale']); } if ($io->confirm('Can this field be null in the database (nullable)', false)) { $data['nullable'] = true; } return $data; } private function printAvailableTypes(ConsoleStyle $io): void { $allTypes = $this->getTypesMap(); if ('Hyper' === getenv('TERM_PROGRAM')) { $wizard = 'wizard 🧙'; } else { $wizard = '\\' === \DIRECTORY_SEPARATOR ? 'wizard' : 'wizard 🧙'; } $typesTable = [ 'main' => [ 'string' => [], 'text' => [], 'boolean' => [], 'integer' => ['smallint', 'bigint'], 'float' => [], ], 'relation' => [ 'relation' => 'a '.$wizard.' will help you build the relation', EntityRelation::MANY_TO_ONE => [], EntityRelation::ONE_TO_MANY => [], EntityRelation::MANY_TO_MANY => [], EntityRelation::ONE_TO_ONE => [], ], 'array_object' => [ 'array' => ['simple_array'], 'json' => [], 'object' => [], 'binary' => [], 'blob' => [], ], 'date_time' => [ 'datetime' => ['datetime_immutable'], 'datetimetz' => ['datetimetz_immutable'], 'date' => ['date_immutable'], 'time' => ['time_immutable'], 'dateinterval' => [], ], ]; $printSection = function (array $sectionTypes) use ($io, &$allTypes) { foreach ($sectionTypes as $mainType => $subTypes) { unset($allTypes[$mainType]); $line = sprintf(' * <comment>%s</comment>', $mainType); if (\is_string($subTypes) && $subTypes) { $line .= sprintf(' (%s)', $subTypes); } elseif (\is_array($subTypes) && !empty($subTypes)) { $line .= sprintf(' (or %s)', implode(', ', array_map(function ($subType) { return sprintf('<comment>%s</comment>', $subType); }, $subTypes))); foreach ($subTypes as $subType) { unset($allTypes[$subType]); } } $io->writeln($line); } $io->writeln(''); }; $io->writeln('<info>Main types</info>'); $printSection($typesTable['main']); $io->writeln('<info>Relationships / Associations</info>'); $printSection($typesTable['relation']); $io->writeln('<info>Array/Object Types</info>'); $printSection($typesTable['array_object']); $io->writeln('<info>Date/Time Types</info>'); $printSection($typesTable['date_time']); $io->writeln('<info>Other Types</info>'); // empty the values $allTypes = array_map(function () { return []; }, $allTypes); $printSection($allTypes); } private function createEntityClassQuestion(string $questionText): Question { $question = new Question($questionText); $question->setValidator([Validator::class, 'notBlank']); $question->setAutocompleterValues($this->doctrineHelper->getEntitiesForAutocomplete()); return $question; } private function askRelationDetails(ConsoleStyle $io, string $generatedEntityClass, string $type, string $newFieldName) { // ask the targetEntity $targetEntityClass = null; while (null === $targetEntityClass) { $question = $this->createEntityClassQuestion('What class should this entity be related to?'); $answeredEntityClass = $io->askQuestion($question); // find the correct class name - but give priority over looking // in the Entity namespace versus just checking the full class // name to avoid issues with classes like "Directory" that exist // in PHP's core. if (class_exists($this->getEntityNamespace().'\\'.$answeredEntityClass)) { $targetEntityClass = $this->getEntityNamespace().'\\'.$answeredEntityClass; } elseif (class_exists($answeredEntityClass)) { $targetEntityClass = $answeredEntityClass; } else { $io->error(sprintf('Unknown class "%s"', $answeredEntityClass)); continue; } } // help the user select the type if ('relation' === $type) { $type = $this->askRelationType($io, $generatedEntityClass, $targetEntityClass); } $askFieldName = function (string $targetClass, string $defaultValue) use ($io) { return $io->ask( sprintf('New field name inside %s', Str::getShortClassName($targetClass)), $defaultValue, function ($name) use ($targetClass) { // it's still *possible* to create duplicate properties - by // trying to generate the same property 2 times during the // same make:entity run. property_exists() only knows about // properties that *originally* existed on this class. if (property_exists($targetClass, $name)) { throw new \InvalidArgumentException(sprintf('The "%s" class already has a "%s" property.', $targetClass, $name)); } return Validator::validateDoctrineFieldName($name, $this->doctrineHelper->getRegistry()); } ); }; $askIsNullable = function (string $propertyName, string $targetClass) use ($io) { return $io->confirm(sprintf( 'Is the <comment>%s</comment>.<comment>%s</comment> property allowed to be null (nullable)?', Str::getShortClassName($targetClass), $propertyName )); }; $askOrphanRemoval = function (string $owningClass, string $inverseClass) use ($io) { $io->text([ 'Do you want to activate <comment>orphanRemoval</comment> on your relationship?', sprintf( 'A <comment>%s</comment> is "orphaned" when it is removed from its related <comment>%s</comment>.', Str::getShortClassName($owningClass), Str::getShortClassName($inverseClass) ), sprintf( 'e.g. <comment>$%s->remove%s($%s)</comment>', Str::asLowerCamelCase(Str::getShortClassName($inverseClass)), Str::asCamelCase(Str::getShortClassName($owningClass)), Str::asLowerCamelCase(Str::getShortClassName($owningClass)) ), '', sprintf( 'NOTE: If a <comment>%s</comment> may *change* from one <comment>%s</comment> to another, answer "no".', Str::getShortClassName($owningClass), Str::getShortClassName($inverseClass) ), ]); return $io->confirm(sprintf('Do you want to automatically delete orphaned <comment>%s</comment> objects (orphanRemoval)?', $owningClass), false); }; $askInverseSide = function (EntityRelation $relation) use ($io) { if ($this->isClassInVendor($relation->getInverseClass())) { $relation->setMapInverseRelation(false); return; } // recommend an inverse side, except for OneToOne, where it's inefficient $recommendMappingInverse = EntityRelation::ONE_TO_ONE !== $relation->getType(); $getterMethodName = 'get'.Str::asCamelCase(Str::getShortClassName($relation->getOwningClass())); if (EntityRelation::ONE_TO_ONE !== $relation->getType()) { // pluralize! $getterMethodName = Str::singularCamelCaseToPluralCamelCase($getterMethodName); } $mapInverse = $io->confirm( sprintf( 'Do you want to add a new property to <comment>%s</comment> so that you can access/update <comment>%s</comment> objects from it - e.g. <comment>$%s->%s()</comment>?', Str::getShortClassName($relation->getInverseClass()), Str::getShortClassName($relation->getOwningClass()), Str::asLowerCamelCase(Str::getShortClassName($relation->getInverseClass())), $getterMethodName ), $recommendMappingInverse ); $relation->setMapInverseRelation($mapInverse); }; switch ($type) { case EntityRelation::MANY_TO_ONE: $relation = new EntityRelation( EntityRelation::MANY_TO_ONE, $generatedEntityClass, $targetEntityClass ); $relation->setOwningProperty($newFieldName); $relation->setIsNullable($askIsNullable( $relation->getOwningProperty(), $relation->getOwningClass() )); $askInverseSide($relation); if ($relation->getMapInverseRelation()) { $io->comment(sprintf( 'A new property will also be added to the <comment>%s</comment> class so that you can access the related <comment>%s</comment> objects from it.', Str::getShortClassName($relation->getInverseClass()), Str::getShortClassName($relation->getOwningClass()) )); $relation->setInverseProperty($askFieldName( $relation->getInverseClass(), Str::singularCamelCaseToPluralCamelCase(Str::getShortClassName($relation->getOwningClass())) )); // orphan removal only applies if the inverse relation is set if (!$relation->isNullable()) { $relation->setOrphanRemoval($askOrphanRemoval( $relation->getOwningClass(), $relation->getInverseClass() )); } } break; case EntityRelation::ONE_TO_MANY: // we *actually* create a ManyToOne, but populate it differently $relation = new EntityRelation( EntityRelation::MANY_TO_ONE, $targetEntityClass, $generatedEntityClass ); $relation->setInverseProperty($newFieldName); $io->comment(sprintf( 'A new property will also be added to the <comment>%s</comment> class so that you can access and set the related <comment>%s</comment> object from it.', Str::getShortClassName($relation->getOwningClass()), Str::getShortClassName($relation->getInverseClass()) )); $relation->setOwningProperty($askFieldName( $relation->getOwningClass(), Str::asLowerCamelCase(Str::getShortClassName($relation->getInverseClass())) )); $relation->setIsNullable($askIsNullable( $relation->getOwningProperty(), $relation->getOwningClass() )); if (!$relation->isNullable()) { $relation->setOrphanRemoval($askOrphanRemoval( $relation->getOwningClass(), $relation->getInverseClass() )); } break; case EntityRelation::MANY_TO_MANY: $relation = new EntityRelation( EntityRelation::MANY_TO_MANY, $generatedEntityClass, $targetEntityClass ); $relation->setOwningProperty($newFieldName); $askInverseSide($relation); if ($relation->getMapInverseRelation()) { $io->comment(sprintf( 'A new property will also be added to the <comment>%s</comment> class so that you can access the related <comment>%s</comment> objects from it.', Str::getShortClassName($relation->getInverseClass()), Str::getShortClassName($relation->getOwningClass()) )); $relation->setInverseProperty($askFieldName( $relation->getInverseClass(), Str::singularCamelCaseToPluralCamelCase(Str::getShortClassName($relation->getOwningClass())) )); } break; case EntityRelation::ONE_TO_ONE: $relation = new EntityRelation( EntityRelation::ONE_TO_ONE, $generatedEntityClass, $targetEntityClass ); $relation->setOwningProperty($newFieldName); $relation->setIsNullable($askIsNullable( $relation->getOwningProperty(), $relation->getOwningClass() )); $askInverseSide($relation); if ($relation->getMapInverseRelation()) { $io->comment(sprintf( 'A new property will also be added to the <comment>%s</comment> class so that you can access the related <comment>%s</comment> object from it.', Str::getShortClassName($relation->getInverseClass()), Str::getShortClassName($relation->getOwningClass()) )); $relation->setInverseProperty($askFieldName( $relation->getInverseClass(), Str::asLowerCamelCase(Str::getShortClassName($relation->getOwningClass())) )); } break; default: throw new \InvalidArgumentException('Invalid type: '.$type); } return $relation; } private function askRelationType(ConsoleStyle $io, string $entityClass, string $targetEntityClass) { $io->writeln('What type of relationship is this?'); $originalEntityShort = Str::getShortClassName($entityClass); $targetEntityShort = Str::getShortClassName($targetEntityClass); $rows = []; $rows[] = [ EntityRelation::MANY_TO_ONE, sprintf("Each <comment>%s</comment> relates to (has) <info>one</info> <comment>%s</comment>.\nEach <comment>%s</comment> can relate to (can have) <info>many</info> <comment>%s</comment> objects", $originalEntityShort, $targetEntityShort, $targetEntityShort, $originalEntityShort), ]; $rows[] = ['', '']; $rows[] = [ EntityRelation::ONE_TO_MANY, sprintf("Each <comment>%s</comment> can relate to (can have) <info>many</info> <comment>%s</comment> objects.\nEach <comment>%s</comment> relates to (has) <info>one</info> <comment>%s</comment>", $originalEntityShort, $targetEntityShort, $targetEntityShort, $originalEntityShort), ]; $rows[] = ['', '']; $rows[] = [ EntityRelation::MANY_TO_MANY, sprintf("Each <comment>%s</comment> can relate to (can have) <info>many</info> <comment>%s</comment> objects.\nEach <comment>%s</comment> can also relate to (can also have) <info>many</info> <comment>%s</comment> objects", $originalEntityShort, $targetEntityShort, $targetEntityShort, $originalEntityShort), ]; $rows[] = ['', '']; $rows[] = [ EntityRelation::ONE_TO_ONE, sprintf("Each <comment>%s</comment> relates to (has) exactly <info>one</info> <comment>%s</comment>.\nEach <comment>%s</comment> also relates to (has) exactly <info>one</info> <comment>%s</comment>.", $originalEntityShort, $targetEntityShort, $targetEntityShort, $originalEntityShort), ]; $io->table([ 'Type', 'Description', ], $rows); $question = new Question(sprintf( 'Relation type? [%s]', implode(', ', EntityRelation::getValidRelationTypes()) )); $question->setAutocompleterValues(EntityRelation::getValidRelationTypes()); $question->setValidator(function ($type) { if (!\in_array($type, EntityRelation::getValidRelationTypes())) { throw new \InvalidArgumentException(sprintf('Invalid type: use one of: %s', implode(', ', EntityRelation::getValidRelationTypes()))); } return $type; }); return $io->askQuestion($question); } private function createClassManipulator(string $path, ConsoleStyle $io, bool $overwrite, string $className): ClassSourceManipulator { $useAttributes = $this->doctrineHelper->doesClassUsesAttributes($className) && $this->doctrineHelper->isDoctrineSupportingAttributes(); $useAnnotations = $this->doctrineHelper->isClassAnnotated($className) || !$useAttributes; $manipulator = new ClassSourceManipulator($this->fileManager->getFileContents($path), $overwrite, $useAnnotations, true, $useAttributes); $manipulator->setIo($io); return $manipulator; } private function getPathOfClass(string $class): string { return (new ClassDetails($class))->getPath(); } private function isClassInVendor(string $class): bool { $path = $this->getPathOfClass($class); return $this->fileManager->isPathInVendor($path); } private function regenerateEntities(string $classOrNamespace, bool $overwrite, Generator $generator): void { $regenerator = new EntityRegenerator($this->doctrineHelper, $this->fileManager, $generator, $this->entityClassGenerator, $overwrite); $regenerator->regenerateEntities($classOrNamespace); } private function getPropertyNames(string $class): array { if (!class_exists($class)) { return []; } $reflClass = new \ReflectionClass($class); return array_map(function (\ReflectionProperty $prop) { return $prop->getName(); }, $reflClass->getProperties()); } /** @legacy Drop when Annotations are no longer supported */ private function doesEntityUseAnnotationMapping(string $className): bool { if (!class_exists($className)) { $otherClassMetadatas = $this->doctrineHelper->getMetadata(Str::getNamespace($className).'\\', true); // if we have no metadata, we should assume this is the first class being mapped if (empty($otherClassMetadatas)) { return false; } $className = reset($otherClassMetadatas)->getName(); } return $this->doctrineHelper->isClassAnnotated($className); } /** @legacy Drop when Annotations are no longer supported */ private function doesEntityUseAttributeMapping(string $className): bool { if (!$this->phpCompatUtil->canUseAttributes()) { return false; } if (!class_exists($className)) { $otherClassMetadatas = $this->doctrineHelper->getMetadata(Str::getNamespace($className).'\\', true); // if we have no metadata, we should assume this is the first class being mapped if (empty($otherClassMetadatas)) { return false; } $className = reset($otherClassMetadatas)->getName(); } return $this->doctrineHelper->doesClassUsesAttributes($className); } private function getEntityNamespace(): string { return $this->doctrineHelper->getEntityNamespace(); } private function getTypesMap(): array { $types = Type::getTypesMap(); // remove deprecated json_array if it exists if (\defined(sprintf('%s::JSON_ARRAY', Type::class))) { unset($types[Type::JSON_ARRAY]); } return $types; } } src/Maker/MakeFixtures.php 0000644 00000005526 15120141001 0011503 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Maker; use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Common\Persistence\ObjectManager as LegacyObjectManager; use Doctrine\ORM\Mapping\Column; use Doctrine\Persistence\ObjectManager; use Symfony\Bundle\MakerBundle\ConsoleStyle; use Symfony\Bundle\MakerBundle\DependencyBuilder; use Symfony\Bundle\MakerBundle\Generator; use Symfony\Bundle\MakerBundle\InputConfiguration; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; /** * @author Javier Eguiluz <javier.eguiluz@gmail.com> * @author Ryan Weaver <weaverryan@gmail.com> */ final class MakeFixtures extends AbstractMaker { public static function getCommandName(): string { return 'make:fixtures'; } public static function getCommandDescription(): string { return 'Creates a new class to load Doctrine fixtures'; } public function configureCommand(Command $command, InputConfiguration $inputConf) { $command ->addArgument('fixtures-class', InputArgument::OPTIONAL, 'The class name of the fixtures to create (e.g. <fg=yellow>AppFixtures</>)') ->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeFixture.txt')) ; } public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator) { $fixturesClassNameDetails = $generator->createClassNameDetails( $input->getArgument('fixtures-class'), 'DataFixtures\\' ); $generator->generateClass( $fixturesClassNameDetails->getFullName(), 'doctrine/Fixtures.tpl.php', [ 'object_manager_class' => interface_exists(ObjectManager::class) ? ObjectManager::class : LegacyObjectManager::class, ] ); $generator->writeChanges(); $this->writeSuccessMessage($io); $io->text([ 'Next: Open your new fixtures class and start customizing it.', sprintf('Load your fixtures by running: <comment>php %s doctrine:fixtures:load</comment>', $_SERVER['PHP_SELF']), 'Docs: <fg=yellow>https://symfony.com/doc/current/bundles/DoctrineFixturesBundle/index.html</>', ]); } public function configureDependencies(DependencyBuilder $dependencies) { $dependencies->addClassDependency( Column::class, 'doctrine' ); $dependencies->addClassDependency( Fixture::class, 'orm-fixtures', true, true ); } } src/Maker/MakeForm.php 0000644 00000011754 15120141001 0010575 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Maker; use Doctrine\Bundle\DoctrineBundle\DoctrineBundle; use Symfony\Bundle\MakerBundle\ConsoleStyle; use Symfony\Bundle\MakerBundle\DependencyBuilder; use Symfony\Bundle\MakerBundle\Doctrine\DoctrineHelper; use Symfony\Bundle\MakerBundle\Generator; use Symfony\Bundle\MakerBundle\InputConfiguration; use Symfony\Bundle\MakerBundle\Renderer\FormTypeRenderer; use Symfony\Bundle\MakerBundle\Str; use Symfony\Bundle\MakerBundle\Util\ClassDetails; use Symfony\Bundle\MakerBundle\Validator; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Question\Question; use Symfony\Component\Form\AbstractType; use Symfony\Component\Validator\Validation; /** * @author Javier Eguiluz <javier.eguiluz@gmail.com> * @author Ryan Weaver <weaverryan@gmail.com> */ final class MakeForm extends AbstractMaker { private $entityHelper; private $formTypeRenderer; public function __construct(DoctrineHelper $entityHelper, FormTypeRenderer $formTypeRenderer) { $this->entityHelper = $entityHelper; $this->formTypeRenderer = $formTypeRenderer; } public static function getCommandName(): string { return 'make:form'; } public static function getCommandDescription(): string { return 'Creates a new form class'; } public function configureCommand(Command $command, InputConfiguration $inputConfig): void { $command ->addArgument('name', InputArgument::OPTIONAL, sprintf('The name of the form class (e.g. <fg=yellow>%sType</>)', Str::asClassName(Str::getRandomTerm()))) ->addArgument('bound-class', InputArgument::OPTIONAL, 'The name of Entity or fully qualified model class name that the new form will be bound to (empty for none)') ->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeForm.txt')) ; $inputConfig->setArgumentAsNonInteractive('bound-class'); } public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void { if (null === $input->getArgument('bound-class')) { $argument = $command->getDefinition()->getArgument('bound-class'); $entities = $this->entityHelper->getEntitiesForAutocomplete(); $question = new Question($argument->getDescription()); $question->setValidator(function ($answer) use ($entities) {return Validator::existsOrNull($answer, $entities); }); $question->setAutocompleterValues($entities); $question->setMaxAttempts(3); $input->setArgument('bound-class', $io->askQuestion($question)); } } public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void { $formClassNameDetails = $generator->createClassNameDetails( $input->getArgument('name'), 'Form\\', 'Type' ); $formFields = ['field_name' => null]; $boundClass = $input->getArgument('bound-class'); $boundClassDetails = null; if (null !== $boundClass) { $boundClassDetails = $generator->createClassNameDetails( $boundClass, 'Entity\\' ); $doctrineEntityDetails = $this->entityHelper->createDoctrineDetails($boundClassDetails->getFullName()); if (null !== $doctrineEntityDetails) { $formFields = $doctrineEntityDetails->getFormFields(); } else { $classDetails = new ClassDetails($boundClassDetails->getFullName()); $formFields = $classDetails->getFormFields(); } } $this->formTypeRenderer->render( $formClassNameDetails, $formFields, $boundClassDetails ); $generator->writeChanges(); $this->writeSuccessMessage($io); $io->text([ 'Next: Add fields to your form and start using it.', 'Find the documentation at <fg=yellow>https://symfony.com/doc/current/forms.html</>', ]); } public function configureDependencies(DependencyBuilder $dependencies): void { $dependencies->addClassDependency( AbstractType::class, // technically only form is needed, but the user will *probably* also want validation 'form' ); $dependencies->addClassDependency( Validation::class, 'validator', // add as an optional dependency: the user *probably* wants validation false ); $dependencies->addClassDependency( DoctrineBundle::class, 'orm', false ); } } src/Maker/MakeFunctionalTest.php 0000644 00000006762 15120141001 0012637 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Maker; use Symfony\Bundle\FrameworkBundle\Test\WebTestAssertionsTrait; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Symfony\Bundle\MakerBundle\ConsoleStyle; use Symfony\Bundle\MakerBundle\DependencyBuilder; use Symfony\Bundle\MakerBundle\Generator; use Symfony\Bundle\MakerBundle\InputConfiguration; use Symfony\Bundle\MakerBundle\Util\UseStatementGenerator; use Symfony\Component\BrowserKit\History; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\CssSelector\CssSelectorConverter; use Symfony\Component\Panther\PantherTestCase; use Symfony\Component\Panther\PantherTestCaseTrait; trigger_deprecation('symfony/maker-bundle', '1.29', 'The "%s" class is deprecated, use "%s" instead.', MakeFunctionalTest::class, MakeTest::class); /** * @deprecated since MakerBundle 1.29, use Symfony\Bundle\MakerBundle\Maker\MakeTest instead. * * @author Javier Eguiluz <javier.eguiluz@gmail.com> * @author Ryan Weaver <weaverryan@gmail.com> */ class MakeFunctionalTest extends AbstractMaker { public static function getCommandName(): string { return 'make:functional-test'; } public static function getCommandDescription(): string { return 'Creates a new functional test class'; } public function configureCommand(Command $command, InputConfiguration $inputConfig): void { $command ->addArgument('name', InputArgument::OPTIONAL, 'The name of the functional test class (e.g. <fg=yellow>DefaultControllerTest</>)') ->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeFunctionalTest.txt')) ; } public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void { $testClassNameDetails = $generator->createClassNameDetails( $input->getArgument('name'), 'Tests\\', 'Test' ); $pantherAvailable = trait_exists(PantherTestCaseTrait::class); $useStatements = new UseStatementGenerator([ ($pantherAvailable ? PantherTestCase::class : WebTestCase::class), ]); $generator->generateClass( $testClassNameDetails->getFullName(), 'test/Functional.tpl.php', [ 'use_statements' => $useStatements, 'web_assertions_are_available' => trait_exists(WebTestAssertionsTrait::class), 'panther_is_available' => $pantherAvailable, ] ); $generator->writeChanges(); $this->writeSuccessMessage($io); $io->text([ 'Next: Open your new test class and start customizing it.', 'Find the documentation at <fg=yellow>https://symfony.com/doc/current/testing.html#functional-tests</>', ]); } public function configureDependencies(DependencyBuilder $dependencies): void { $dependencies->addClassDependency( History::class, 'browser-kit', true, true ); $dependencies->addClassDependency( CssSelectorConverter::class, 'css-selector', true, true ); } } src/Maker/MakeMessage.php 0000644 00000012354 15120141001 0011253 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Maker; use Symfony\Bundle\MakerBundle\ConsoleStyle; use Symfony\Bundle\MakerBundle\DependencyBuilder; use Symfony\Bundle\MakerBundle\FileManager; use Symfony\Bundle\MakerBundle\Generator; use Symfony\Bundle\MakerBundle\InputConfiguration; use Symfony\Bundle\MakerBundle\Util\UseStatementGenerator; use Symfony\Bundle\MakerBundle\Util\YamlSourceManipulator; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Messenger\Handler\MessageHandlerInterface; use Symfony\Component\Messenger\MessageBusInterface; /** * @author Ryan Weaver <ryan@symfonycasts.com> * @author Nicolas Philippe <nikophil@gmail.com> * * @internal */ final class MakeMessage extends AbstractMaker { private $fileManager; public function __construct(FileManager $fileManager) { $this->fileManager = $fileManager; } public static function getCommandName(): string { return 'make:message'; } public static function getCommandDescription(): string { return 'Creates a new message and handler'; } public function configureCommand(Command $command, InputConfiguration $inputConfig): void { $command ->addArgument('name', InputArgument::OPTIONAL, 'The name of the message class (e.g. <fg=yellow>SendEmailMessage</>)') ->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeMessage.txt')) ; } public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void { $command->addArgument('chosen-transport', InputArgument::OPTIONAL); try { $manipulator = new YamlSourceManipulator($this->fileManager->getFileContents('config/packages/messenger.yaml')); $messengerData = $manipulator->getData(); } catch (\Exception $e) { } if (!isset($messengerData, $messengerData['framework']['messenger']['transports'])) { return; } $transports = array_keys($messengerData['framework']['messenger']['transports']); array_unshift($transports, $noTransport = '[no transport]'); $chosenTransport = $io->choice( 'Which transport do you want to route your message to?', $transports, $noTransport ); if ($noTransport !== $chosenTransport) { $input->setArgument('chosen-transport', $chosenTransport); } } public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void { $messageClassNameDetails = $generator->createClassNameDetails( $input->getArgument('name'), 'Message\\' ); $handlerClassNameDetails = $generator->createClassNameDetails( $input->getArgument('name').'Handler', 'MessageHandler\\', 'Handler' ); $generator->generateClass( $messageClassNameDetails->getFullName(), 'message/Message.tpl.php' ); $useStatements = new UseStatementGenerator([ MessageHandlerInterface::class, $messageClassNameDetails->getFullName(), ]); $generator->generateClass( $handlerClassNameDetails->getFullName(), 'message/MessageHandler.tpl.php', [ 'use_statements' => $useStatements, 'message_class_name' => $messageClassNameDetails->getShortName(), ] ); if (null !== $chosenTransport = $input->getArgument('chosen-transport')) { $this->updateMessengerConfig($generator, $chosenTransport, $messageClassNameDetails->getFullName()); } $generator->writeChanges(); $this->writeSuccessMessage($io); $io->text([ 'Next: Open your new message class and add the properties you need.', ' Then, open the new message handler and do whatever work you want!', 'Find the documentation at <fg=yellow>https://symfony.com/doc/current/messenger.html</>', ]); } private function updateMessengerConfig(Generator $generator, string $chosenTransport, string $messageClass): void { $manipulator = new YamlSourceManipulator($this->fileManager->getFileContents($configFilePath = 'config/packages/messenger.yaml')); $messengerData = $manipulator->getData(); if (!isset($messengerData['framework']['messenger']['routing'])) { $messengerData['framework']['messenger']['routing'] = []; } $messengerData['framework']['messenger']['routing'][$messageClass] = $chosenTransport; $manipulator->setData($messengerData); $generator->dumpFile($configFilePath, $manipulator->getContents()); } public function configureDependencies(DependencyBuilder $dependencies): void { $dependencies->addClassDependency( MessageBusInterface::class, 'messenger' ); } } src/Maker/MakeMessengerMiddleware.php 0000644 00000005653 15120141001 0013621 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Maker; use Symfony\Bundle\MakerBundle\ConsoleStyle; use Symfony\Bundle\MakerBundle\DependencyBuilder; use Symfony\Bundle\MakerBundle\Generator; use Symfony\Bundle\MakerBundle\InputConfiguration; use Symfony\Bundle\MakerBundle\Util\UseStatementGenerator; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Messenger\Middleware\MiddlewareInterface; use Symfony\Component\Messenger\Middleware\StackInterface; /** * @author Imad ZAIRIG <imadzairig@gmail.com> * * @internal */ final class MakeMessengerMiddleware extends AbstractMaker { public static function getCommandName(): string { return 'make:messenger-middleware'; } public static function getCommandDescription(): string { return 'Creates a new messenger middleware'; } public function configureCommand(Command $command, InputConfiguration $inputConfig): void { $command ->addArgument('name', InputArgument::OPTIONAL, 'The name of the middleware class (e.g. <fg=yellow>CustomMiddleware</>)') ->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeMessage.txt')); } public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void { $middlewareClassNameDetails = $generator->createClassNameDetails( $input->getArgument('name'), 'Middleware\\', 'Middleware' ); $useStatements = new UseStatementGenerator([ Envelope::class, MiddlewareInterface::class, StackInterface::class, ]); $generator->generateClass( $middlewareClassNameDetails->getFullName(), 'middleware/Middleware.tpl.php', [ 'use_statements' => $useStatements, ] ); $generator->writeChanges(); $this->writeSuccessMessage($io); $io->text([ 'Next:', sprintf('- Open the <info>%s</info> class and add the code you need', $middlewareClassNameDetails->getFullName()), '- Add the middleware to your <info>config/packages/messenger.yaml</info> file', 'Find the documentation at <fg=yellow>https://symfony.com/doc/current/messenger.html#middleware</>', ]); } public function configureDependencies(DependencyBuilder $dependencies): void { $dependencies->addClassDependency( MessageBusInterface::class, 'messenger' ); } } src/Maker/MakeMigration.php 0000644 00000013005 15120141001 0011612 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Maker; use Doctrine\Bundle\MigrationsBundle\Command\MigrationsDiffDoctrineCommand; use Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle; use Symfony\Bundle\MakerBundle\ApplicationAwareMakerInterface; use Symfony\Bundle\MakerBundle\Console\MigrationDiffFilteredOutput; use Symfony\Bundle\MakerBundle\ConsoleStyle; use Symfony\Bundle\MakerBundle\DependencyBuilder; use Symfony\Bundle\MakerBundle\Generator; use Symfony\Bundle\MakerBundle\InputConfiguration; use Symfony\Component\Console\Application; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; /** * @author Amrouche Hamza <hamza.simperfit@gmail.com> * @author Ryan Weaver <ryan@knpuniversity.com> */ final class MakeMigration extends AbstractMaker implements ApplicationAwareMakerInterface { private $projectDir; /** * @var Application */ private $application; public function __construct(string $projectDir) { $this->projectDir = $projectDir; } public static function getCommandName(): string { return 'make:migration'; } public static function getCommandDescription(): string { return 'Creates a new migration based on database changes'; } public function setApplication(Application $application) { $this->application = $application; } public function configureCommand(Command $command, InputConfiguration $inputConf) { $command ->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeMigration.txt')) ; if (class_exists(MigrationsDiffDoctrineCommand::class)) { // support for DoctrineMigrationsBundle 2.x $command ->addOption('db', null, InputOption::VALUE_REQUIRED, 'The database connection name') ->addOption('em', null, InputOption::VALUE_OPTIONAL, 'The entity manager name') ->addOption('shard', null, InputOption::VALUE_REQUIRED, 'The shard connection name') ; } } public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator) { $options = ['doctrine:migrations:diff']; // DoctrineMigrationsBundle 2.x support if ($input->hasOption('db') && null !== $input->getOption('db')) { $options[] = '--db='.$input->getOption('db'); } if ($input->hasOption('em') && null !== $input->getOption('em')) { $options[] = '--em='.$input->getOption('em'); } if ($input->hasOption('shard') && null !== $input->getOption('shard')) { $options[] = '--shard='.$input->getOption('shard'); } // end 2.x support $generateMigrationCommand = $this->application->find('doctrine:migrations:diff'); $generateMigrationCommandInput = new ArgvInput($options); if (!$input->isInteractive()) { $generateMigrationCommandInput->setInteractive(false); } $commandOutput = new MigrationDiffFilteredOutput($io->getOutput()); try { $returnCode = $generateMigrationCommand->run($generateMigrationCommandInput, $commandOutput); // non-zero code would ideally mean the internal command has already printed an errror // this happens if you "decline" generating a migration when you already // have some available if (0 !== $returnCode) { return $returnCode; } $migrationOutput = $commandOutput->fetch(); if (false !== strpos($migrationOutput, 'No changes detected')) { $this->noChangesMessage($io); return; } } catch (\Doctrine\Migrations\Generator\Exception\NoChangesDetected $exception) { $this->noChangesMessage($io); return; } $this->writeSuccessMessage($io); $migrationName = $this->getGeneratedMigrationFilename($migrationOutput); $io->text([ sprintf('Next: Review the new migration <info>%s</info>', $migrationName), 'Then: Run the migration with <info>php bin/console doctrine:migrations:migrate</info>', 'See <fg=yellow>https://symfony.com/doc/current/bundles/DoctrineMigrationsBundle/index.html</>', ]); } private function noChangesMessage(ConsoleStyle $io) { $io->warning([ 'No database changes were detected.', ]); $io->text([ 'The database schema and the application mapping information are already in sync.', '', ]); } public function configureDependencies(DependencyBuilder $dependencies) { $dependencies->addClassDependency( DoctrineMigrationsBundle::class, 'migrations' ); } private function getGeneratedMigrationFilename(string $migrationOutput): string { preg_match('#"(.*?)"#', $migrationOutput, $matches); if (!isset($matches[0])) { throw new \Exception('Your migration generated successfully, but an error occurred printing the summary of what occurred.'); } return str_replace($this->projectDir.'/', '', $matches[0]); } } src/Maker/MakeRegistrationForm.php 0000644 00000060067 15120141001 0013171 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Maker; use Doctrine\Bundle\DoctrineBundle\DoctrineBundle; use Doctrine\Common\Annotations\Annotation; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\Column; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Bridge\Twig\Mime\TemplatedEmail; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\MakerBundle\ConsoleStyle; use Symfony\Bundle\MakerBundle\DependencyBuilder; use Symfony\Bundle\MakerBundle\Doctrine\DoctrineHelper; use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException; use Symfony\Bundle\MakerBundle\FileManager; use Symfony\Bundle\MakerBundle\Generator; use Symfony\Bundle\MakerBundle\InputConfiguration; use Symfony\Bundle\MakerBundle\Renderer\FormTypeRenderer; use Symfony\Bundle\MakerBundle\Security\InteractiveSecurityHelper; use Symfony\Bundle\MakerBundle\Str; use Symfony\Bundle\MakerBundle\Util\ClassDetails; use Symfony\Bundle\MakerBundle\Util\ClassNameDetails; use Symfony\Bundle\MakerBundle\Util\ClassSourceManipulator; use Symfony\Bundle\MakerBundle\Util\UseStatementGenerator; use Symfony\Bundle\MakerBundle\Util\YamlSourceManipulator; use Symfony\Bundle\MakerBundle\Validator; use Symfony\Bundle\SecurityBundle\SecurityBundle; use Symfony\Bundle\TwigBundle\TwigBundle; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\PasswordType; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Mailer\MailerInterface; use Symfony\Component\Mime\Address; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\RouterInterface; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Guard\AuthenticatorInterface as GuardAuthenticatorInterface; use Symfony\Component\Security\Http\Authentication\UserAuthenticatorInterface; use Symfony\Component\Translation\Translator; use Symfony\Component\Validator\Validation; use Symfony\Contracts\Translation\TranslatorInterface; use SymfonyCasts\Bundle\VerifyEmail\Exception\VerifyEmailExceptionInterface; use SymfonyCasts\Bundle\VerifyEmail\Model\VerifyEmailSignatureComponents; use SymfonyCasts\Bundle\VerifyEmail\SymfonyCastsVerifyEmailBundle; use SymfonyCasts\Bundle\VerifyEmail\VerifyEmailHelperInterface; /** * @author Ryan Weaver <ryan@symfonycasts.com> * @author Jesse Rushlow <jr@rushlow.dev> * * @internal */ final class MakeRegistrationForm extends AbstractMaker { private $fileManager; private $formTypeRenderer; private $router; private $doctrineHelper; private $userClass; private $usernameField; private $passwordField; private $willVerifyEmail = false; private $verifyEmailAnonymously = false; private $idGetter; private $emailGetter; private $fromEmailAddress; private $fromEmailName; private $autoLoginAuthenticator; private $firewallName; private $redirectRouteName; private $addUniqueEntityConstraint; private $useNewAuthenticatorSystem = false; public function __construct(FileManager $fileManager, FormTypeRenderer $formTypeRenderer, RouterInterface $router, DoctrineHelper $doctrineHelper) { $this->fileManager = $fileManager; $this->formTypeRenderer = $formTypeRenderer; $this->router = $router; $this->doctrineHelper = $doctrineHelper; } public static function getCommandName(): string { return 'make:registration-form'; } public static function getCommandDescription(): string { return 'Creates a new registration form system'; } public function configureCommand(Command $command, InputConfiguration $inputConf): void { $command ->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeRegistrationForm.txt')) ; } public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void { $interactiveSecurityHelper = new InteractiveSecurityHelper(); if (!$this->fileManager->fileExists($path = 'config/packages/security.yaml')) { throw new RuntimeCommandException('The file "config/packages/security.yaml" does not exist. This command needs that file to accurately build your registration form.'); } $manipulator = new YamlSourceManipulator($this->fileManager->getFileContents($path)); $securityData = $manipulator->getData(); $providersData = $securityData['security']['providers'] ?? []; // Determine if we should use new security features introduced in Symfony 5.2 // @legacy - Can be removed when Symfony 5.4 support is dropped if (!interface_exists(GuardAuthenticatorInterface::class) || ($securityData['security']['enable_authenticator_manager'] ?? false)) { $this->useNewAuthenticatorSystem = true; } $this->userClass = $interactiveSecurityHelper->guessUserClass( $io, $providersData, 'Enter the User class that you want to create during registration (e.g. <fg=yellow>App\\Entity\\User</>)' ); $io->text(sprintf('Creating a registration form for <info>%s</info>', $this->userClass)); $this->usernameField = $interactiveSecurityHelper->guessUserNameField($io, $this->userClass, $providersData); $this->passwordField = $interactiveSecurityHelper->guessPasswordField($io, $this->userClass); // see if it makes sense to add the UniqueEntity constraint $userClassDetails = new ClassDetails($this->userClass); $addAnnotation = false; if (!$userClassDetails->doesDocBlockContainAnnotation('@UniqueEntity')) { $addAnnotation = $io->confirm(sprintf('Do you want to add a <comment>@UniqueEntity</comment> validation annotation on your <comment>%s</comment> class to make sure duplicate accounts aren\'t created?', Str::getShortClassName($this->userClass))); } $this->addUniqueEntityConstraint = $addAnnotation; $this->willVerifyEmail = $io->confirm('Do you want to send an email to verify the user\'s email address after registration?', true); if ($this->willVerifyEmail) { $this->checkComponentsExist($io); $emailText[] = 'By default, users are required to be authenticated when they click the verification link that is emailed to them.'; $emailText[] = 'This prevents the user from registering on their laptop, then clicking the link on their phone, without'; $emailText[] = 'having to log in. To allow multi device email verification, we can embed a user id in the verification link.'; $io->text($emailText); $io->newLine(); $this->verifyEmailAnonymously = $io->confirm('Would you like to include the user id in the verification link to allow anonymous email verification?', false); $this->idGetter = $interactiveSecurityHelper->guessIdGetter($io, $this->userClass); $this->emailGetter = $interactiveSecurityHelper->guessEmailGetter($io, $this->userClass, 'email'); $this->fromEmailAddress = $io->ask( 'What email address will be used to send registration confirmations? e.g. mailer@your-domain.com', null, [Validator::class, 'validateEmailAddress'] ); $this->fromEmailName = $io->ask( 'What "name" should be associated with that email address? e.g. "Acme Mail Bot"', null, [Validator::class, 'notBlank'] ); } if ($io->confirm('Do you want to automatically authenticate the user after registration?')) { $this->interactAuthenticatorQuestions( $input, $io, $interactiveSecurityHelper, $securityData, $command ); } if (!$this->autoLoginAuthenticator) { $routeNames = array_keys($this->router->getRouteCollection()->all()); $this->redirectRouteName = $io->choice('What route should the user be redirected to after registration?', $routeNames); } } private function interactAuthenticatorQuestions(InputInterface $input, ConsoleStyle $io, InteractiveSecurityHelper $interactiveSecurityHelper, array $securityData, Command $command): void { $firewallsData = $securityData['security']['firewalls'] ?? []; $firewallName = $interactiveSecurityHelper->guessFirewallName( $io, $securityData, 'Which firewall key in security.yaml holds the authenticator you want to use for logging in?' ); if (!isset($firewallsData[$firewallName])) { $io->note('No firewalls found - skipping authentication after registration. You might want to configure your security before running this command.'); return; } $this->firewallName = $firewallName; // get list of guard authenticators $authenticatorClasses = $interactiveSecurityHelper->getAuthenticatorClasses($firewallsData[$firewallName]); if (empty($authenticatorClasses)) { $io->note('No Guard authenticators found - so your user won\'t be automatically authenticated after registering.'); } else { $this->autoLoginAuthenticator = 1 === \count($authenticatorClasses) ? $authenticatorClasses[0] : $io->choice( 'Which authenticator\'s onAuthenticationSuccess() should be used after logging in?', $authenticatorClasses ); } } public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void { $userClassNameDetails = $generator->createClassNameDetails( '\\'.$this->userClass, 'Entity\\' ); $userDoctrineDetails = $this->doctrineHelper->createDoctrineDetails($userClassNameDetails->getFullName()); $userRepoVars = [ 'repository_full_class_name' => 'Doctrine\ORM\EntityManagerInterface', 'repository_class_name' => 'EntityManagerInterface', 'repository_var' => '$manager', ]; $userRepository = $userDoctrineDetails->getRepositoryClass(); if (null !== $userRepository) { $userRepoClassDetails = $generator->createClassNameDetails('\\'.$userRepository, 'Repository\\', 'Repository'); $userRepoVars = [ 'repository_full_class_name' => $userRepoClassDetails->getFullName(), 'repository_class_name' => $userRepoClassDetails->getShortName(), 'repository_var' => sprintf('$%s', lcfirst($userRepoClassDetails->getShortName())), ]; } $verifyEmailServiceClassNameDetails = $generator->createClassNameDetails( 'EmailVerifier', 'Security\\' ); if ($this->willVerifyEmail) { $useStatements = new UseStatementGenerator([ EntityManagerInterface::class, TemplatedEmail::class, Request::class, MailerInterface::class, UserInterface::class, VerifyEmailExceptionInterface::class, VerifyEmailHelperInterface::class, ]); $generator->generateClass( $verifyEmailServiceClassNameDetails->getFullName(), 'verifyEmail/EmailVerifier.tpl.php', array_merge([ 'use_statements' => $useStatements, 'id_getter' => $this->idGetter, 'email_getter' => $this->emailGetter, 'verify_email_anonymously' => $this->verifyEmailAnonymously, ], $userRepoVars ) ); $generator->generateTemplate( 'registration/confirmation_email.html.twig', 'registration/twig_email.tpl.php' ); } // 1) Generate the form class $usernameField = $this->usernameField; $formClassDetails = $this->generateFormClass( $userClassNameDetails, $generator, $usernameField ); // 2) Generate the controller $controllerClassNameDetails = $generator->createClassNameDetails( 'RegistrationController', 'Controller\\' ); $useStatements = new UseStatementGenerator([ AbstractController::class, $formClassDetails->getFullName(), $userClassNameDetails->getFullName(), Request::class, Response::class, Route::class, UserPasswordHasherInterface::class, EntityManagerInterface::class, ]); if ($this->willVerifyEmail) { $useStatements->addUseStatement([ $verifyEmailServiceClassNameDetails->getFullName(), TemplatedEmail::class, Address::class, VerifyEmailExceptionInterface::class, ]); if ($this->verifyEmailAnonymously) { $useStatements->addUseStatement($userRepoVars['repository_full_class_name']); } } if ($this->autoLoginAuthenticator) { $useStatements->addUseStatement([ $this->autoLoginAuthenticator, UserAuthenticatorInterface::class, ]); } if ($isTranslatorAvailable = class_exists(Translator::class)) { $useStatements->addUseStatement(TranslatorInterface::class); } $generator->generateController( $controllerClassNameDetails->getFullName(), 'registration/RegistrationController.tpl.php', array_merge([ 'use_statements' => $useStatements, 'route_path' => '/register', 'route_name' => 'app_register', 'form_class_name' => $formClassDetails->getShortName(), 'user_class_name' => $userClassNameDetails->getShortName(), 'password_field' => $this->passwordField, 'will_verify_email' => $this->willVerifyEmail, 'email_verifier_class_details' => $verifyEmailServiceClassNameDetails, 'verify_email_anonymously' => $this->verifyEmailAnonymously, 'from_email' => $this->fromEmailAddress, 'from_email_name' => $this->fromEmailName, 'email_getter' => $this->emailGetter, 'authenticator_class_name' => $this->autoLoginAuthenticator ? Str::getShortClassName($this->autoLoginAuthenticator) : null, 'authenticator_full_class_name' => $this->autoLoginAuthenticator, 'use_new_authenticator_system' => $this->useNewAuthenticatorSystem, 'firewall_name' => $this->firewallName, 'redirect_route_name' => $this->redirectRouteName, 'password_hasher_class_details' => ($passwordClassDetails = $generator->createClassNameDetails(UserPasswordHasherInterface::class, '\\')), 'password_hasher_variable_name' => str_replace('Interface', '', sprintf('$%s', lcfirst($passwordClassDetails->getShortName()))), // @legacy see passwordHasher conditional above 'use_password_hasher' => true, 'translator_available' => $isTranslatorAvailable, ], $userRepoVars ) ); // 3) Generate the template $generator->generateTemplate( 'registration/register.html.twig', 'registration/twig_template.tpl.php', [ 'username_field' => $usernameField, 'will_verify_email' => $this->willVerifyEmail, ] ); // 4) Update the User class if necessary if ($this->addUniqueEntityConstraint) { $classDetails = new ClassDetails($this->userClass); $userManipulator = new ClassSourceManipulator( file_get_contents($classDetails->getPath()) ); $userManipulator->setIo($io); if ($this->doctrineHelper->isDoctrineSupportingAttributes()) { $userManipulator->addAttributeToClass( UniqueEntity::class, ['fields' => [$usernameField], 'message' => sprintf('There is already an account with this %s', $usernameField)] ); } else { $userManipulator->addAnnotationToClass( UniqueEntity::class, [ 'fields' => [$usernameField], 'message' => sprintf('There is already an account with this %s', $usernameField), ] ); } $this->fileManager->dumpFile($classDetails->getPath(), $userManipulator->getSourceCode()); } if ($this->willVerifyEmail) { $classDetails = new ClassDetails($this->userClass); $userManipulator = new ClassSourceManipulator( file_get_contents($classDetails->getPath()), false, $this->doctrineHelper->isClassAnnotated($this->userClass), true, $this->doctrineHelper->doesClassUsesAttributes($this->userClass) ); $userManipulator->setIo($io); $userManipulator->addProperty( 'isVerified', ['@ORM\Column(type="boolean")'], false, [$userManipulator->buildAttributeNode(Column::class, ['type' => 'boolean'], 'ORM')] ); $userManipulator->addAccessorMethod('isVerified', 'isVerified', 'bool', false); $userManipulator->addSetter('isVerified', 'bool', false); $this->fileManager->dumpFile($classDetails->getPath(), $userManipulator->getSourceCode()); } $generator->writeChanges(); $this->writeSuccessMessage($io); $this->successMessage($io, $this->willVerifyEmail, $userClassNameDetails->getShortName()); } private function successMessage(ConsoleStyle $io, bool $emailVerification, string $userClass): void { $closing[] = 'Next:'; if (!$emailVerification) { $closing[] = 'Make any changes you need to the form, controller & template.'; } else { $index = 1; if ($missingPackagesMessage = $this->getMissingComponentsComposerMessage()) { $closing[] = '1) Install some missing packages:'; $closing[] = sprintf(' <fg=green>%s</>', $missingPackagesMessage); ++$index; } $closing[] = sprintf('%d) In <fg=yellow>RegistrationController::verifyUserEmail()</>:', $index++); $closing[] = ' * Customize the last <fg=yellow>redirectToRoute()</> after a successful email verification.'; $closing[] = ' * Make sure you\'re rendering <fg=yellow>success</> flash messages or change the <fg=yellow>$this->addFlash()</> line.'; $closing[] = sprintf('%d) Review and customize the form, controller, and templates as needed.', $index++); $closing[] = sprintf('%d) Run <fg=yellow>"php bin/console make:migration"</> to generate a migration for the newly added <fg=yellow>%s::isVerified</> property.', $index++, $userClass); } $io->text($closing); $io->newLine(); $io->text('Then open your browser, go to "/register" and enjoy your new form!'); $io->newLine(); } private function checkComponentsExist(ConsoleStyle $io): void { $message = $this->getMissingComponentsComposerMessage(); if ($message) { $io->warning([ 'We\'re missing some important components. Don\'t forget to install these after you\'re finished.', $message, ]); } } private function getMissingComponentsComposerMessage(): ?string { $missing = false; $composerMessage = 'composer require'; // verify-email-bundle 1.1.1 includes support for translations and a fix for the bad expiration time bug. // we need to check that if the bundle is installed, it is version 1.1.1 or greater if (class_exists(SymfonyCastsVerifyEmailBundle::class)) { $reflectedComponents = new \ReflectionClass(VerifyEmailSignatureComponents::class); if (!$reflectedComponents->hasMethod('getExpirationMessageKey')) { throw new RuntimeCommandException('Please upgrade symfonycasts/verify-email-bundle to version 1.1.1 or greater.'); } } else { $missing = true; $composerMessage = sprintf('%s symfonycasts/verify-email-bundle', $composerMessage); } if (!interface_exists(MailerInterface::class)) { $missing = true; $composerMessage = sprintf('%s symfony/mailer', $composerMessage); } if (!$missing) { return null; } return $composerMessage; } public function configureDependencies(DependencyBuilder $dependencies): void { $dependencies->addClassDependency( Annotation::class, 'doctrine/annotations' ); $dependencies->addClassDependency( AbstractType::class, 'form' ); $dependencies->addClassDependency( Validation::class, 'validator' ); $dependencies->addClassDependency( TwigBundle::class, 'twig-bundle' ); $dependencies->addClassDependency( DoctrineBundle::class, 'orm' ); $dependencies->addClassDependency( SecurityBundle::class, 'security' ); } private function generateFormClass(ClassNameDetails $userClassDetails, Generator $generator, string $usernameField): ClassNameDetails { $formClassDetails = $generator->createClassNameDetails( 'RegistrationFormType', 'Form\\' ); $formFields = [ $usernameField => null, 'agreeTerms' => [ 'type' => CheckboxType::class, 'options_code' => <<<EOF 'mapped' => false, 'constraints' => [ new IsTrue([ 'message' => 'You should agree to our terms.', ]), ], EOF ], 'plainPassword' => [ 'type' => PasswordType::class, 'options_code' => <<<EOF // instead of being set onto the object directly, // this is read and encoded in the controller 'mapped' => false, 'attr' => ['autocomplete' => 'new-password'], 'constraints' => [ new NotBlank([ 'message' => 'Please enter a password', ]), new Length([ 'min' => 6, 'minMessage' => 'Your password should be at least {{ limit }} characters', // max length allowed by Symfony for security reasons 'max' => 4096, ]), ], EOF ], ]; $this->formTypeRenderer->render( $formClassDetails, $formFields, $userClassDetails, [ 'Symfony\Component\Validator\Constraints\IsTrue', 'Symfony\Component\Validator\Constraints\Length', 'Symfony\Component\Validator\Constraints\NotBlank', ] ); return $formClassDetails; } } src/Maker/MakeResetPassword.php 0000644 00000051250 15120141001 0012472 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Maker; use Doctrine\Common\Annotations\Annotation; use Doctrine\ORM\EntityManagerInterface; use PhpParser\Builder\Param; use Symfony\Bridge\Twig\AppVariable; use Symfony\Bridge\Twig\Mime\TemplatedEmail; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\MakerBundle\ConsoleStyle; use Symfony\Bundle\MakerBundle\DependencyBuilder; use Symfony\Bundle\MakerBundle\Doctrine\DoctrineHelper; use Symfony\Bundle\MakerBundle\Doctrine\EntityClassGenerator; use Symfony\Bundle\MakerBundle\Doctrine\ORMDependencyBuilder; use Symfony\Bundle\MakerBundle\Doctrine\RelationManyToOne; use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException; use Symfony\Bundle\MakerBundle\FileManager; use Symfony\Bundle\MakerBundle\Generator; use Symfony\Bundle\MakerBundle\InputConfiguration; use Symfony\Bundle\MakerBundle\Security\InteractiveSecurityHelper; use Symfony\Bundle\MakerBundle\Util\ClassNameDetails; use Symfony\Bundle\MakerBundle\Util\ClassSourceManipulator; use Symfony\Bundle\MakerBundle\Util\UseStatementGenerator; use Symfony\Bundle\MakerBundle\Util\YamlSourceManipulator; use Symfony\Bundle\MakerBundle\Validator; use Symfony\Bundle\SecurityBundle\SecurityBundle; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\EmailType; use Symfony\Component\Form\Extension\Core\Type\PasswordType; use Symfony\Component\Form\Extension\Core\Type\RepeatedType; use Symfony\Component\Form\Form; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Mailer\MailerInterface; use Symfony\Component\Mime\Address; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; use Symfony\Component\Translation\Translator; use Symfony\Component\Validator\Constraints\Length; use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Validation; use Symfony\Component\Yaml\Yaml; use Symfony\Contracts\Translation\TranslatorInterface; use SymfonyCasts\Bundle\ResetPassword\Controller\ResetPasswordControllerTrait; use SymfonyCasts\Bundle\ResetPassword\Exception\ResetPasswordExceptionInterface; use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordRequestInterface; use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordRequestTrait; use SymfonyCasts\Bundle\ResetPassword\Persistence\Repository\ResetPasswordRequestRepositoryTrait; use SymfonyCasts\Bundle\ResetPassword\Persistence\ResetPasswordRequestRepositoryInterface; use SymfonyCasts\Bundle\ResetPassword\ResetPasswordHelper; use SymfonyCasts\Bundle\ResetPassword\ResetPasswordHelperInterface; use SymfonyCasts\Bundle\ResetPassword\SymfonyCastsResetPasswordBundle; /** * @author Romaric Drigon <romaric.drigon@gmail.com> * @author Jesse Rushlow <jr@rushlow.dev> * @author Ryan Weaver <ryan@symfonycasts.com> * @author Antoine Michelet <jean.marcel.michelet@gmail.com> * * @internal * @final */ class MakeResetPassword extends AbstractMaker { private $fileManager; private $doctrineHelper; private $entityClassGenerator; private $fromEmailAddress; private $fromEmailName; private $controllerResetSuccessRedirect; private $userClass; private $emailPropertyName; private $emailGetterMethodName; private $passwordSetterMethodName; public function __construct(FileManager $fileManager, DoctrineHelper $doctrineHelper, EntityClassGenerator $entityClassGenerator) { $this->fileManager = $fileManager; $this->doctrineHelper = $doctrineHelper; $this->entityClassGenerator = $entityClassGenerator; } public static function getCommandName(): string { return 'make:reset-password'; } public static function getCommandDescription(): string { return 'Create controller, entity, and repositories for use with symfonycasts/reset-password-bundle'; } public function configureCommand(Command $command, InputConfiguration $inputConfig): void { $command ->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeResetPassword.txt')) ; } public function configureDependencies(DependencyBuilder $dependencies): void { $dependencies->addClassDependency(SymfonyCastsResetPasswordBundle::class, 'symfonycasts/reset-password-bundle'); $dependencies->addClassDependency(MailerInterface::class, 'symfony/mailer'); $dependencies->addClassDependency(Form::class, 'symfony/form'); $dependencies->addClassDependency(Validation::class, 'symfony/validator'); $dependencies->addClassDependency(SecurityBundle::class, 'security-bundle'); $dependencies->addClassDependency(AppVariable::class, 'twig'); ORMDependencyBuilder::buildDependencies($dependencies); $dependencies->addClassDependency(Annotation::class, 'annotations'); // reset-password-bundle 1.6 includes the ability to generate a fake token. // we need to check that version 1.6 is installed if (class_exists(ResetPasswordHelper::class) && !method_exists(ResetPasswordHelper::class, 'generateFakeResetToken')) { throw new RuntimeCommandException('Please run "composer upgrade symfonycasts/reset-password-bundle". Version 1.6 or greater of this bundle is required.'); } } public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void { $io->title('Let\'s make a password reset feature!'); $interactiveSecurityHelper = new InteractiveSecurityHelper(); if (!$this->fileManager->fileExists($path = 'config/packages/security.yaml')) { throw new RuntimeCommandException('The file "config/packages/security.yaml" does not exist. This command needs that file to accurately build the reset password form.'); } $manipulator = new YamlSourceManipulator($this->fileManager->getFileContents($path)); $securityData = $manipulator->getData(); $providersData = $securityData['security']['providers'] ?? []; $this->userClass = $interactiveSecurityHelper->guessUserClass( $io, $providersData, 'What is the User entity that should be used with the "forgotten password" feature? (e.g. <fg=yellow>App\\Entity\\User</>)' ); $this->emailPropertyName = $interactiveSecurityHelper->guessEmailField($io, $this->userClass); $this->emailGetterMethodName = $interactiveSecurityHelper->guessEmailGetter($io, $this->userClass, $this->emailPropertyName); $this->passwordSetterMethodName = $interactiveSecurityHelper->guessPasswordSetter($io, $this->userClass); $io->text(sprintf('Implementing reset password for <info>%s</info>', $this->userClass)); $io->section('- ResetPasswordController -'); $io->text('A named route is used for redirecting after a successful reset. Even a route that does not exist yet can be used here.'); $this->controllerResetSuccessRedirect = $io->ask( 'What route should users be redirected to after their password has been successfully reset?', 'app_home', [Validator::class, 'notBlank'] ); $io->section('- Email -'); $emailText[] = 'These are used to generate the email code. Don\'t worry, you can change them in the code later!'; $io->text($emailText); $this->fromEmailAddress = $io->ask( 'What email address will be used to send reset confirmations? e.g. mailer@your-domain.com', null, [Validator::class, 'validateEmailAddress'] ); $this->fromEmailName = $io->ask( 'What "name" should be associated with that email address? e.g. "Acme Mail Bot"', null, [Validator::class, 'notBlank'] ); } public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void { $userClassNameDetails = $generator->createClassNameDetails( '\\'.$this->userClass, 'Entity\\' ); $controllerClassNameDetails = $generator->createClassNameDetails( 'ResetPasswordController', 'Controller\\' ); $requestClassNameDetails = $generator->createClassNameDetails( 'ResetPasswordRequest', 'Entity\\' ); $repositoryClassNameDetails = $generator->createClassNameDetails( 'ResetPasswordRequestRepository', 'Repository\\' ); $requestFormTypeClassNameDetails = $generator->createClassNameDetails( 'ResetPasswordRequestFormType', 'Form\\' ); $changePasswordFormTypeClassNameDetails = $generator->createClassNameDetails( 'ChangePasswordFormType', 'Form\\' ); /* * @legacy Conditional can be removed when MakerBundle no longer * supports Symfony < 5.2 */ $passwordHasher = UserPasswordEncoderInterface::class; if (interface_exists(UserPasswordHasherInterface::class)) { $passwordHasher = UserPasswordHasherInterface::class; } $useStatements = new UseStatementGenerator([ AbstractController::class, $userClassNameDetails->getFullName(), $changePasswordFormTypeClassNameDetails->getFullName(), $requestFormTypeClassNameDetails->getFullName(), TemplatedEmail::class, RedirectResponse::class, Request::class, Response::class, MailerInterface::class, Address::class, Route::class, ResetPasswordControllerTrait::class, ResetPasswordExceptionInterface::class, ResetPasswordHelperInterface::class, $passwordHasher, EntityManagerInterface::class, ]); // Namespace for ResetPasswordExceptionInterface was imported above $problemValidateMessageOrConstant = \defined('SymfonyCasts\Bundle\ResetPassword\Exception\ResetPasswordExceptionInterface::MESSAGE_PROBLEM_VALIDATE') ? 'ResetPasswordExceptionInterface::MESSAGE_PROBLEM_VALIDATE' : "'There was a problem validating your password reset request'"; $problemHandleMessageOrConstant = \defined('SymfonyCasts\Bundle\ResetPassword\Exception\ResetPasswordExceptionInterface::MESSAGE_PROBLEM_HANDLE') ? 'ResetPasswordExceptionInterface::MESSAGE_PROBLEM_HANDLE' : "'There was a problem handling your password reset request'"; if ($isTranslatorAvailable = class_exists(Translator::class)) { $useStatements->addUseStatement(TranslatorInterface::class); } $generator->generateController( $controllerClassNameDetails->getFullName(), 'resetPassword/ResetPasswordController.tpl.php', [ 'use_statements' => $useStatements, 'user_class_name' => $userClassNameDetails->getShortName(), 'request_form_type_class_name' => $requestFormTypeClassNameDetails->getShortName(), 'reset_form_type_class_name' => $changePasswordFormTypeClassNameDetails->getShortName(), 'password_setter' => $this->passwordSetterMethodName, 'success_redirect_route' => $this->controllerResetSuccessRedirect, 'from_email' => $this->fromEmailAddress, 'from_email_name' => $this->fromEmailName, 'email_getter' => $this->emailGetterMethodName, 'email_field' => $this->emailPropertyName, 'password_hasher_class_details' => ($passwordClassDetails = $generator->createClassNameDetails($passwordHasher, '\\')), 'password_hasher_variable_name' => str_replace('Interface', '', sprintf('$%s', lcfirst($passwordClassDetails->getShortName()))), // @legacy see passwordHasher conditional above 'use_password_hasher' => UserPasswordHasherInterface::class === $passwordHasher, // @legacy see passwordHasher conditional above 'problem_validate_message_or_constant' => $problemValidateMessageOrConstant, 'problem_handle_message_or_constant' => $problemHandleMessageOrConstant, 'translator_available' => $isTranslatorAvailable, ] ); $this->generateRequestEntity($generator, $requestClassNameDetails, $repositoryClassNameDetails); $this->setBundleConfig($io, $generator, $repositoryClassNameDetails->getFullName()); $useStatements = new UseStatementGenerator([ AbstractType::class, EmailType::class, FormBuilderInterface::class, OptionsResolver::class, NotBlank::class, ]); $generator->generateClass( $requestFormTypeClassNameDetails->getFullName(), 'resetPassword/ResetPasswordRequestFormType.tpl.php', [ 'use_statements' => $useStatements, 'email_field' => $this->emailPropertyName, ] ); $useStatements = new UseStatementGenerator([ AbstractType::class, PasswordType::class, RepeatedType::class, FormBuilderInterface::class, OptionsResolver::class, Length::class, NotBlank::class, ]); $generator->generateClass( $changePasswordFormTypeClassNameDetails->getFullName(), 'resetPassword/ChangePasswordFormType.tpl.php', ['use_statements' => $useStatements] ); $generator->generateTemplate( 'reset_password/check_email.html.twig', 'resetPassword/twig_check_email.tpl.php' ); $generator->generateTemplate( 'reset_password/email.html.twig', 'resetPassword/twig_email.tpl.php' ); $generator->generateTemplate( 'reset_password/request.html.twig', 'resetPassword/twig_request.tpl.php', [ 'email_field' => $this->emailPropertyName, ] ); $generator->generateTemplate( 'reset_password/reset.html.twig', 'resetPassword/twig_reset.tpl.php' ); $generator->writeChanges(); $this->writeSuccessMessage($io); $this->successMessage($input, $io, $requestClassNameDetails->getFullName()); } private function setBundleConfig(ConsoleStyle $io, Generator $generator, string $repositoryClassFullName): void { $configFileExists = $this->fileManager->fileExists($path = 'config/packages/reset_password.yaml'); /* * reset_password.yaml does not exist, we assume flex was present when * the bundle was installed & a customized configuration is in use. * Remind the developer to set the repository class accordingly. */ if (!$configFileExists) { $io->text(sprintf('We can\'t find %s. That\'s ok, you probably have a customized configuration.', $path)); $io->text('Just remember to set the <fg=yellow>request_password_repository</> in your configuration.'); $io->newLine(); return; } $manipulator = new YamlSourceManipulator($this->fileManager->getFileContents($path)); $data = $manipulator->getData(); $symfonyCastsKey = 'symfonycasts_reset_password'; /* * reset_password.yaml exists, and was probably created by flex; * Let's replace it with a "clean" file. */ if (1 >= \count($data[$symfonyCastsKey])) { $yaml = [ $symfonyCastsKey => [ 'request_password_repository' => $repositoryClassFullName, ], ]; $generator->dumpFile($path, Yaml::dump($yaml)); return; } /* * reset_password.yaml exists and appears to have been customized * before running make:reset-password. Let's just change the repository * value and preserve everything else. */ $data[$symfonyCastsKey]['request_password_repository'] = $repositoryClassFullName; $manipulator->setData($data); $generator->dumpFile($path, $manipulator->getContents()); } private function successMessage(InputInterface $input, ConsoleStyle $io, string $requestClassName): void { $closing[] = 'Next:'; $closing[] = sprintf(' 1) Run <fg=yellow>"php bin/console make:migration"</> to generate a migration for the new <fg=yellow>"%s"</> entity.', $requestClassName); $closing[] = ' 2) Review forms in <fg=yellow>"src/Form"</> to customize validation and labels.'; $closing[] = ' 3) Review and customize the templates in <fg=yellow>`templates/reset_password`</>.'; $closing[] = ' 4) Make sure your <fg=yellow>MAILER_DSN</> env var has the correct settings.'; $closing[] = ' 5) Create a "forgot your password link" to the <fg=yellow>app_forgot_password_request</> route on your login form.'; $io->text($closing); $io->newLine(); $io->text('Then open your browser, go to "/reset-password" and enjoy!'); $io->newLine(); } private function generateRequestEntity(Generator $generator, ClassNameDetails $requestClassNameDetails, ClassNameDetails $repositoryClassNameDetails): void { $requestEntityPath = $this->entityClassGenerator->generateEntityClass($requestClassNameDetails, false, false, false); $generator->writeChanges(); $useAttributesForDoctrineMapping = $this->doctrineHelper->isDoctrineSupportingAttributes() && $this->doctrineHelper->doesClassUsesAttributes($requestClassNameDetails->getFullName()); $manipulator = new ClassSourceManipulator( $this->fileManager->getFileContents($requestEntityPath), false, !$useAttributesForDoctrineMapping, true, $useAttributesForDoctrineMapping ); $manipulator->addInterface(ResetPasswordRequestInterface::class); $manipulator->addTrait(ResetPasswordRequestTrait::class); $manipulator->addConstructor([ (new Param('user'))->setType('object')->getNode(), (new Param('expiresAt'))->setType('\DateTimeInterface')->getNode(), (new Param('selector'))->setType('string')->getNode(), (new Param('hashedToken'))->setType('string')->getNode(), ], <<<'CODE' <?php $this->user = $user; $this->initialize($expiresAt, $selector, $hashedToken); CODE ); $manipulator->addManyToOneRelation((new RelationManyToOne()) ->setPropertyName('user') ->setTargetClassName($this->userClass) ->setMapInverseRelation(false) ->setCustomReturnType('object', false) ->avoidSetter() ); $this->fileManager->dumpFile($requestEntityPath, $manipulator->getSourceCode()); $this->entityClassGenerator->generateRepositoryClass( $repositoryClassNameDetails->getFullName(), $requestClassNameDetails->getFullName(), false, false ); $generator->writeChanges(); $pathRequestRepository = $this->fileManager->getRelativePathForFutureClass( $repositoryClassNameDetails->getFullName() ); $manipulator = new ClassSourceManipulator( $this->fileManager->getFileContents($pathRequestRepository) ); $manipulator->addInterface(ResetPasswordRequestRepositoryInterface::class); $manipulator->addTrait(ResetPasswordRequestRepositoryTrait::class); $methodBuilder = $manipulator->createMethodBuilder('createResetPasswordRequest', ResetPasswordRequestInterface::class, false); $manipulator->addMethodBuilder($methodBuilder, [ (new Param('user'))->setType('object')->getNode(), (new Param('expiresAt'))->setType('\DateTimeInterface')->getNode(), (new Param('selector'))->setType('string')->getNode(), (new Param('hashedToken'))->setType('string')->getNode(), ], <<<'CODE' <?php return new ResetPasswordRequest($user, $expiresAt, $selector, $hashedToken); CODE ); $this->fileManager->dumpFile($pathRequestRepository, $manipulator->getSourceCode()); } } src/Maker/MakeSerializerEncoder.php 0000644 00000005607 15120141001 0013303 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Maker; use Symfony\Bundle\MakerBundle\ConsoleStyle; use Symfony\Bundle\MakerBundle\DependencyBuilder; use Symfony\Bundle\MakerBundle\Generator; use Symfony\Bundle\MakerBundle\InputConfiguration; use Symfony\Bundle\MakerBundle\Util\UseStatementGenerator; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Serializer\Encoder\DecoderInterface; use Symfony\Component\Serializer\Encoder\EncoderInterface; use Symfony\Component\Serializer\Serializer; /** * @author Piotr Grabski-Gradzinski <piotr.gradzinski@gmail.com> */ final class MakeSerializerEncoder extends AbstractMaker { public static function getCommandName(): string { return 'make:serializer:encoder'; } public static function getCommandDescription(): string { return 'Creates a new serializer encoder class'; } public function configureCommand(Command $command, InputConfiguration $inputConfig): void { $command ->addArgument('name', InputArgument::OPTIONAL, 'Choose a class name for your encoder (e.g. <fg=yellow>YamlEncoder</>)') ->addArgument('format', InputArgument::OPTIONAL, 'Pick your format name (e.g. <fg=yellow>yaml</>)') ->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeSerializerEncoder.txt')) ; } public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void { $encoderClassNameDetails = $generator->createClassNameDetails( $input->getArgument('name'), 'Serializer\\', 'Encoder' ); $format = $input->getArgument('format'); $useStatements = new UseStatementGenerator([ DecoderInterface::class, EncoderInterface::class, ]); $generator->generateClass( $encoderClassNameDetails->getFullName(), 'serializer/Encoder.tpl.php', [ 'use_statements' => $useStatements, 'format' => $format, ] ); $generator->writeChanges(); $this->writeSuccessMessage($io); $io->text([ 'Next: Open your new serializer encoder class and start customizing it.', 'Find the documentation at <fg=yellow>http://symfony.com/doc/current/serializer/custom_encoders.html</>', ]); } public function configureDependencies(DependencyBuilder $dependencies): void { $dependencies->addClassDependency( Serializer::class, 'serializer' ); } } src/Maker/MakeSerializerNormalizer.php 0000644 00000005560 15120141001 0014044 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Maker; use Symfony\Bundle\MakerBundle\ConsoleStyle; use Symfony\Bundle\MakerBundle\DependencyBuilder; use Symfony\Bundle\MakerBundle\Generator; use Symfony\Bundle\MakerBundle\InputConfiguration; use Symfony\Bundle\MakerBundle\Util\UseStatementGenerator; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\Serializer\Serializer; /** * @author Grégoire Pineau <lyrixx@lyrixx.info> */ final class MakeSerializerNormalizer extends AbstractMaker { public static function getCommandName(): string { return 'make:serializer:normalizer'; } public static function getCommandDescription(): string { return 'Creates a new serializer normalizer class'; } public function configureCommand(Command $command, InputConfiguration $inputConfig): void { $command ->addArgument('name', InputArgument::OPTIONAL, 'Choose a class name for your normalizer (e.g. <fg=yellow>UserNormalizer</>)') ->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeSerializerNormalizer.txt')) ; } public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void { $normalizerClassNameDetails = $generator->createClassNameDetails( $input->getArgument('name'), 'Serializer\\Normalizer\\', 'Normalizer' ); $useStatements = new UseStatementGenerator([ NormalizerInterface::class, ObjectNormalizer::class, CacheableSupportsMethodInterface::class, ]); $generator->generateClass( $normalizerClassNameDetails->getFullName(), 'serializer/Normalizer.tpl.php', [ 'use_statements' => $useStatements, ] ); $generator->writeChanges(); $this->writeSuccessMessage($io); $io->text([ 'Next: Open your new serializer normalizer class and start customizing it.', 'Find the documentation at <fg=yellow>https://symfony.com/doc/current/serializer/custom_normalizer.html</>', ]); } public function configureDependencies(DependencyBuilder $dependencies): void { $dependencies->addClassDependency( Serializer::class, 'serializer' ); } } src/Maker/MakeStimulusController.php 0000644 00000016547 15120141001 0013570 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Maker; use Symfony\Bundle\MakerBundle\ConsoleStyle; use Symfony\Bundle\MakerBundle\DependencyBuilder; use Symfony\Bundle\MakerBundle\Generator; use Symfony\Bundle\MakerBundle\InputConfiguration; use Symfony\Bundle\MakerBundle\Str; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Question\Question; use Symfony\WebpackEncoreBundle\WebpackEncoreBundle; /** * @author Abdelilah Jabri <jbrabdelilah@gmail.com> * * @internal */ final class MakeStimulusController extends AbstractMaker { public static function getCommandName(): string { return 'make:stimulus-controller'; } public static function getCommandDescription(): string { return 'Creates a new Stimulus controller'; } public function configureCommand(Command $command, InputConfiguration $inputConfig): void { $command ->addArgument('name', InputArgument::REQUIRED, 'The name of the Stimulus controller (e.g. <fg=yellow>hello</>)') ->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeStimulusController.txt')); } public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void { $command->addArgument('extension', InputArgument::OPTIONAL); $command->addArgument('targets', InputArgument::OPTIONAL, '', []); $command->addArgument('values', InputArgument::OPTIONAL, '', []); $chosenExtension = $io->choice( 'Language (<fg=yellow>JavaScript</> or <fg=yellow>TypeScript</>)', [ 'js' => 'JavaScript', 'ts' => 'TypeScript', ] ); $input->setArgument('extension', $chosenExtension); if ($io->confirm('Do you want to include targets?')) { $targets = []; $isFirstTarget = true; while (true) { $newTarget = $this->askForNextTarget($io, $targets, $isFirstTarget); $isFirstTarget = false; if (null === $newTarget) { break; } $targets[] = $newTarget; } $input->setArgument('targets', $targets); } if ($io->confirm('Do you want to include values?')) { $values = []; $isFirstValue = true; while (true) { $newValue = $this->askForNextValue($io, $values, $isFirstValue); $isFirstValue = false; if (null === $newValue) { break; } $values[$newValue['name']] = $newValue; } $input->setArgument('values', $values); } } public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void { $controllerName = Str::asSnakeCase($input->getArgument('name')); $chosenExtension = $input->getArgument('extension'); $targets = $input->getArgument('targets'); $values = $input->getArgument('values'); $targets = empty($targets) ? $targets : sprintf("['%s']", implode("', '", $targets)); $fileName = sprintf('%s_controller.%s', $controllerName, $chosenExtension); $filePath = sprintf('assets/controllers/%s', $fileName); $generator->generateFile( $filePath, 'stimulus/Controller.tpl.php', [ 'targets' => $targets, 'values' => $values, ] ); $generator->writeChanges(); $this->writeSuccessMessage($io); $io->text([ 'Next:', sprintf('- Open <info>%s</info> and add the code you need', $filePath), 'Find the documentation at <fg=yellow>https://github.com/symfony/stimulus-bridge</>', ]); } private function askForNextTarget(ConsoleStyle $io, array $targets, bool $isFirstTarget): ?string { $questionText = 'New target name (press <return> to stop adding targets)'; if (!$isFirstTarget) { $questionText = 'Add another target? Enter the target name (or press <return> to stop adding targets)'; } $targetName = $io->ask($questionText, null, function (?string $name) use ($targets) { if (\in_array($name, $targets)) { throw new \InvalidArgumentException(sprintf('The "%s" target already exists.', $name)); } return $name; }); return !$targetName ? null : $targetName; } private function askForNextValue(ConsoleStyle $io, array $values, bool $isFirstValue): ?array { $questionText = 'New value name (press <return> to stop adding values)'; if (!$isFirstValue) { $questionText = 'Add another value? Enter the value name (or press <return> to stop adding values)'; } $valueName = $io->ask($questionText, null, function ($name) use ($values) { if (\array_key_exists($name, $values)) { throw new \InvalidArgumentException(sprintf('The "%s" value already exists.', $name)); } return $name; }); if (!$valueName) { return null; } $defaultType = 'String'; // try to guess the type by the value name prefix/suffix // convert to snake case for simplicity $snakeCasedField = Str::asSnakeCase($valueName); if ('_id' === $suffix = substr($snakeCasedField, -3)) { $defaultType = 'Number'; } elseif (0 === strpos($snakeCasedField, 'is_')) { $defaultType = 'Boolean'; } elseif (0 === strpos($snakeCasedField, 'has_')) { $defaultType = 'Boolean'; } $type = null; $types = $this->getValuesTypes(); while (null === $type) { $question = new Question('Value type (enter <comment>?</comment> to see all types)', $defaultType); $question->setAutocompleterValues($types); $type = $io->askQuestion($question); if ('?' === $type) { $this->printAvailableTypes($io); $io->writeln(''); $type = null; } elseif (!\in_array($type, $types)) { $this->printAvailableTypes($io); $io->error(sprintf('Invalid type "%s".', $type)); $io->writeln(''); $type = null; } } return ['name' => $valueName, 'type' => $type]; } private function printAvailableTypes(ConsoleStyle $io): void { foreach ($this->getValuesTypes() as $type) { $io->writeln(sprintf('<info>%s</info>', $type)); } } private function getValuesTypes(): array { return [ 'Array', 'Boolean', 'Number', 'Object', 'String', ]; } public function configureDependencies(DependencyBuilder $dependencies): void { $dependencies->addClassDependency( WebpackEncoreBundle::class, 'webpack-encore-bundle' ); } } src/Maker/MakeSubscriber.php 0000644 00000010560 15120141001 0011767 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Maker; use Symfony\Bundle\MakerBundle\ConsoleStyle; use Symfony\Bundle\MakerBundle\DependencyBuilder; use Symfony\Bundle\MakerBundle\EventRegistry; use Symfony\Bundle\MakerBundle\Generator; use Symfony\Bundle\MakerBundle\InputConfiguration; use Symfony\Bundle\MakerBundle\Str; use Symfony\Bundle\MakerBundle\Util\UseStatementGenerator; use Symfony\Bundle\MakerBundle\Validator; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Question\Question; use Symfony\Component\EventDispatcher\EventSubscriberInterface; /** * @author Javier Eguiluz <javier.eguiluz@gmail.com> * @author Ryan Weaver <weaverryan@gmail.com> */ final class MakeSubscriber extends AbstractMaker { private $eventRegistry; public function __construct(EventRegistry $eventRegistry) { $this->eventRegistry = $eventRegistry; } public static function getCommandName(): string { return 'make:subscriber'; } public static function getCommandDescription(): string { return 'Creates a new event subscriber class'; } public function configureCommand(Command $command, InputConfiguration $inputConfig): void { $command ->addArgument('name', InputArgument::OPTIONAL, 'Choose a class name for your event subscriber (e.g. <fg=yellow>ExceptionSubscriber</>)') ->addArgument('event', InputArgument::OPTIONAL, 'What event do you want to subscribe to?') ->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeSubscriber.txt')) ; $inputConfig->setArgumentAsNonInteractive('event'); } public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void { if (!$input->getArgument('event')) { $events = $this->eventRegistry->getAllActiveEvents(); $io->writeln(' <fg=green>Suggested Events:</>'); $io->listing($this->eventRegistry->listActiveEvents($events)); $question = new Question(sprintf(' <fg=green>%s</>', $command->getDefinition()->getArgument('event')->getDescription())); $question->setAutocompleterValues($events); $question->setValidator([Validator::class, 'notBlank']); $event = $io->askQuestion($question); $input->setArgument('event', $event); } } public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void { $subscriberClassNameDetails = $generator->createClassNameDetails( $input->getArgument('name'), 'EventSubscriber\\', 'Subscriber' ); $event = $input->getArgument('event'); $eventFullClassName = $this->eventRegistry->getEventClassName($event); $eventClassName = $eventFullClassName ? Str::getShortClassName($eventFullClassName) : null; $useStatements = new UseStatementGenerator([ EventSubscriberInterface::class, ]); if (null !== $eventFullClassName) { $useStatements->addUseStatement($eventFullClassName); } $generator->generateClass( $subscriberClassNameDetails->getFullName(), 'event/Subscriber.tpl.php', [ 'use_statements' => $useStatements, 'event' => class_exists($event) ? sprintf('%s::class', $eventClassName) : sprintf('\'%s\'', $event), 'event_arg' => $eventClassName ? sprintf('%s $event', $eventClassName) : '$event', 'method_name' => class_exists($event) ? Str::asEventMethod($eventClassName) : Str::asEventMethod($event), ] ); $generator->writeChanges(); $this->writeSuccessMessage($io); $io->text([ 'Next: Open your new subscriber class and start customizing it.', 'Find the documentation at <fg=yellow>https://symfony.com/doc/current/event_dispatcher.html#creating-an-event-subscriber</>', ]); } public function configureDependencies(DependencyBuilder $dependencies): void { } } src/Maker/MakeTest.php 0000644 00000021502 15120141001 0010601 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Maker; use ApiPlatform\Core\Bridge\Symfony\Bundle\Test\ApiTestCase as LegacyApiTestCase; use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Bundle\FrameworkBundle\Test\WebTestAssertionsTrait; use Symfony\Bundle\MakerBundle\ConsoleStyle; use Symfony\Bundle\MakerBundle\DependencyBuilder; use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException; use Symfony\Bundle\MakerBundle\Generator; use Symfony\Bundle\MakerBundle\InputAwareMakerInterface; use Symfony\Bundle\MakerBundle\InputConfiguration; use Symfony\Bundle\MakerBundle\Validator; use Symfony\Component\BrowserKit\History; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\CssSelector\CssSelectorConverter; use Symfony\Component\Panther\PantherTestCaseTrait; /** * @author Kévin Dunglas <kevin@dunglas.fr> * @author Javier Eguiluz <javier.eguiluz@gmail.com> * @author Ryan Weaver <weaverryan@gmail.com> */ final class MakeTest extends AbstractMaker implements InputAwareMakerInterface { private const DESCRIPTIONS = [ 'TestCase' => 'basic PHPUnit tests', 'KernelTestCase' => 'basic tests that have access to Symfony services', 'WebTestCase' => 'to run browser-like scenarios, but that don\'t execute JavaScript code', 'ApiTestCase' => 'to run API-oriented scenarios', 'PantherTestCase' => 'to run e2e scenarios, using a real-browser or HTTP client and a real web server', ]; private const DOCS = [ 'TestCase' => 'https://symfony.com/doc/current/testing.html#unit-tests', 'KernelTestCase' => 'https://symfony.com/doc/current/testing/database.html#functional-testing-of-a-doctrine-repository', 'WebTestCase' => 'https://symfony.com/doc/current/testing.html#functional-tests', 'ApiTestCase' => 'https://api-platform.com/docs/distribution/testing/', 'PantherTestCase' => 'https://github.com/symfony/panther#testing-usage', ]; public static function getCommandName(): string { return 'make:test'; } /** * @deprecated remove this method when removing make:unit-test and make:functional-test */ public static function getCommandAliases(): iterable { yield 'make:unit-test'; yield 'make:functional-test'; } public static function getCommandDescription(): string { return 'Creates a new test class'; } public function configureCommand(Command $command, InputConfiguration $inputConfig): void { $typesDesc = []; $typesHelp = []; foreach (self::DESCRIPTIONS as $type => $desc) { $typesDesc[] = sprintf('<fg=yellow>%s</> (%s)', $type, $desc); $typesHelp[] = sprintf('* <info>%s</info>: %s', $type, $desc); } $command ->addArgument('type', InputArgument::OPTIONAL, 'The type of test: '.implode(', ', $typesDesc)) ->addArgument('name', InputArgument::OPTIONAL, 'The name of the test class (e.g. <fg=yellow>BlogPostTest</>)') ->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeTest.txt').implode("\n", $typesHelp)); $inputConfig->setArgumentAsNonInteractive('name'); $inputConfig->setArgumentAsNonInteractive('type'); } public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void { /* @deprecated remove the following block when removing make:unit-test and make:functional-test */ $this->handleDeprecatedMakerCommands($input, $io); if (null !== $type = $input->getArgument('type')) { if (!isset(self::DESCRIPTIONS[$type])) { throw new RuntimeCommandException(sprintf('The test type must be one of "%s", "%s" given.', implode('", "', array_keys(self::DESCRIPTIONS)), $type)); } } else { $input->setArgument( 'type', $io->choice('Which test type would you like?', self::DESCRIPTIONS) ); } if ('ApiTestCase' === $input->getArgument('type') && !class_exists(ApiTestCase::class) && !class_exists(LegacyApiTestCase::class)) { $io->warning([ 'API Platform is required for this test type. Install it with', 'composer require api', ]); } if ('PantherTestCase' === $input->getArgument('type') && !trait_exists(PantherTestCaseTrait::class)) { $io->warning([ 'symfony/panther is required for this test type. Install it with', 'composer require symfony/panther --dev', ]); } if (null === $input->getArgument('name')) { $io->writeln([ '', 'Choose a class name for your test, like:', ' * <fg=yellow>UtilTest</> (to create tests/UtilTest.php)', ' * <fg=yellow>Service\\UtilTest</> (to create tests/Service/UtilTest.php)', ' * <fg=yellow>\\App\Tests\\Service\\UtilTest</> (to create tests/Service/UtilTest.php)', ]); $nameArgument = $command->getDefinition()->getArgument('name'); $value = $io->ask($nameArgument->getDescription(), $nameArgument->getDefault(), [Validator::class, 'notBlank']); $input->setArgument($nameArgument->getName(), $value); } } public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void { $testClassNameDetails = $generator->createClassNameDetails( $input->getArgument('name'), 'Tests\\', 'Test' ); $type = $input->getArgument('type'); $generator->generateClass( $testClassNameDetails->getFullName(), "test/$type.tpl.php", [ 'web_assertions_are_available' => trait_exists(WebTestAssertionsTrait::class), 'use_legacy_container_property' => $this->useLegacyContainerProperty(), 'api_test_case_fqcn' => !class_exists(ApiTestCase::class) ? LegacyApiTestCase::class : ApiTestCase::class, ] ); $generator->writeChanges(); $this->writeSuccessMessage($io); $io->text([ 'Next: Open your new test class and start customizing it.', sprintf('Find the documentation at <fg=yellow>%s</>', self::DOCS[$type]), ]); } public function configureDependencies(DependencyBuilder $dependencies, InputInterface $input = null): void { if (null === $input) { return; } switch ($input->getArgument('type')) { case 'WebTestCase': $dependencies->addClassDependency( History::class, 'browser-kit', true, true ); $dependencies->addClassDependency( CssSelectorConverter::class, 'css-selector', true, true ); return; case 'ApiTestCase': $dependencies->addClassDependency( !class_exists(ApiTestCase::class) ? LegacyApiTestCase::class : ApiTestCase::class, 'api', true, false ); return; case 'PantherTestCase': $dependencies->addClassDependency( PantherTestCaseTrait::class, 'panther', true, true ); return; } } /** * @deprecated */ private function handleDeprecatedMakerCommands(InputInterface $input, ConsoleStyle $io): void { $currentCommand = $input->getFirstArgument(); switch ($currentCommand) { case 'make:unit-test': $input->setArgument('type', 'TestCase'); $io->warning('The "make:unit-test" command is deprecated, use "make:test" instead.'); break; case 'make:functional-test': $input->setArgument('type', trait_exists(PantherTestCaseTrait::class) ? 'WebTestCase' : 'PantherTestCase'); $io->warning('The "make:functional-test" command is deprecated, use "make:test" instead.'); break; } } private function useLegacyContainerProperty(): bool { // for 5.2 and lower return !method_exists(KernelTestCase::class, 'getContainer'); } } src/Maker/MakeTwigExtension.php 0000644 00000005163 15120141001 0012476 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Maker; use Symfony\Bundle\MakerBundle\ConsoleStyle; use Symfony\Bundle\MakerBundle\DependencyBuilder; use Symfony\Bundle\MakerBundle\Generator; use Symfony\Bundle\MakerBundle\InputConfiguration; use Symfony\Bundle\MakerBundle\Util\UseStatementGenerator; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Twig\Extension\AbstractExtension; use Twig\TwigFilter; use Twig\TwigFunction; /** * @author Javier Eguiluz <javier.eguiluz@gmail.com> * @author Ryan Weaver <weaverryan@gmail.com> */ final class MakeTwigExtension extends AbstractMaker { public static function getCommandName(): string { return 'make:twig-extension'; } public static function getCommandDescription(): string { return 'Creates a new Twig extension class'; } public function configureCommand(Command $command, InputConfiguration $inputConfig): void { $command ->addArgument('name', InputArgument::OPTIONAL, 'The name of the Twig extension class (e.g. <fg=yellow>AppExtension</>)') ->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeTwigExtension.txt')) ; } public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void { $extensionClassNameDetails = $generator->createClassNameDetails( $input->getArgument('name'), 'Twig\\', 'Extension' ); $useStatements = new UseStatementGenerator([ AbstractExtension::class, TwigFilter::class, TwigFunction::class, ]); $generator->generateClass( $extensionClassNameDetails->getFullName(), 'twig/Extension.tpl.php', ['use_statements' => $useStatements] ); $generator->writeChanges(); $this->writeSuccessMessage($io); $io->text([ 'Next: Open your new extension class and start customizing it.', 'Find the documentation at <fg=yellow>http://symfony.com/doc/current/templating/twig_extension.html</>', ]); } public function configureDependencies(DependencyBuilder $dependencies): void { $dependencies->addClassDependency( AbstractExtension::class, 'twig' ); } } src/Maker/MakeUnitTest.php 0000644 00000004775 15120141001 0011456 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Maker; use PHPUnit\Framework\TestCase; use Symfony\Bundle\MakerBundle\ConsoleStyle; use Symfony\Bundle\MakerBundle\DependencyBuilder; use Symfony\Bundle\MakerBundle\Generator; use Symfony\Bundle\MakerBundle\InputConfiguration; use Symfony\Bundle\MakerBundle\Util\UseStatementGenerator; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; trigger_deprecation('symfony/maker-bundle', '1.29', 'The "%s" class is deprecated, use "%s" instead.', MakeUnitTest::class, MakeTest::class); /** * @deprecated since MakerBundle 1.29, use Symfony\Bundle\MakerBundle\Maker\MakeTest instead. * * @author Javier Eguiluz <javier.eguiluz@gmail.com> * @author Ryan Weaver <weaverryan@gmail.com> */ final class MakeUnitTest extends AbstractMaker { public static function getCommandName(): string { return 'make:unit-test'; } public static function getCommandDescription(): string { return 'Creates a new unit test class'; } public function configureCommand(Command $command, InputConfiguration $inputConfig): void { $command ->addArgument('name', InputArgument::OPTIONAL, 'The name of the unit test class (e.g. <fg=yellow>UtilTest</>)') ->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeUnitTest.txt')) ; } public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void { $testClassNameDetails = $generator->createClassNameDetails( $input->getArgument('name'), 'Tests\\', 'Test' ); $generator->generateClass( $testClassNameDetails->getFullName(), 'test/Unit.tpl.php', ['use_statements' => new UseStatementGenerator([TestCase::class])] ); $generator->writeChanges(); $this->writeSuccessMessage($io); $io->text([ 'Next: Open your new test class and start customizing it.', 'Find the documentation at <fg=yellow>https://symfony.com/doc/current/testing.html#unit-tests</>', ]); } public function configureDependencies(DependencyBuilder $dependencies): void { } } src/Maker/MakeUser.php 0000644 00000026263 15120141001 0010611 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Maker; use Doctrine\Bundle\DoctrineBundle\DoctrineBundle; use Symfony\Bundle\MakerBundle\ConsoleStyle; use Symfony\Bundle\MakerBundle\DependencyBuilder; use Symfony\Bundle\MakerBundle\Doctrine\DoctrineHelper; use Symfony\Bundle\MakerBundle\Doctrine\EntityClassGenerator; use Symfony\Bundle\MakerBundle\Doctrine\ORMDependencyBuilder; use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException; use Symfony\Bundle\MakerBundle\FileManager; use Symfony\Bundle\MakerBundle\Generator; use Symfony\Bundle\MakerBundle\InputConfiguration; use Symfony\Bundle\MakerBundle\Security\SecurityConfigUpdater; use Symfony\Bundle\MakerBundle\Security\UserClassBuilder; use Symfony\Bundle\MakerBundle\Security\UserClassConfiguration; use Symfony\Bundle\MakerBundle\Util\ClassSourceManipulator; use Symfony\Bundle\MakerBundle\Util\UseStatementGenerator; use Symfony\Bundle\MakerBundle\Util\YamlManipulationFailedException; use Symfony\Bundle\MakerBundle\Validator; use Symfony\Bundle\SecurityBundle\SecurityBundle; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Security\Core\Exception\UnsupportedUserException; use Symfony\Component\Security\Core\Exception\UserNotFoundException; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Yaml\Yaml; /** * @author Ryan Weaver <weaverryan@gmail.com> * * @internal */ final class MakeUser extends AbstractMaker { private $fileManager; private $userClassBuilder; private $configUpdater; private $entityClassGenerator; private $doctrineHelper; public function __construct(FileManager $fileManager, UserClassBuilder $userClassBuilder, SecurityConfigUpdater $configUpdater, EntityClassGenerator $entityClassGenerator, DoctrineHelper $doctrineHelper) { $this->fileManager = $fileManager; $this->userClassBuilder = $userClassBuilder; $this->configUpdater = $configUpdater; $this->entityClassGenerator = $entityClassGenerator; $this->doctrineHelper = $doctrineHelper; } public static function getCommandName(): string { return 'make:user'; } public static function getCommandDescription(): string { return 'Creates a new security user class'; } public function configureCommand(Command $command, InputConfiguration $inputConfig): void { $command ->addArgument('name', InputArgument::OPTIONAL, 'The name of the security user class (e.g. <fg=yellow>User</>)') ->addOption('is-entity', null, InputOption::VALUE_NONE, 'Do you want to store user data in the database (via Doctrine)?') ->addOption('identity-property-name', null, InputOption::VALUE_REQUIRED, 'Enter a property name that will be the unique "display" name for the user (e.g. <comment>email, username, uuid</comment>)') ->addOption('with-password', null, InputOption::VALUE_NONE, 'Will this app be responsible for checking the password? Choose <comment>No</comment> if the password is actually checked by some other system (e.g. a single sign-on server)') ->addOption('use-argon2', null, InputOption::VALUE_NONE, 'Use the Argon2i password encoder? (deprecated)') ->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeUser.txt')); $inputConfig->setArgumentAsNonInteractive('name'); } public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void { if (null === $input->getArgument('name')) { $name = $io->ask( $command->getDefinition()->getArgument('name')->getDescription(), 'User' ); $input->setArgument('name', $name); } $userIsEntity = $io->confirm( 'Do you want to store user data in the database (via Doctrine)?', class_exists(DoctrineBundle::class) ); if ($userIsEntity) { $dependencies = new DependencyBuilder(); ORMDependencyBuilder::buildDependencies($dependencies); $missingPackagesMessage = $dependencies->getMissingPackagesMessage(self::getCommandName(), 'Doctrine must be installed to store user data in the database'); if ($missingPackagesMessage) { throw new RuntimeCommandException($missingPackagesMessage); } } $input->setOption('is-entity', $userIsEntity); $identityFieldName = $io->ask('Enter a property name that will be the unique "display" name for the user (e.g. <comment>email, username, uuid</comment>)', 'email', [Validator::class, 'validatePropertyName']); $input->setOption('identity-property-name', $identityFieldName); $io->text('Will this app need to hash/check user passwords? Choose <comment>No</comment> if passwords are not needed or will be checked/hashed by some other system (e.g. a single sign-on server).'); $userWillHavePassword = $io->confirm('Does this app need to hash/check user passwords?'); $input->setOption('with-password', $userWillHavePassword); } public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void { $userClassConfiguration = new UserClassConfiguration( $input->getOption('is-entity'), $input->getOption('identity-property-name'), $input->getOption('with-password') ); if ($input->getOption('use-argon2')) { @trigger_error('The "--use-argon2" option is deprecated since MakerBundle 1.12.', \E_USER_DEPRECATED); $userClassConfiguration->useArgon2(true); } $userClassNameDetails = $generator->createClassNameDetails( $input->getArgument('name'), $userClassConfiguration->isEntity() ? 'Entity\\' : 'Security\\' ); // A) Generate the User class if ($userClassConfiguration->isEntity()) { $classPath = $this->entityClassGenerator->generateEntityClass( $userClassNameDetails, false, // api resource $userClassConfiguration->hasPassword() // security user ); } else { $classPath = $generator->generateClass($userClassNameDetails->getFullName(), 'Class.tpl.php'); } // need to write changes early so we can modify the contents below $generator->writeChanges(); $useAttributesForDoctrineMapping = $userClassConfiguration->isEntity() && ($this->doctrineHelper->isDoctrineSupportingAttributes()) && $this->doctrineHelper->doesClassUsesAttributes($userClassNameDetails->getFullName()); // B) Implement UserInterface $manipulator = new ClassSourceManipulator( $this->fileManager->getFileContents($classPath), true, !$useAttributesForDoctrineMapping, true, $useAttributesForDoctrineMapping ); $manipulator->setIo($io); $this->userClassBuilder->addUserInterfaceImplementation($manipulator, $userClassConfiguration); $generator->dumpFile($classPath, $manipulator->getSourceCode()); // C) Generate a custom user provider, if necessary if (!$userClassConfiguration->isEntity()) { $userClassConfiguration->setUserProviderClass($generator->getRootNamespace().'\\Security\\UserProvider'); $useStatements = new UseStatementGenerator([ UnsupportedUserException::class, UserNotFoundException::class, PasswordAuthenticatedUserInterface::class, PasswordUpgraderInterface::class, UserInterface::class, UserProviderInterface::class, ]); $customProviderPath = $generator->generateClass( $userClassConfiguration->getUserProviderClass(), 'security/UserProvider.tpl.php', [ 'use_statements' => $useStatements, 'user_short_name' => $userClassNameDetails->getShortName(), ] ); } // D) Update security.yaml $securityYamlUpdated = false; $path = 'config/packages/security.yaml'; if ($this->fileManager->fileExists($path)) { try { $newYaml = $this->configUpdater->updateForUserClass( $this->fileManager->getFileContents($path), $userClassConfiguration, $userClassNameDetails->getFullName() ); $generator->dumpFile($path, $newYaml); $securityYamlUpdated = true; } catch (YamlManipulationFailedException $e) { } } $generator->writeChanges(); $this->writeSuccessMessage($io); $io->text('Next Steps:'); $nextSteps = [ sprintf('Review your new <info>%s</info> class.', $userClassNameDetails->getFullName()), ]; if ($userClassConfiguration->isEntity()) { $nextSteps[] = sprintf( 'Use <comment>make:entity</comment> to add more fields to your <info>%s</info> entity and then run <comment>make:migration</comment>.', $userClassNameDetails->getShortName() ); } else { $nextSteps[] = sprintf( 'Open <info>%s</info> to finish implementing your user provider.', $this->fileManager->relativizePath($customProviderPath) ); } if (!$securityYamlUpdated) { $yamlExample = $this->configUpdater->updateForUserClass( 'security: {}', $userClassConfiguration, $userClassNameDetails->getFullName() ); $nextSteps[] = "Your <info>security.yaml</info> could not be updated automatically. You'll need to add the following config manually:\n\n".$yamlExample; } $nextSteps[] = 'Create a way to authenticate! See https://symfony.com/doc/current/security.html'; $nextSteps = array_map(function ($step) { return sprintf(' - %s', $step); }, $nextSteps); $io->text($nextSteps); } public function configureDependencies(DependencyBuilder $dependencies, InputInterface $input = null): void { // checking for SecurityBundle guarantees security.yaml is present $dependencies->addClassDependency( SecurityBundle::class, 'security' ); // needed to update the YAML files $dependencies->addClassDependency( Yaml::class, 'yaml' ); if (null !== $input && $input->getOption('is-entity')) { ORMDependencyBuilder::buildDependencies($dependencies); } } } src/Maker/MakeValidator.php 0000644 00000005262 15120141001 0011614 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Maker; use Symfony\Bundle\MakerBundle\ConsoleStyle; use Symfony\Bundle\MakerBundle\DependencyBuilder; use Symfony\Bundle\MakerBundle\Generator; use Symfony\Bundle\MakerBundle\InputConfiguration; use Symfony\Bundle\MakerBundle\Str; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Validator\Validation; /** * @author Javier Eguiluz <javier.eguiluz@gmail.com> * @author Ryan Weaver <weaverryan@gmail.com> */ final class MakeValidator extends AbstractMaker { public static function getCommandName(): string { return 'make:validator'; } public static function getCommandDescription(): string { return 'Creates a new validator and constraint class'; } public function configureCommand(Command $command, InputConfiguration $inputConf) { $command ->addArgument('name', InputArgument::OPTIONAL, 'The name of the validator class (e.g. <fg=yellow>EnabledValidator</>)') ->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeValidator.txt')) ; } public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator) { $validatorClassNameDetails = $generator->createClassNameDetails( $input->getArgument('name'), 'Validator\\', 'Validator' ); $constraintFullClassName = Str::removeSuffix($validatorClassNameDetails->getFullName(), 'Validator'); $generator->generateClass( $validatorClassNameDetails->getFullName(), 'validator/Validator.tpl.php', [ 'constraint_class_name' => $constraintFullClassName, ] ); $generator->generateClass( $constraintFullClassName, 'validator/Constraint.tpl.php', [] ); $generator->writeChanges(); $this->writeSuccessMessage($io); $io->text([ 'Next: Open your new constraint & validators and add your logic.', 'Find the documentation at <fg=yellow>http://symfony.com/doc/current/validation/custom_constraint.html</>', ]); } public function configureDependencies(DependencyBuilder $dependencies) { $dependencies->addClassDependency( Validation::class, 'validator' ); } } src/Maker/MakeVoter.php 0000644 00000004435 15120141001 0010767 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Maker; use Symfony\Bundle\MakerBundle\ConsoleStyle; use Symfony\Bundle\MakerBundle\DependencyBuilder; use Symfony\Bundle\MakerBundle\Generator; use Symfony\Bundle\MakerBundle\InputConfiguration; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Security\Core\Authorization\Voter\Voter; /** * @author Javier Eguiluz <javier.eguiluz@gmail.com> * @author Ryan Weaver <weaverryan@gmail.com> */ final class MakeVoter extends AbstractMaker { public static function getCommandName(): string { return 'make:voter'; } public static function getCommandDescription(): string { return 'Creates a new security voter class'; } public function configureCommand(Command $command, InputConfiguration $inputConfig): void { $command ->addArgument('name', InputArgument::OPTIONAL, 'The name of the security voter class (e.g. <fg=yellow>BlogPostVoter</>)') ->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeVoter.txt')) ; } public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void { $voterClassNameDetails = $generator->createClassNameDetails( $input->getArgument('name'), 'Security\\Voter\\', 'Voter' ); $generator->generateClass( $voterClassNameDetails->getFullName(), 'security/Voter.tpl.php', [] ); $generator->writeChanges(); $this->writeSuccessMessage($io); $io->text([ 'Next: Open your voter and add your logic.', 'Find the documentation at <fg=yellow>https://symfony.com/doc/current/security/voters.html</>', ]); } public function configureDependencies(DependencyBuilder $dependencies): void { $dependencies->addClassDependency( Voter::class, 'security' ); } } src/Renderer/FormTypeRenderer.php 0000644 00000004502 15120141001 0013030 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Renderer; use Symfony\Bundle\MakerBundle\Generator; use Symfony\Bundle\MakerBundle\Str; use Symfony\Bundle\MakerBundle\Util\ClassNameDetails; use Symfony\Bundle\MakerBundle\Util\UseStatementGenerator; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; /** * @internal */ final class FormTypeRenderer { private $generator; public function __construct(Generator $generator) { $this->generator = $generator; } public function render(ClassNameDetails $formClassDetails, array $formFields, ClassNameDetails $boundClassDetails = null, array $constraintClasses = [], array $extraUseClasses = []): void { $fieldTypeUseStatements = []; $fields = []; foreach ($formFields as $name => $fieldTypeOptions) { $fieldTypeOptions = $fieldTypeOptions ?? ['type' => null, 'options_code' => null]; if (isset($fieldTypeOptions['type'])) { $fieldTypeUseStatements[] = $fieldTypeOptions['type']; $fieldTypeOptions['type'] = Str::getShortClassName($fieldTypeOptions['type']); } $fields[$name] = $fieldTypeOptions; } $useStatements = new UseStatementGenerator(array_unique(array_merge( $fieldTypeUseStatements, $extraUseClasses, $constraintClasses ))); $useStatements->addUseStatement([ AbstractType::class, FormBuilderInterface::class, OptionsResolver::class, ]); if ($boundClassDetails) { $useStatements->addUseStatement($boundClassDetails->getFullName()); } $this->generator->generateClass( $formClassDetails->getFullName(), 'form/Type.tpl.php', [ 'use_statements' => $useStatements, 'bounded_class_name' => $boundClassDetails ? $boundClassDetails->getShortName() : null, 'form_fields' => $fields, ] ); } } src/Resources/config/makers.xml 0000644 00000015610 15120141001 0012542 0 ustar 00 <?xml version="1.0" ?> <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> <services> <defaults public="false" /> <service id="maker.maker.make_authenticator" class="Symfony\Bundle\MakerBundle\Maker\MakeAuthenticator"> <argument type="service" id="maker.file_manager" /> <argument type="service" id="maker.security_config_updater" /> <argument type="service" id="maker.generator" /> <argument type="service" id="maker.doctrine_helper" /> <argument type="service" id="maker.security_controller_builder" /> <tag name="maker.command" /> </service> <service id="maker.maker.make_command" class="Symfony\Bundle\MakerBundle\Maker\MakeCommand"> <argument type="service" id="maker.php_compat_util" /> <tag name="maker.command" /> </service> <service id="maker.maker.make_controller" class="Symfony\Bundle\MakerBundle\Maker\MakeController"> <argument type="service" id="maker.php_compat_util" /> <tag name="maker.command" /> </service> <service id="maker.maker.make_crud" class="Symfony\Bundle\MakerBundle\Maker\MakeCrud"> <argument type="service" id="maker.doctrine_helper" /> <argument type="service" id="maker.renderer.form_type_renderer" /> <tag name="maker.command" /> </service> <service id="maker.maker.make_docker_database" class="Symfony\Bundle\MakerBundle\Maker\MakeDockerDatabase"> <argument type="service" id="maker.file_manager" /> <tag name="maker.command" /> </service> <service id="maker.maker.make_entity" class="Symfony\Bundle\MakerBundle\Maker\MakeEntity"> <argument type="service" id="maker.file_manager" /> <argument type="service" id="maker.doctrine_helper" /> <argument>null</argument> <argument type="service" id="maker.generator" /> <argument type="service" id="maker.entity_class_generator" /> <argument type="service" id="maker.php_compat_util" /> <tag name="maker.command" /> </service> <service id="maker.maker.make_fixtures" class="Symfony\Bundle\MakerBundle\Maker\MakeFixtures"> <tag name="maker.command" /> </service> <service id="maker.maker.make_form" class="Symfony\Bundle\MakerBundle\Maker\MakeForm"> <argument type="service" id="maker.doctrine_helper" /> <argument type="service" id="maker.renderer.form_type_renderer" /> <tag name="maker.command" /> </service> <service id="maker.maker.make_functional_test" class="Symfony\Bundle\MakerBundle\Maker\MakeFunctionalTest"> <tag name="maker.command" /> </service> <service id="maker.maker.make_message" class="Symfony\Bundle\MakerBundle\Maker\MakeMessage"> <argument type="service" id="maker.file_manager" /> <tag name="maker.command" /> </service> <service id="maker.maker.make_messenger_middleware" class="Symfony\Bundle\MakerBundle\Maker\MakeMessengerMiddleware"> <tag name="maker.command" /> </service> <service id="maker.maker.make_registration_form" class="Symfony\Bundle\MakerBundle\Maker\MakeRegistrationForm"> <argument type="service" id="maker.file_manager" /> <argument type="service" id="maker.renderer.form_type_renderer" /> <argument type="service" id="router" /> <argument type="service" id="maker.doctrine_helper" /> <tag name="maker.command" /> </service> <service id="maker.maker.make_reset_password" class="Symfony\Bundle\MakerBundle\Maker\MakeResetPassword"> <argument type="service" id="maker.file_manager" /> <argument type="service" id="maker.doctrine_helper" /> <argument type="service" id="maker.entity_class_generator" /> <tag name="maker.command" /> </service> <service id="maker.maker.make_serializer_encoder" class="Symfony\Bundle\MakerBundle\Maker\MakeSerializerEncoder"> <tag name="maker.command" /> </service> <service id="maker.maker.make_serializer_normalizer" class="Symfony\Bundle\MakerBundle\Maker\MakeSerializerNormalizer"> <tag name="maker.command" /> </service> <service id="maker.maker.make_subscriber" class="Symfony\Bundle\MakerBundle\Maker\MakeSubscriber"> <tag name="maker.command" /> <argument type="service" id="maker.event_registry" /> </service> <service id="maker.maker.make_twig_extension" class="Symfony\Bundle\MakerBundle\Maker\MakeTwigExtension"> <tag name="maker.command" /> </service> <service id="maker.maker.make_test" class="Symfony\Bundle\MakerBundle\Maker\MakeTest"> <tag name="maker.command" /> </service> <service id="maker.maker.make_unit_test" class="Symfony\Bundle\MakerBundle\Maker\MakeUnitTest"> <tag name="maker.command" /> </service> <service id="maker.maker.make_validator" class="Symfony\Bundle\MakerBundle\Maker\MakeValidator"> <tag name="maker.command" /> </service> <service id="maker.maker.make_voter" class="Symfony\Bundle\MakerBundle\Maker\MakeVoter"> <tag name="maker.command" /> </service> <service id="maker.maker.make_user" class="Symfony\Bundle\MakerBundle\Maker\MakeUser"> <argument type="service" id="maker.file_manager" /> <argument type="service" id="maker.user_class_builder" /> <argument type="service" id="maker.security_config_updater" /> <argument type="service" id="maker.entity_class_generator" /> <argument type="service" id="maker.doctrine_helper" /> <tag name="maker.command" /> </service> <service id="maker.maker.make_migration" class="Symfony\Bundle\MakerBundle\Maker\MakeMigration"> <argument>%kernel.project_dir%</argument> <tag name="maker.command" /> </service> <service id="maker.maker.make_stimulus_controller" class="Symfony\Bundle\MakerBundle\Maker\MakeStimulusController"> <tag name="maker.command" /> </service> </services> </container> src/Resources/config/services.xml 0000644 00000010412 15120141001 0013076 0 ustar 00 <?xml version="1.0" ?> <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> <services> <defaults public="false" /> <service id="maker.file_manager" class="Symfony\Bundle\MakerBundle\FileManager"> <argument type="service" id="filesystem" /> <argument type="service" id="maker.autoloader_util" /> <argument type="service" id="maker.file_link_formatter" /> <argument>%kernel.project_dir%</argument> <argument>%twig.default_path%</argument> </service> <service id="maker.autoloader_finder" class="Symfony\Bundle\MakerBundle\Util\ComposerAutoloaderFinder" > <argument /> <!-- root namespace --> </service> <service id="maker.autoloader_util" class="Symfony\Bundle\MakerBundle\Util\AutoloaderUtil"> <argument type="service" id="maker.autoloader_finder" /> </service> <service id="maker.file_link_formatter" class="Symfony\Bundle\MakerBundle\Util\MakerFileLinkFormatter" > <argument type="service" id="debug.file_link_formatter" on-invalid="ignore" /> </service> <service id="maker.event_registry" class="Symfony\Bundle\MakerBundle\EventRegistry"> <argument type="service" id="event_dispatcher" /> </service> <service id="maker.console_error_listener" class="Symfony\Bundle\MakerBundle\Event\ConsoleErrorSubscriber"> <tag name="kernel.event_subscriber" /> </service> <service id="maker.doctrine_helper" class="Symfony\Bundle\MakerBundle\Doctrine\DoctrineHelper"> <argument /> <!-- entity namespace --> <argument type="service" id="maker.php_compat_util" /> <argument type="service" id="doctrine" on-invalid="ignore" /> <argument key="$attributeMappingSupport">%maker.compatible_check.doctrine.supports_attributes%</argument> </service> <service id="maker.auto_command.abstract" class="Symfony\Bundle\MakerBundle\Command\MakerCommand" abstract="true"> <argument /> <!-- maker --> <argument type="service" id="maker.file_manager" /> <argument type="service" id="maker.generator" /> </service> <service id="maker.generator" class="Symfony\Bundle\MakerBundle\Generator"> <argument type="service" id="maker.file_manager" /> <argument /> <!-- root namespace --> <argument type="service" id="maker.php_compat_util" /> <argument type="service" id="maker.template_component_generator" /> </service> <service id="maker.entity_class_generator" class="Symfony\Bundle\MakerBundle\Doctrine\EntityClassGenerator"> <argument type="service" id="maker.generator" /> <argument type="service" id="maker.doctrine_helper" /> </service> <service id="maker.user_class_builder" class="Symfony\Bundle\MakerBundle\Security\UserClassBuilder" /> <service id="maker.security_config_updater" class="Symfony\Bundle\MakerBundle\Security\SecurityConfigUpdater" /> <service id="maker.renderer.form_type_renderer" class="Symfony\Bundle\MakerBundle\Renderer\FormTypeRenderer"> <argument type="service" id="maker.generator" /> </service> <service id="maker.security_controller_builder" class="Symfony\Bundle\MakerBundle\Security\SecurityControllerBuilder"> <argument type="service" id="maker.php_compat_util" /> </service> <service id="maker.php_compat_util" class="Symfony\Bundle\MakerBundle\Util\PhpCompatUtil"> <argument type="service" id="maker.file_manager" /> </service> <service id="maker.template_component_generator" class="Symfony\Bundle\MakerBundle\Util\TemplateComponentGenerator"> <argument type="service" id="maker.php_compat_util" /> </service> </services> </container> src/Resources/doc/index.rst 0000644 00000006340 15120141001 0011677 0 ustar 00 The Symfony MakerBundle ======================= Symfony Maker helps you create empty commands, controllers, form classes, tests and more so you can forget about writing boilerplate code. This bundle is an alternative to `SensioGeneratorBundle`_ for modern Symfony applications and requires using Symfony 3.4 or newer. This bundle assumes you're using a standard Symfony 4 directory structure, but many commands can generate code into any application. Installation ------------ Run this command to install and enable this bundle in your application: .. code-block:: terminal $ composer require --dev symfony/maker-bundle Usage ----- This bundle provides several commands under the ``make:`` namespace. List them all executing this command: .. code-block:: terminal $ php bin/console list make make:command Creates a new console command class make:controller Creates a new controller class make:entity Creates a new Doctrine entity class [...] make:validator Creates a new validator and constraint class make:voter Creates a new security voter class The names of the commands are self-explanatory, but some of them include optional arguments and options. Check them out with the ``--help`` option: .. code-block:: terminal $ php bin/console make:controller --help Configuration ------------- This bundle doesn't require any configuration. But, you *can* configure the root namespace that is used to "guess" what classes you want to generate: .. code-block:: yaml # config/packages/dev/maker.yaml # create this file if you need to configure anything maker: # tell MakerBundle that all of your classes live in an # Acme namespace, instead of the default App # (e.g. Acme\Entity\Article, Acme\Command\MyCommand, etc) root_namespace: 'Acme' Creating your Own Makers ------------------------ In case your applications need to generate custom boilerplate code, you can create your own ``make:...`` command reusing the tools provided by this bundle. To do that, you should create a class that extends `AbstractMaker`_ in your ``src/Maker/`` directory. And this is really it! For examples of how to complete your new maker command, see the `core maker commands`_. Make sure your class is registered as a service and tagged with ``maker.command``. If you're using the standard Symfony ``services.yaml`` configuration, this will be done automatically. Overriding the Generated Code ----------------------------- Generated code can never be perfect for everyone. The MakerBundle tries to balance adding "extension points" with keeping the library simple so that existing commands can be improved and new commands can be added. For that reason, in general, the generated code cannot be modified. In many cases, adding your *own* maker command is so easy, that we recommend that. However, if there is some extension point that you'd like, please open an issue so we can discuss! .. _`SensioGeneratorBundle`: https://github.com/sensiolabs/SensioGeneratorBundle .. _`AbstractMaker`: https://github.com/symfony/maker-bundle/blob/main/src/Maker/AbstractMaker.php .. _`core maker commands`: https://github.com/symfony/maker-bundle/tree/main/src/Maker src/Resources/help/MakeAuth.txt 0000644 00000000553 15120141001 0012461 0 ustar 00 The <info>%command.name%</info> command generates various authentication systems, by asking questions. It can provide an empty authenticator, or a full login form authentication process. In both cases it also updates your <info>security.yaml</info>. For the login form, it also generates a controller and the twig template. <info>php %command.full_name%</info> src/Resources/help/MakeCommand.txt 0000644 00000000316 15120141001 0013133 0 ustar 00 The <info>%command.name%</info> command generates a new command: <info>php %command.full_name% app:do-something</info> If the argument is missing, the command will ask for the command name interactively. src/Resources/help/MakeController.txt 0000644 00000000547 15120141001 0013706 0 ustar 00 The <info>%command.name%</info> command generates a new controller class. <info>php %command.full_name% CoolStuffController</info> If the argument is missing, the command will ask for the controller class name interactively. You can also generate the controller alone, without template with this option: <info>php %command.full_name% --no-template</info> src/Resources/help/MakeCrud.txt 0000644 00000000357 15120141001 0012457 0 ustar 00 The <info>%command.name%</info> command generates crud controller with templates for selected entity. <info>php %command.full_name% BlogPost</info> If the argument is missing, the command will ask for the entity class name interactively. src/Resources/help/MakeDockerDatabase.txt 0000644 00000000265 15120141001 0014414 0 ustar 00 The <info>%command.name%</info> command generates or updates databases services in docker-compose.yaml <info>php %command.full_name%</info> Supports MySQL, MariaDB and PostgreSQL src/Resources/help/MakeEntity.txt 0000644 00000001503 15120141001 0013030 0 ustar 00 The <info>%command.name%</info> command creates or updates an entity and repository class. <info>php %command.full_name% BlogPost</info> If the argument is missing, the command will ask for the entity class name interactively. You can also mark this class as an API Platform resource. A hypermedia CRUD API will automatically be available for this entity class: <info>php %command.full_name% --api-resource</info> Symfony can also broadcast all changes made to the entity to the client using Symfony UX Turbo. <info>php %command.full_name% --broadcast</info> You can also generate all the getter/setter/adder/remover methods for the properties of existing entities: <info>php %command.full_name% --regenerate</info> You can also *overwrite* any existing methods: <info>php %command.full_name% --regenerate --overwrite</info> src/Resources/help/MakeFixture.txt 0000644 00000000320 15120141001 0013176 0 ustar 00 The <info>%command.name%</info> command generates a new Doctrine fixtures class. <info>php %command.full_name% AppFixtures</info> If the argument is missing, the command will ask for a class interactively. src/Resources/help/MakeForm.txt 0000644 00000001107 15120141001 0012457 0 ustar 00 The <info>%command.name%</info> command generates a new form class. <info>php %command.full_name% UserType</info> If the argument is missing, the command will ask for the form class interactively. You can optionally specify the bound class in a second argument. This can be the name of an entity like <info>User</info> <info>php %command.full_name% UserType User</info> You can also specify a fully qualified name to another class like <info>\App\Dto\UserData</info>. Slashes must be escaped in the argument. <info>php %command.full_name% UserType \\App\\Dto\\UserData</info> src/Resources/help/MakeFunctionalTest.txt 0000644 00000000337 15120141001 0014522 0 ustar 00 The <info>%command.name%</info> command generates a new functional test class. <info>php %command.full_name% DefaultControllerTest</info> If the argument is missing, the command will ask for the class name interactively. src/Resources/help/MakeMessage.txt 0000644 00000000333 15120141001 0013140 0 ustar 00 The <info>%command.name%</info> command generates a new message class & handler. <info>php %command.full_name% EmailMessage</info> If the argument is missing, the command will ask for the message class interactively. src/Resources/help/MakeMiddleware.txt 0000644 00000000330 15120141001 0013626 0 ustar 00 The <info>%command.name%</info> command generates a new Middleware class. <info>php %command.full_name% CustomMiddleware</info> If the argument is missing, the command will ask for the message class interactively. src/Resources/help/MakeMigration.txt 0000644 00000000151 15120141001 0013503 0 ustar 00 The <info>%command.name%</info> command generates a new migration: <info>php %command.full_name%</info> src/Resources/help/MakeRegistrationForm.txt 0000644 00000000331 15120141001 0015050 0 ustar 00 The <info>%command.name%</info> command generates a complete registration form, controller & template. <info>php %command.full_name%</info> The command will ask for several pieces of information to build your form. src/Resources/help/MakeResetPassword.txt 0000644 00000001637 15120141001 0014371 0 ustar 00 The <info>%command.name%</info> command generates all the files needed to implement a fully-functional & secure password reset system. The SymfonycastsResetPasswordBundle is required and can be added using composer: <info>composer require symfonycasts/reset-password-bundle</info> For more information on the <info>reset-password-bundle</info> check out: <href=https://github.com/symfonycasts/reset-password-bundle>https://github.com/symfonycasts/reset-password-bundle</> <info>%command.name%</info> requires a user entity with an email property, email getter method, and a password setter method. Maker will ask for these interactively if they cannot be guessed. Maker will also update your <info>reset-password.yaml</info> configuration file if one exists. If you have customized the configuration file, maker will attempt to modify it accordingly but preserve your customizations. <info>php %command.full_name%</info> src/Resources/help/MakeSerializerEncoder.txt 0000644 00000000330 15120141001 0015162 0 ustar 00 The <info>%command.name%</info> command generates a new serializer encoder class. <info>php %command.full_name% YamlEncoder</info> If the argument is missing, the command will ask for the class name interactively. src/Resources/help/MakeSerializerNormalizer.txt 0000644 00000000336 15120141001 0015733 0 ustar 00 The <info>%command.name%</info> command generates a new serializer normalizer class. <info>php %command.full_name% UserNormalizer</info> If the argument is missing, the command will ask for the class name interactively. src/Resources/help/MakeStimulusController.txt 0000644 00000000317 15120141001 0015447 0 ustar 00 The <info>%command.name%</info> command generates new Stimulus Controller. <info>php %command.full_name% hello</info> If the argument is missing, the command will ask for the controller name interactively. src/Resources/help/MakeSubscriber.txt 0000644 00000000336 15120141001 0013662 0 ustar 00 The <info>%command.name%</info> command generates a new event subscriber class. <info>php %command.full_name% ExceptionSubscriber</info> If the argument is missing, the command will ask for the class name interactively. src/Resources/help/MakeTest.txt 0000644 00000000464 15120141001 0012500 0 ustar 00 The <info>%command.name%</info> command generates a new test class. <info>php %command.full_name% TestCase BlogPostTest</info> If the first argument is missing, the command will ask for the test type interactively. If the second argument is missing, the command will ask for the class name interactively. src/Resources/help/MakeTwigExtension.txt 0000644 00000000325 15120141001 0014364 0 ustar 00 The <info>%command.name%</info> command generates a new twig extension class. <info>php %command.full_name% AppExtension</info> If the argument is missing, the command will ask for the class name interactively. src/Resources/help/MakeUnitTest.txt 0000644 00000000314 15120141001 0013332 0 ustar 00 The <info>%command.name%</info> command generates a new unit test class. <info>php %command.full_name% UtilTest</info> If the argument is missing, the command will ask for the class name interactively. src/Resources/help/MakeUser.txt 0000644 00000000517 15120141001 0012476 0 ustar 00 The <info>%command.name%</info> command generates a new user class for security and updates your security.yaml file for it. It will also generate a user provider class if your situation needs a custom class. <info>php %command.full_name% User</info> If the argument is missing, the command will ask for the class name interactively. src/Resources/help/MakeValidator.txt 0000644 00000000345 15120141001 0013504 0 ustar 00 The <info>%command.name%</info> command generates a new validation constraint. <info>php %command.full_name% EnabledValidator</info> If the argument is missing, the command will ask for the constraint class name interactively. src/Resources/help/MakeVoter.txt 0000644 00000000320 15120141001 0012647 0 ustar 00 The <info>%command.name%</info> command generates a new security voter. <info>php %command.full_name% BlogPostVoter</info> If the argument is missing, the command will ask for the class name interactively. src/Resources/skeleton/authenticator/EmptyAuthenticator.tpl.php 0000644 00000002511 15120141001 0021123 0 ustar 00 <?= "<?php\n" ?> namespace <?= $namespace ?>; <?= $use_statements; ?> class <?= $class_name ?> extends AbstractAuthenticator { public function supports(Request $request): ?bool { // TODO: Implement supports() method. } public function authenticate(Request $request): Passport { // TODO: Implement authenticate() method. } public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response { // TODO: Implement onAuthenticationSuccess() method. } public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response { // TODO: Implement onAuthenticationFailure() method. } // public function start(Request $request, AuthenticationException $authException = null): Response // { // /* // * If you would like this class to control what happens when an anonymous user accesses a // * protected page (e.g. redirect to /login), uncomment this method and make this class // * implement Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface. // * // * For more details, see https://symfony.com/doc/current/security/experimental_authenticators.html#configuring-the-authentication-entry-point // */ // } } src/Resources/skeleton/authenticator/EmptySecurityController.tpl.php 0000644 00000000202 15120141001 0022157 0 ustar 00 <?= "<?php\n" ?> namespace <?= $namespace ?>; <?= $use_statements; ?> class <?= $class_name; ?> extends AbstractController { } src/Resources/skeleton/authenticator/LoginFormAuthenticator.tpl.php 0000644 00000003072 15120141001 0021724 0 ustar 00 <?= "<?php\n" ?> namespace <?= $namespace ?>; <?= $use_statements; ?> class <?= $class_name; ?> extends AbstractLoginFormAuthenticator { use TargetPathTrait; public const LOGIN_ROUTE = 'app_login'; private <?= $use_typed_properties ? 'UrlGeneratorInterface ' : null ?>$urlGenerator; public function __construct(UrlGeneratorInterface $urlGenerator) { $this->urlGenerator = $urlGenerator; } public function authenticate(Request $request): Passport { $<?= $username_field_var ?> = $request->request->get('<?= $username_field ?>', ''); $request->getSession()->set(Security::LAST_USERNAME, $<?= $username_field_var ?>); return new Passport( new UserBadge($<?= $username_field_var ?>), new PasswordCredentials($request->request->get('password', '')), [ new CsrfTokenBadge('authenticate', $request->request->get('_csrf_token')), ] ); } public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response { if ($targetPath = $this->getTargetPath($request->getSession(), $firewallName)) { return new RedirectResponse($targetPath); } // For example: // return new RedirectResponse($this->urlGenerator->generate('some_route')); throw new \Exception('TODO: provide a valid redirect inside '.__FILE__); } protected function getLoginUrl(Request $request): string { return $this->urlGenerator->generate(self::LOGIN_ROUTE); } } src/Resources/skeleton/authenticator/login_form.tpl.php 0000644 00000003203 15120141001 0017424 0 ustar 00 {% extends 'base.html.twig' %} {% block title %}Log in!{% endblock %} {% block body %} <form method="post"> {% if error %} <div class="alert alert-danger">{{ error.messageKey|trans(error.messageData, 'security') }}</div> {% endif %} <?php if ($logout_setup): ?> {% if app.user %} <div class="mb-3"> You are logged in as {{ app.user.userIdentifier }}, <a href="{{ path('app_logout') }}">Logout</a> </div> {% endif %} <?php endif; ?> <h1 class="h3 mb-3 font-weight-normal">Please sign in</h1> <label for="input<?= ucfirst($username_field); ?>"><?= $username_label; ?></label> <input type="<?= $username_is_email ? 'email' : 'text'; ?>" value="{{ last_username }}" name="<?= $username_field; ?>" id="input<?= ucfirst($username_field); ?>" class="form-control" autocomplete="<?= $username_is_email ? 'email' : 'username'; ?>" required autofocus> <label for="inputPassword">Password</label> <input type="password" name="password" id="inputPassword" class="form-control" autocomplete="current-password" required> <input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}" > {# Uncomment this section and add a remember_me option below your firewall to activate remember me functionality. See https://symfony.com/doc/current/security/remember_me.html <div class="checkbox mb-3"> <label> <input type="checkbox" name="_remember_me"> Remember me </label> </div> #} <button class="btn btn-lg btn-primary" type="submit"> Sign in </button> </form> {% endblock %} src/Resources/skeleton/command/Command.tpl.php 0000644 00000002452 15120141001 0015420 0 ustar 00 <?= "<?php\n"; ?> namespace <?= $namespace; ?>; <?= $use_statements; ?> <?php if ($use_attributes): ?> #[AsCommand( name: '<?= $command_name; ?>', description: 'Add a short description for your command', )] <?php endif; ?> class <?= $class_name; ?> extends Command { <?php if (!$use_attributes): ?> protected static $defaultName = '<?= $command_name; ?>'; protected static $defaultDescription = 'Add a short description for your command'; <?php endif; ?> protected function configure(): void { $this <?= $set_description ? " ->setDescription(self::\$defaultDescription)\n" : '' ?> ->addArgument('arg1', InputArgument::OPTIONAL, 'Argument description') ->addOption('option1', null, InputOption::VALUE_NONE, 'Option description') ; } protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $arg1 = $input->getArgument('arg1'); if ($arg1) { $io->note(sprintf('You passed an argument: %s', $arg1)); } if ($input->getOption('option1')) { // ... } $io->success('You have a new command! Now make it your own! Pass --help to see your options.'); return Command::SUCCESS; } } src/Resources/skeleton/controller/Controller.tpl.php 0000644 00000001226 15120141001 0016730 0 ustar 00 <?= "<?php\n" ?> namespace <?= $namespace; ?>; <?= $use_statements; ?> class <?= $class_name; ?> extends AbstractController { <?= $generator->generateRouteForControllerMethod($route_path, $route_name); ?> public function index(): <?php if ($with_template) { ?>Response<?php } else { ?>JsonResponse<?php } ?> { <?php if ($with_template) { ?> return $this->render('<?= $template_name ?>', [ 'controller_name' => '<?= $class_name ?>', ]); <?php } else { ?> return $this->json([ 'message' => 'Welcome to your new controller!', 'path' => '<?= $relative_path; ?>', ]); <?php } ?> } } src/Resources/skeleton/controller/twig_template.tpl.php 0000644 00000001253 15120141001 0017452 0 ustar 00 <?= $helper->getHeadPrintCode("Hello $class_name!"); ?> {% block body %} <style> .example-wrapper { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; } .example-wrapper code { background: #F5F5F5; padding: 2px 6px; } </style> <div class="example-wrapper"> <h1>Hello {{ controller_name }}! ✅</h1> This friendly message is coming from: <ul> <li>Your controller at <code><?= $helper->getFileLink("$root_directory/$controller_path", "$controller_path"); ?></code></li> <li>Your template at <code><?= $helper->getFileLink("$root_directory/$relative_path", "$relative_path"); ?></code></li> </ul> </div> {% endblock %} src/Resources/skeleton/crud/controller/Controller.tpl.php 0000644 00000014610 15120141001 0017666 0 ustar 00 <?= "<?php\n" ?> namespace <?= $namespace ?>; <?= $use_statements; ?> <?php if ($use_attributes) { ?> #[Route('<?= $route_path ?>')] <?php } else { ?> /** * @Route("<?= $route_path ?>") */ <?php } ?> class <?= $class_name ?> extends AbstractController { <?= $generator->generateRouteForControllerMethod('/', sprintf('%s_index', $route_name), ['GET']) ?> <?php if (isset($repository_full_class_name)): ?> public function index(<?= $repository_class_name ?> $<?= $repository_var ?>): Response { return $this->render('<?= $templates_path ?>/index.html.twig', [ '<?= $entity_twig_var_plural ?>' => $<?= $repository_var ?>->findAll(), ]); } <?php else: ?> public function index(EntityManagerInterface $entityManager): Response { $<?= $entity_var_plural ?> = $entityManager ->getRepository(<?= $entity_class_name ?>::class) ->findAll(); return $this->render('<?= $templates_path ?>/index.html.twig', [ '<?= $entity_twig_var_plural ?>' => $<?= $entity_var_plural ?>, ]); } <?php endif ?> <?= $generator->generateRouteForControllerMethod('/new', sprintf('%s_new', $route_name), ['GET', 'POST']) ?> <?php if (isset($repository_full_class_name) && $generator->repositoryHasAddRemoveMethods($repository_full_class_name)) { ?> public function new(Request $request, <?= $repository_class_name ?> $<?= $repository_var ?>): Response <?php } else { ?> public function new(Request $request, EntityManagerInterface $entityManager): Response <?php } ?> { $<?= $entity_var_singular ?> = new <?= $entity_class_name ?>(); $form = $this->createForm(<?= $form_class_name ?>::class, $<?= $entity_var_singular ?>); $form->handleRequest($request); <?php if (isset($repository_full_class_name) && $generator->repositoryHasAddRemoveMethods($repository_full_class_name)) { ?> if ($form->isSubmitted() && $form->isValid()) { $<?= $repository_var ?>->add($<?= $entity_var_singular ?>, true); return $this->redirectToRoute('<?= $route_name ?>_index', [], Response::HTTP_SEE_OTHER); } <?php } else { ?> if ($form->isSubmitted() && $form->isValid()) { $entityManager->persist($<?= $entity_var_singular ?>); $entityManager->flush(); return $this->redirectToRoute('<?= $route_name ?>_index', [], Response::HTTP_SEE_OTHER); } <?php } ?> <?php if ($use_render_form) { ?> return $this->renderForm('<?= $templates_path ?>/new.html.twig', [ '<?= $entity_twig_var_singular ?>' => $<?= $entity_var_singular ?>, 'form' => $form, ]); <?php } else { ?> return $this->render('<?= $templates_path ?>/new.html.twig', [ '<?= $entity_twig_var_singular ?>' => $<?= $entity_var_singular ?>, 'form' => $form->createView(), ]); <?php } ?> } <?= $generator->generateRouteForControllerMethod(sprintf('/{%s}', $entity_identifier), sprintf('%s_show', $route_name), ['GET']) ?> public function show(<?= $entity_class_name ?> $<?= $entity_var_singular ?>): Response { return $this->render('<?= $templates_path ?>/show.html.twig', [ '<?= $entity_twig_var_singular ?>' => $<?= $entity_var_singular ?>, ]); } <?= $generator->generateRouteForControllerMethod(sprintf('/{%s}/edit', $entity_identifier), sprintf('%s_edit', $route_name), ['GET', 'POST']) ?> <?php if (isset($repository_full_class_name) && $generator->repositoryHasAddRemoveMethods($repository_full_class_name)) { ?> public function edit(Request $request, <?= $entity_class_name ?> $<?= $entity_var_singular ?>, <?= $repository_class_name ?> $<?= $repository_var ?>): Response <?php } else { ?> public function edit(Request $request, <?= $entity_class_name ?> $<?= $entity_var_singular ?>, EntityManagerInterface $entityManager): Response <?php } ?> { $form = $this->createForm(<?= $form_class_name ?>::class, $<?= $entity_var_singular ?>); $form->handleRequest($request); <?php if (isset($repository_full_class_name) && $generator->repositoryHasAddRemoveMethods($repository_full_class_name)) { ?> if ($form->isSubmitted() && $form->isValid()) { $<?= $repository_var ?>->add($<?= $entity_var_singular ?>, true); return $this->redirectToRoute('<?= $route_name ?>_index', [], Response::HTTP_SEE_OTHER); } <?php } else { ?> if ($form->isSubmitted() && $form->isValid()) { $entityManager->flush(); return $this->redirectToRoute('<?= $route_name ?>_index', [], Response::HTTP_SEE_OTHER); } <?php } ?> <?php if ($use_render_form) { ?> return $this->renderForm('<?= $templates_path ?>/edit.html.twig', [ '<?= $entity_twig_var_singular ?>' => $<?= $entity_var_singular ?>, 'form' => $form, ]); <?php } else { ?> return $this->render('<?= $templates_path ?>/edit.html.twig', [ '<?= $entity_twig_var_singular ?>' => $<?= $entity_var_singular ?>, 'form' => $form->createView(), ]); <?php } ?> } <?= $generator->generateRouteForControllerMethod(sprintf('/{%s}', $entity_identifier), sprintf('%s_delete', $route_name), ['POST']) ?> <?php if (isset($repository_full_class_name) && $generator->repositoryHasAddRemoveMethods($repository_full_class_name)) { ?> public function delete(Request $request, <?= $entity_class_name ?> $<?= $entity_var_singular ?>, <?= $repository_class_name ?> $<?= $repository_var ?>): Response <?php } else { ?> public function delete(Request $request, <?= $entity_class_name ?> $<?= $entity_var_singular ?>, EntityManagerInterface $entityManager): Response <?php } ?> { <?php if (isset($repository_full_class_name) && $generator->repositoryHasAddRemoveMethods($repository_full_class_name)) { ?> if ($this->isCsrfTokenValid('delete'.$<?= $entity_var_singular ?>->get<?= ucfirst($entity_identifier) ?>(), $request->request->get('_token'))) { $<?= $repository_var ?>->remove($<?= $entity_var_singular ?>, true); } <?php } else { ?> if ($this->isCsrfTokenValid('delete'.$<?= $entity_var_singular ?>->get<?= ucfirst($entity_identifier) ?>(), $request->request->get('_token'))) { $entityManager->remove($<?= $entity_var_singular ?>); $entityManager->flush(); } <?php } ?> return $this->redirectToRoute('<?= $route_name ?>_index', [], Response::HTTP_SEE_OTHER); } } src/Resources/skeleton/crud/templates/_delete_form.tpl.php 0000644 00000000642 15120141001 0020002 0 ustar 00 <form method="post" action="{{ path('<?= $route_name ?>_delete', {'<?= $entity_identifier ?>': <?= $entity_twig_var_singular ?>.<?= $entity_identifier ?>}) }}" onsubmit="return confirm('Are you sure you want to delete this item?');"> <input type="hidden" name="_token" value="{{ csrf_token('delete' ~ <?= $entity_twig_var_singular ?>.<?= $entity_identifier ?>) }}"> <button class="btn">Delete</button> </form> src/Resources/skeleton/crud/templates/_form.tpl.php 0000644 00000000214 15120141001 0016453 0 ustar 00 {{ form_start(form) }} {{ form_widget(form) }} <button class="btn">{{ button_label|default('Save') }}</button> {{ form_end(form) }} src/Resources/skeleton/crud/templates/edit.tpl.php 0000644 00000000556 15120141001 0016307 0 ustar 00 <?= $helper->getHeadPrintCode('Edit '.$entity_class_name) ?> {% block body %} <h1>Edit <?= $entity_class_name ?></h1> {{ include('<?= $templates_path ?>/_form.html.twig', {'button_label': 'Update'}) }} <a href="{{ path('<?= $route_name ?>_index') }}">back to list</a> {{ include('<?= $templates_path ?>/_delete_form.html.twig') }} {% endblock %} src/Resources/skeleton/crud/templates/index.tpl.php 0000644 00000002476 15120141001 0016474 0 ustar 00 <?= $helper->getHeadPrintCode($entity_class_name.' index'); ?> {% block body %} <h1><?= $entity_class_name ?> index</h1> <table class="table"> <thead> <tr> <?php foreach ($entity_fields as $field): ?> <th><?= ucfirst($field['fieldName']) ?></th> <?php endforeach; ?> <th>actions</th> </tr> </thead> <tbody> {% for <?= $entity_twig_var_singular ?> in <?= $entity_twig_var_plural ?> %} <tr> <?php foreach ($entity_fields as $field): ?> <td>{{ <?= $helper->getEntityFieldPrintCode($entity_twig_var_singular, $field) ?> }}</td> <?php endforeach; ?> <td> <a href="{{ path('<?= $route_name ?>_show', {'<?= $entity_identifier ?>': <?= $entity_twig_var_singular ?>.<?= $entity_identifier ?>}) }}">show</a> <a href="{{ path('<?= $route_name ?>_edit', {'<?= $entity_identifier ?>': <?= $entity_twig_var_singular ?>.<?= $entity_identifier ?>}) }}">edit</a> </td> </tr> {% else %} <tr> <td colspan="<?= (count($entity_fields) + 1) ?>">no records found</td> </tr> {% endfor %} </tbody> </table> <a href="{{ path('<?= $route_name ?>_new') }}">Create new</a> {% endblock %} src/Resources/skeleton/crud/templates/new.tpl.php 0000644 00000000423 15120141001 0016144 0 ustar 00 <?= $helper->getHeadPrintCode('New '.$entity_class_name) ?> {% block body %} <h1>Create new <?= $entity_class_name ?></h1> {{ include('<?= $templates_path ?>/_form.html.twig') }} <a href="{{ path('<?= $route_name ?>_index') }}">back to list</a> {% endblock %} src/Resources/skeleton/crud/templates/show.tpl.php 0000644 00000001366 15120141001 0016342 0 ustar 00 <?= $helper->getHeadPrintCode($entity_class_name) ?> {% block body %} <h1><?= $entity_class_name ?></h1> <table class="table"> <tbody> <?php foreach ($entity_fields as $field): ?> <tr> <th><?= ucfirst($field['fieldName']) ?></th> <td>{{ <?= $helper->getEntityFieldPrintCode($entity_twig_var_singular, $field) ?> }}</td> </tr> <?php endforeach; ?> </tbody> </table> <a href="{{ path('<?= $route_name ?>_index') }}">back to list</a> <a href="{{ path('<?= $route_name ?>_edit', {'<?= $entity_identifier ?>': <?= $entity_twig_var_singular ?>.<?= $entity_identifier ?>}) }}">edit</a> {{ include('<?= $templates_path ?>/_delete_form.html.twig') }} {% endblock %} src/Resources/skeleton/crud/test/Test.EntityManager.tpl.php 0000644 00000010366 15120141002 0020031 0 ustar 00 <?= "<?php\n" ?> <?php use Symfony\Bundle\MakerBundle\Str; ?> namespace <?= $namespace ?>; <?= $use_statements; ?> class <?= $class_name ?> extends WebTestCase<?= "\n" ?> { <?= $use_typed_properties ? null : " /** @var KernelBrowser */\n" ?> private <?= $use_typed_properties ? 'KernelBrowser ' : null ?>$client; <?= $use_typed_properties ? null : " /** @var EntityManagerInterface */\n" ?> private <?= $use_typed_properties ? 'EntityManagerInterface ' : null ?>$manager; <?= $use_typed_properties ? null : " /** @var EntityRepository */\n" ?> private <?= $use_typed_properties ? 'EntityRepository ' : null ?>$repository; private <?= $use_typed_properties ? 'string ' : null ?>$path = '<?= $route_path; ?>/'; protected function setUp(): void { $this->client = static::createClient(); $this->manager = (static::getContainer()->get('doctrine'))->getManager(); $this->repository = $this->manager->getRepository(<?= $entity_class_name; ?>::class); foreach ($this->repository->findAll() as $object) { $this->manager->remove($object); } $this->manager->flush(); } public function testIndex(): void { $crawler = $this->client->request('GET', $this->path); self::assertResponseStatusCodeSame(200); self::assertPageTitleContains('<?= ucfirst($entity_var_singular); ?> index'); // Use the $crawler to perform additional assertions e.g. // self::assertSame('Some text on the page', $crawler->filter('.p')->first()); } public function testNew(): void { $this->markTestIncomplete(); $this->client->request('GET', sprintf('%snew', $this->path)); self::assertResponseStatusCodeSame(200); $this->client->submitForm('Save', [ <?php foreach ($form_fields as $form_field => $typeOptions): ?> '<?= $form_field_prefix; ?>[<?= $form_field; ?>]' => 'Testing', <?php endforeach; ?> ]); self::assertResponseRedirects('/sweet/food/'); self::assertSame(1, $this->getRepository()->count([])); } public function testShow(): void { $this->markTestIncomplete(); $fixture = new <?= $entity_class_name; ?>(); <?php foreach ($form_fields as $form_field => $typeOptions): ?> $fixture->set<?= ucfirst($form_field); ?>('My Title'); <?php endforeach; ?> $this->repository->add($fixture, true); $this->client->request('GET', sprintf('%s%s', $this->path, $fixture->getId())); self::assertResponseStatusCodeSame(200); self::assertPageTitleContains('<?= ucfirst($entity_var_singular); ?>'); // Use assertions to check that the properties are properly displayed. } public function testEdit(): void { $this->markTestIncomplete(); $fixture = new <?= $entity_class_name; ?>(); <?php foreach ($form_fields as $form_field => $typeOptions): ?> $fixture->set<?= ucfirst($form_field); ?>('Value'); <?php endforeach; ?> $this->manager->persist($fixture); $this->manager->flush(); $this->client->request('GET', sprintf('%s%s/edit', $this->path, $fixture->getId())); $this->client->submitForm('Update', [ <?php foreach ($form_fields as $form_field => $typeOptions): ?> '<?= $form_field_prefix; ?>[<?= $form_field; ?>]' => 'Something New', <?php endforeach; ?> ]); self::assertResponseRedirects('<?= $route_path; ?>/'); $fixture = $this->repository->findAll(); <?php foreach ($form_fields as $form_field => $typeOptions): ?> self::assertSame('Something New', $fixture[0]->get<?= ucfirst($form_field); ?>()); <?php endforeach; ?> } public function testRemove(): void { $this->markTestIncomplete(); $fixture = new <?= $entity_class_name; ?>(); <?php foreach ($form_fields as $form_field => $typeOptions): ?> $fixture->set<?= ucfirst($form_field); ?>('Value'); <?php endforeach; ?> $$this->manager->remove($fixture); $this->manager->flush(); $this->client->request('GET', sprintf('%s%s', $this->path, $fixture->getId())); $this->client->submitForm('Delete'); self::assertResponseRedirects('<?= $route_path; ?>/'); self::assertSame(0, $this->repository->count([])); } } src/Resources/skeleton/crud/test/Test.tpl.php 0000644 00000010461 15120141002 0015257 0 ustar 00 <?= "<?php\n" ?> <?php use Symfony\Bundle\MakerBundle\Str; ?> namespace <?= $namespace ?>; <?= $use_statements; ?> class <?= $class_name ?> extends WebTestCase<?= "\n" ?> { <?= $use_typed_properties ? null : " /** @var KernelBrowser */\n" ?> private <?= $use_typed_properties ? 'KernelBrowser ' : null ?>$client; <?= $use_typed_properties ? null : " /** @var $repository_class_name */\n" ?> private <?= $use_typed_properties ? "$repository_class_name " : null ?>$repository; private <?= $use_typed_properties ? 'string ' : null ?>$path = '<?= $route_path; ?>/'; protected function setUp(): void { $this->client = static::createClient(); $this->repository = (static::getContainer()->get('doctrine'))->getRepository(<?= $entity_class_name; ?>::class); foreach ($this->repository->findAll() as $object) { $this->repository->remove($object, true); } } public function testIndex(): void { $crawler = $this->client->request('GET', $this->path); self::assertResponseStatusCodeSame(200); self::assertPageTitleContains('<?= ucfirst($entity_var_singular); ?> index'); // Use the $crawler to perform additional assertions e.g. // self::assertSame('Some text on the page', $crawler->filter('.p')->first()); } public function testNew(): void { $originalNumObjectsInRepository = count($this->repository->findAll()); $this->markTestIncomplete(); $this->client->request('GET', sprintf('%snew', $this->path)); self::assertResponseStatusCodeSame(200); $this->client->submitForm('Save', [ <?php foreach ($form_fields as $form_field => $typeOptions): ?> '<?= $form_field_prefix; ?>[<?= $form_field; ?>]' => 'Testing', <?php endforeach; ?> ]); self::assertResponseRedirects('<?= $route_path; ?>/'); self::assertSame($originalNumObjectsInRepository + 1, count($this->repository->findAll())); } public function testShow(): void { $this->markTestIncomplete(); $fixture = new <?= $entity_class_name; ?>(); <?php foreach ($form_fields as $form_field => $typeOptions): ?> $fixture->set<?= ucfirst($form_field); ?>('My Title'); <?php endforeach; ?> $this->repository->add($fixture, true); $this->client->request('GET', sprintf('%s%s', $this->path, $fixture->getId())); self::assertResponseStatusCodeSame(200); self::assertPageTitleContains('<?= ucfirst($entity_var_singular); ?>'); // Use assertions to check that the properties are properly displayed. } public function testEdit(): void { $this->markTestIncomplete(); $fixture = new <?= $entity_class_name; ?>(); <?php foreach ($form_fields as $form_field => $typeOptions): ?> $fixture->set<?= ucfirst($form_field); ?>('My Title'); <?php endforeach; ?> $this->repository->add($fixture, true); $this->client->request('GET', sprintf('%s%s/edit', $this->path, $fixture->getId())); $this->client->submitForm('Update', [ <?php foreach ($form_fields as $form_field => $typeOptions): ?> '<?= $form_field_prefix; ?>[<?= $form_field; ?>]' => 'Something New', <?php endforeach; ?> ]); self::assertResponseRedirects('<?= $route_path; ?>/'); $fixture = $this->repository->findAll(); <?php foreach ($form_fields as $form_field => $typeOptions): ?> self::assertSame('Something New', $fixture[0]->get<?= ucfirst($form_field); ?>()); <?php endforeach; ?> } public function testRemove(): void { $this->markTestIncomplete(); $originalNumObjectsInRepository = count($this->repository->findAll()); $fixture = new <?= $entity_class_name; ?>(); <?php foreach ($form_fields as $form_field => $typeOptions): ?> $fixture->set<?= ucfirst($form_field); ?>('My Title'); <?php endforeach; ?> $this->repository->add($fixture, true); self::assertSame($originalNumObjectsInRepository + 1, count($this->repository->findAll())); $this->client->request('GET', sprintf('%s%s', $this->path, $fixture->getId())); $this->client->submitForm('Delete'); self::assertSame($originalNumObjectsInRepository, count($this->repository->findAll())); self::assertResponseRedirects('<?= $route_path; ?>/'); } } src/Resources/skeleton/doctrine/Entity.tpl.php 0000644 00000002275 15120141002 0015513 0 ustar 00 <?= "<?php\n" ?> namespace <?= $namespace ?>; <?= $use_statements; ?> <?php if (!$use_attributes || !$doctrine_use_attributes): ?> /** <?php if ($api_resource && !$use_attributes): ?> * @ApiResource() <?php endif ?> <?php if ($broadcast && !$use_attributes): ?> * @Broadcast() <?php endif ?> * @ORM\Entity(repositoryClass=<?= $repository_class_name ?>::class) <?php if ($should_escape_table_name): ?> * @ORM\Table(name="`<?= $table_name ?>`") <?php endif ?> */ <?php endif ?> <?php if ($doctrine_use_attributes): ?> #[ORM\Entity(repositoryClass: <?= $repository_class_name ?>::class)] <?php if ($should_escape_table_name): ?>#[ORM\Table(name: '`<?= $table_name ?>`')] <?php endif ?> <?php endif?> <?php if ($api_resource && $use_attributes): ?> #[ApiResource] <?php endif ?> <?php if ($broadcast && $use_attributes): ?> #[Broadcast] <?php endif ?> class <?= $class_name."\n" ?> { <?php if (!$doctrine_use_attributes): ?>/** * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ <?php else: ?>#[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column(type: 'integer')] <?php endif ?>private $id; public function getId(): ?int { return $this->id; } } src/Resources/skeleton/doctrine/Fixtures.tpl.php 0000644 00000000532 15120141002 0016042 0 ustar 00 <?= "<?php\n" ?> namespace <?= $namespace; ?>; use Doctrine\Bundle\FixturesBundle\Fixture; use <?= $object_manager_class; ?>; class <?= $class_name ?> extends Fixture { public function load(ObjectManager $manager): void { // $product = new Product(); // $manager->persist($product); $manager->flush(); } } src/Resources/skeleton/doctrine/Repository.tpl.php 0000644 00000005600 15120141002 0016411 0 ustar 00 <?= "<?php\n"; ?> namespace <?= $namespace; ?>; <?= $use_statements; ?> /** * @extends ServiceEntityRepository<<?= $entity_class_name; ?>> * * @method <?= $entity_class_name; ?>|null find($id, $lockMode = null, $lockVersion = null) * @method <?= $entity_class_name; ?>|null findOneBy(array $criteria, array $orderBy = null) * @method <?= $entity_class_name; ?>[] findAll() * @method <?= $entity_class_name; ?>[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) */ class <?= $class_name; ?> extends ServiceEntityRepository<?= $with_password_upgrade ? " implements PasswordUpgraderInterface\n" : "\n" ?> { public function __construct(ManagerRegistry $registry) { parent::__construct($registry, <?= $entity_class_name; ?>::class); } public function add(<?= $entity_class_name ?> $entity, bool $flush = false): void { $this->getEntityManager()->persist($entity); if ($flush) { $this->getEntityManager()->flush(); } } public function remove(<?= $entity_class_name ?> $entity, bool $flush = false): void { $this->getEntityManager()->remove($entity); if ($flush) { $this->getEntityManager()->flush(); } } <?php if ($include_example_comments): // When adding a new method without existing default comments, the blank line is automatically added.?> <?php endif; ?> <?php if ($with_password_upgrade): ?> /** * Used to upgrade (rehash) the user's password automatically over time. */ public function upgradePassword(<?= sprintf('%s ', $password_upgrade_user_interface->getShortName()); ?>$user, string $newHashedPassword): void { if (!$user instanceof <?= $entity_class_name ?>) { throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', \get_class($user))); } $user->setPassword($newHashedPassword); $this->add($user, true); } <?php endif ?> <?php if ($include_example_comments): ?> // /** // * @return <?= $entity_class_name ?>[] Returns an array of <?= $entity_class_name ?> objects // */ // public function findByExampleField($value): array // { // return $this->createQueryBuilder('<?= $entity_alias; ?>') // ->andWhere('<?= $entity_alias; ?>.exampleField = :val') // ->setParameter('val', $value) // ->orderBy('<?= $entity_alias; ?>.id', 'ASC') // ->setMaxResults(10) // ->getQuery() // ->getResult() // ; // } // public function findOneBySomeField($value): ?<?= $entity_class_name."\n" ?> // { // return $this->createQueryBuilder('<?= $entity_alias ?>') // ->andWhere('<?= $entity_alias ?>.exampleField = :val') // ->setParameter('val', $value) // ->getQuery() // ->getOneOrNullResult() // ; // } <?php endif; ?> } src/Resources/skeleton/doctrine/broadcast_twig_template.tpl.php 0000644 00000001207 15120141002 0021120 0 ustar 00 {# Learn how to use Turbo Streams: https://github.com/symfony/ux-turbo#broadcast-doctrine-entities-update #} {% block create %} <turbo-stream action="append" target="<?= $class_name_plural ?>"> <template> <div id="{{ '<?= $class_name ?>_' ~ id }}"> #{{ id }} created </div> </template> </turbo-stream> {% endblock %} {% block update %} <turbo-stream action="update" target="<?= $class_name ?>_{{ id }}"> <template> #{{ id }} updated </template> </turbo-stream> {% endblock %} {% block remove %} <turbo-stream action="remove" target="<?= $class_name ?>_{{ id }}"></turbo-stream> {% endblock %} src/Resources/skeleton/event/Subscriber.tpl.php 0000644 00000000574 15120141002 0015654 0 ustar 00 <?= "<?php\n" ?> namespace <?= $namespace; ?>; <?= $use_statements; ?> class <?= $class_name ?> implements EventSubscriberInterface { public function <?= $method_name ?>(<?= $event_arg ?>): void { // ... } public static function getSubscribedEvents(): array { return [ <?= $event ?> => '<?= $method_name ?>', ]; } } src/Resources/skeleton/form/Type.tpl.php 0000644 00000002130 15120141002 0014302 0 ustar 00 <?= "<?php\n" ?> namespace <?= $namespace ?>; <?= $use_statements; ?> class <?= $class_name ?> extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void { $builder <?php foreach ($form_fields as $form_field => $typeOptions): ?> <?php if (null === $typeOptions['type'] && !$typeOptions['options_code']): ?> ->add('<?= $form_field ?>') <?php elseif (null !== $typeOptions['type'] && !$typeOptions['options_code']): ?> ->add('<?= $form_field ?>', <?= $typeOptions['type'] ?>::class) <?php else: ?> ->add('<?= $form_field ?>', <?= $typeOptions['type'] ? ($typeOptions['type'].'::class') : 'null' ?>, [ <?= $typeOptions['options_code']."\n" ?> ]) <?php endif; ?> <?php endforeach; ?> ; } public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ <?php if ($bounded_class_name): ?> 'data_class' => <?= $bounded_class_name ?>::class, <?php else: ?> // Configure your form options here <?php endif ?> ]); } } src/Resources/skeleton/message/Message.tpl.php 0000644 00000000636 15120141002 0015437 0 ustar 00 <?= "<?php\n" ?> namespace <?= $namespace; ?>; final class <?= $class_name."\n" ?> { /* * Add whatever properties and methods you need * to hold the data for this message class. */ // private $name; // public function __construct(string $name) // { // $this->name = $name; // } // public function getName(): string // { // return $this->name; // } } src/Resources/skeleton/message/MessageHandler.tpl.php 0000644 00000000410 15120141002 0016723 0 ustar 00 <?= "<?php\n" ?> namespace <?= $namespace; ?>; <?= $use_statements; ?> final class <?= $class_name ?> implements MessageHandlerInterface { public function __invoke(<?= $message_class_name ?> $message) { // do something with your message } } src/Resources/skeleton/middleware/Middleware.tpl.php 0000644 00000000462 15120141002 0016616 0 ustar 00 <?= "<?php\n" ?> namespace <?= $namespace; ?>; <?= $use_statements; ?> final class <?= $class_name; ?> implements MiddlewareInterface { public function handle(Envelope $envelope, StackInterface $stack): Envelope { // ... return $stack->next()->handle($envelope, $stack); } } src/Resources/skeleton/registration/RegistrationController.tpl.php 0000644 00000011332 15120141002 0021652 0 ustar 00 <?= "<?php\n" ?> namespace <?= $namespace; ?>; <?= $use_statements; ?> class <?= $class_name; ?> extends AbstractController { <?php if ($will_verify_email): ?> private <?= $generator->getPropertyType($email_verifier_class_details) ?>$emailVerifier; public function __construct(<?= $email_verifier_class_details->getShortName() ?> $emailVerifier) { $this->emailVerifier = $emailVerifier; } <?php endif; ?> <?= $generator->generateRouteForControllerMethod($route_path, $route_name) ?> public function register(Request $request, <?= $password_hasher_class_details->getShortName() ?> <?= $password_hasher_variable_name ?><?= $authenticator_full_class_name ? sprintf(', %s %s, %s $authenticator', ($use_new_authenticator_system ? 'UserAuthenticatorInterface' : 'GuardAuthenticatorHandler'), ($use_new_authenticator_system ? '$userAuthenticator' : '$guardHandler'), $authenticator_class_name) : '' ?>, EntityManagerInterface $entityManager): Response { $user = new <?= $user_class_name ?>(); $form = $this->createForm(<?= $form_class_name ?>::class, $user); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { // encode the plain password $user->set<?= ucfirst($password_field) ?>( <?= $password_hasher_variable_name ?>-><?= $use_password_hasher ? 'hashPassword' : 'encodePassword' ?>( $user, $form->get('plainPassword')->getData() ) ); $entityManager->persist($user); $entityManager->flush(); <?php if ($will_verify_email): ?> // generate a signed url and email it to the user $this->emailVerifier->sendEmailConfirmation('app_verify_email', $user, (new TemplatedEmail()) ->from(new Address('<?= $from_email ?>', '<?= $from_email_name ?>')) ->to($user-><?= $email_getter ?>()) ->subject('Please Confirm your Email') ->htmlTemplate('registration/confirmation_email.html.twig') ); <?php endif; ?> // do anything else you need here, like send an email <?php if ($authenticator_full_class_name): ?> <?php if ($use_new_authenticator_system): ?> return $userAuthenticator->authenticateUser( $user, $authenticator, $request ); <?php else: ?> return $guardHandler->authenticateUserAndHandleSuccess( $user, $request, $authenticator, '<?= $firewall_name; ?>' // firewall name in security.yaml ); <?php endif; ?> <?php else: ?> return $this->redirectToRoute('<?= $redirect_route_name ?>'); <?php endif; ?> } return $this->render('registration/register.html.twig', [ 'registrationForm' => $form->createView(), ]); } <?php if ($will_verify_email): ?> <?= $generator->generateRouteForControllerMethod('/verify/email', 'app_verify_email') ?> public function verifyUserEmail(Request $request<?php if ($translator_available): ?>, TranslatorInterface $translator<?php endif ?><?= $verify_email_anonymously ? sprintf(', %s %s', $repository_class_name, $repository_var) : null ?>): Response { <?php if (!$verify_email_anonymously): ?> $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); <?php else: ?> $id = $request->get('id'); if (null === $id) { return $this->redirectToRoute('app_register'); } <?php if ('$manager' === $repository_var): ?> $repository = $manager->getRepository(<?= $user_class_name ?>::class); $user = $repository->find($id); <?php else: ?> $user = <?= $repository_var; ?>->find($id); <?php endif; ?> if (null === $user) { return $this->redirectToRoute('app_register'); } <?php endif; ?> // validate email confirmation link, sets User::isVerified=true and persists try { $this->emailVerifier->handleEmailConfirmation($request, <?= $verify_email_anonymously ? '$user' : '$this->getUser()' ?>); } catch (VerifyEmailExceptionInterface $exception) { $this->addFlash('verify_email_error', <?php if ($translator_available): ?>$translator->trans($exception->getReason(), [], 'VerifyEmailBundle')<?php else: ?>$exception->getReason()<?php endif ?>); return $this->redirectToRoute('<?= $route_name ?>'); } // @TODO Change the redirect on success and handle or remove the flash message in your templates $this->addFlash('success', 'Your email address has been verified.'); return $this->redirectToRoute('app_register'); } <?php endif; ?> } src/Resources/skeleton/registration/twig_email.tpl.php 0000644 00000000464 15120141002 0017261 0 ustar 00 <h1>Hi! Please confirm your email!</h1> <p> Please confirm your email address by clicking the following link: <br><br> <a href="{{ signedUrl }}">Confirm my Email</a>. This link will expire in {{ expiresAtMessageKey|trans(expiresAtMessageData, 'VerifyEmailBundle') }}. </p> <p> Cheers! </p> src/Resources/skeleton/registration/twig_template.tpl.php 0000644 00000001225 15120141002 0020001 0 ustar 00 <?= $helper->getHeadPrintCode('Register'); ?> {% block body %} <?php if ($will_verify_email): ?> {% for flash_error in app.flashes('verify_email_error') %} <div class="alert alert-danger" role="alert">{{ flash_error }}</div> {% endfor %} <?php endif; ?> <h1>Register</h1> {{ form_start(registrationForm) }} {{ form_row(registrationForm.<?= $username_field ?>) }} {{ form_row(registrationForm.plainPassword, { label: 'Password' }) }} {{ form_row(registrationForm.agreeTerms) }} <button type="submit" class="btn">Register</button> {{ form_end(registrationForm) }} {% endblock %} src/Resources/skeleton/resetPassword/ChangePasswordFormType.tpl.php 0000644 00000003066 15120141002 0021672 0 ustar 00 <?= "<?php\n" ?> namespace <?= $namespace ?>; <?= $use_statements ?> class <?= $class_name ?> extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void { $builder ->add('plainPassword', RepeatedType::class, [ 'type' => PasswordType::class, 'first_options' => [ 'attr' => ['autocomplete' => 'new-password'], 'constraints' => [ new NotBlank([ 'message' => 'Please enter a password', ]), new Length([ 'min' => 6, 'minMessage' => 'Your password should be at least {{ limit }} characters', // max length allowed by Symfony for security reasons 'max' => 4096, ]), ], 'label' => 'New password', ], 'second_options' => [ 'attr' => ['autocomplete' => 'new-password'], 'label' => 'Repeat Password', ], 'invalid_message' => 'The password fields must match.', // Instead of being set onto the object directly, // this is read and encoded in the controller 'mapped' => false, ]) ; } public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([]); } } src/Resources/skeleton/resetPassword/ResetPasswordController.tpl.php 0000644 00000016706 15120141002 0022152 0 ustar 00 <?= "<?php\n" ?> namespace <?= $namespace ?>; <?= $use_statements; ?> <?php if ($use_attributes) { ?> #[Route('/reset-password')] <?php } else { ?> /** * @Route("/reset-password") */ <?php } ?> class <?= $class_name ?> extends AbstractController { use ResetPasswordControllerTrait; private <?= $use_typed_properties ? 'ResetPasswordHelperInterface ' : null ?>$resetPasswordHelper; private <?= $use_typed_properties ? 'EntityManagerInterface ' : null ?>$entityManager; public function __construct(ResetPasswordHelperInterface $resetPasswordHelper, EntityManagerInterface $entityManager) { $this->resetPasswordHelper = $resetPasswordHelper; $this->entityManager = $entityManager; } /** * Display & process form to request a password reset. <?php if ($use_attributes) { ?> */ #[Route('', name: 'app_forgot_password_request')] <?php } else { ?> * * @Route("", name="app_forgot_password_request") */ <?php } ?> public function request(Request $request, MailerInterface $mailer<?php if ($translator_available): ?>, TranslatorInterface $translator<?php endif ?>): Response { $form = $this->createForm(<?= $request_form_type_class_name ?>::class); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { return $this->processSendingPasswordResetEmail( $form->get('<?= $email_field ?>')->getData(), $mailer<?php if ($translator_available): ?>, $translator<?php endif ?><?= "\n" ?> ); } return $this->render('reset_password/request.html.twig', [ 'requestForm' => $form->createView(), ]); } /** * Confirmation page after a user has requested a password reset. <?php if ($use_attributes) { ?> */ #[Route('/check-email', name: 'app_check_email')] <?php } else { ?> * * @Route("/check-email", name="app_check_email") */ <?php } ?> public function checkEmail(): Response { // Generate a fake token if the user does not exist or someone hit this page directly. // This prevents exposing whether or not a user was found with the given email address or not if (null === ($resetToken = $this->getTokenObjectFromSession())) { $resetToken = $this->resetPasswordHelper->generateFakeResetToken(); } return $this->render('reset_password/check_email.html.twig', [ 'resetToken' => $resetToken, ]); } /** * Validates and process the reset URL that the user clicked in their email. <?php if ($use_attributes) { ?> */ #[Route('/reset/{token}', name: 'app_reset_password')] <?php } else { ?> * * @Route("/reset/{token}", name="app_reset_password") */ <?php } ?> public function reset(Request $request, <?= $password_hasher_class_details->getShortName() ?> <?= $password_hasher_variable_name ?><?php if ($translator_available): ?>, TranslatorInterface $translator<?php endif ?>, string $token = null): Response { if ($token) { // We store the token in session and remove it from the URL, to avoid the URL being // loaded in a browser and potentially leaking the token to 3rd party JavaScript. $this->storeTokenInSession($token); return $this->redirectToRoute('app_reset_password'); } $token = $this->getTokenFromSession(); if (null === $token) { throw $this->createNotFoundException('No reset password token found in the URL or in the session.'); } try { $user = $this->resetPasswordHelper->validateTokenAndFetchUser($token); } catch (ResetPasswordExceptionInterface $e) { $this->addFlash('reset_password_error', sprintf( '%s - %s', <?php if ($translator_available): ?>$translator->trans(<?= $problem_validate_message_or_constant ?>, [], 'ResetPasswordBundle')<?php else: ?><?= $problem_validate_message_or_constant ?><?php endif ?>, <?php if ($translator_available): ?>$translator->trans($e->getReason(), [], 'ResetPasswordBundle')<?php else: ?>$e->getReason()<?php endif ?><?= "\n" ?> )); return $this->redirectToRoute('app_forgot_password_request'); } // The token is valid; allow the user to change their password. $form = $this->createForm(<?= $reset_form_type_class_name ?>::class); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { // A password reset token should be used only once, remove it. $this->resetPasswordHelper->removeResetRequest($token); // Encode(hash) the plain password, and set it. $encodedPassword = <?= $password_hasher_variable_name ?>-><?= $use_password_hasher ? 'hashPassword' : 'encodePassword' ?>( $user, $form->get('plainPassword')->getData() ); $user-><?= $password_setter ?>($encodedPassword); $this->entityManager->flush(); // The session is cleaned up after the password has been changed. $this->cleanSessionAfterReset(); return $this->redirectToRoute('<?= $success_redirect_route ?>'); } return $this->render('reset_password/reset.html.twig', [ 'resetForm' => $form->createView(), ]); } private function processSendingPasswordResetEmail(string $emailFormData, MailerInterface $mailer<?php if ($translator_available): ?>, TranslatorInterface $translator<?php endif ?>): RedirectResponse { $user = $this->entityManager->getRepository(<?= $user_class_name ?>::class)->findOneBy([ '<?= $email_field ?>' => $emailFormData, ]); // Do not reveal whether a user account was found or not. if (!$user) { return $this->redirectToRoute('app_check_email'); } try { $resetToken = $this->resetPasswordHelper->generateResetToken($user); } catch (ResetPasswordExceptionInterface $e) { // If you want to tell the user why a reset email was not sent, uncomment // the lines below and change the redirect to 'app_forgot_password_request'. // Caution: This may reveal if a user is registered or not. // // $this->addFlash('reset_password_error', sprintf( // '%s - %s', // <?php if ($translator_available): ?>$translator->trans(<?= $problem_handle_message_or_constant ?>, [], 'ResetPasswordBundle')<?php else: ?><?= $problem_handle_message_or_constant ?><?php endif ?>, // <?php if ($translator_available): ?>$translator->trans($e->getReason(), [], 'ResetPasswordBundle')<?php else: ?>$e->getReason()<?php endif ?><?= "\n" ?> // )); return $this->redirectToRoute('app_check_email'); } $email = (new TemplatedEmail()) ->from(new Address('<?= $from_email ?>', '<?= $from_email_name ?>')) ->to($user-><?= $email_getter ?>()) ->subject('Your password reset request') ->htmlTemplate('reset_password/email.html.twig') ->context([ 'resetToken' => $resetToken, ]) ; $mailer->send($email); // Store the token object in session for retrieval in check-email route. $this->setTokenObjectInSession($resetToken); return $this->redirectToRoute('app_check_email'); } } src/Resources/skeleton/resetPassword/ResetPasswordRequestFormType.tpl.php 0000644 00000001237 15120141002 0023136 0 ustar 00 <?= "<?php\n" ?> namespace <?= $namespace ?>; <?= $use_statements ?> class <?= $class_name ?> extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void { $builder ->add('<?= $email_field ?>', EmailType::class, [ 'attr' => ['autocomplete' => 'email'], 'constraints' => [ new NotBlank([ 'message' => 'Please enter your email', ]), ], ]) ; } public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([]); } } src/Resources/skeleton/resetPassword/twig_check_email.tpl.php 0000644 00000001053 15120141002 0020544 0 ustar 00 {% extends 'base.html.twig' %} {% block title %}Password Reset Email Sent{% endblock %} {% block body %} <p> If an account matching your email exists, then an email was just sent that contains a link that you can use to reset your password. This link will expire in {{ resetToken.expirationMessageKey|trans(resetToken.expirationMessageData, 'ResetPasswordBundle') }}. </p> <p>If you don't receive an email please check your spam folder or <a href="{{ path('app_forgot_password_request') }}">try again</a>.</p> {% endblock %} src/Resources/skeleton/resetPassword/twig_email.tpl.php 0000644 00000000551 15120141002 0017411 0 ustar 00 <h1>Hi!</h1> <p>To reset your password, please visit the following link</p> <a href="{{ url('app_reset_password', {token: resetToken.token}) }}">{{ url('app_reset_password', {token: resetToken.token}) }}</a> <p>This link will expire in {{ resetToken.expirationMessageKey|trans(resetToken.expirationMessageData, 'ResetPasswordBundle') }}.</p> <p>Cheers!</p> src/Resources/skeleton/resetPassword/twig_request.tpl.php 0000644 00000001256 15120141002 0020015 0 ustar 00 {% extends 'base.html.twig' %} {% block title %}Reset your password{% endblock %} {% block body %} {% for flash_error in app.flashes('reset_password_error') %} <div class="alert alert-danger" role="alert">{{ flash_error }}</div> {% endfor %} <h1>Reset your password</h1> {{ form_start(requestForm) }} {{ form_row(requestForm.<?= $email_field ?>) }} <div> <small> Enter your email address and we will send you a link to reset your password. </small> </div> <button class="btn btn-primary">Send password reset email</button> {{ form_end(requestForm) }} {% endblock %} src/Resources/skeleton/resetPassword/twig_reset.tpl.php 0000644 00000000504 15120141002 0017442 0 ustar 00 {% extends 'base.html.twig' %} {% block title %}Reset your password{% endblock %} {% block body %} <h1>Reset your password</h1> {{ form_start(resetForm) }} {{ form_row(resetForm.plainPassword) }} <button class="btn btn-primary">Reset password</button> {{ form_end(resetForm) }} {% endblock %} src/Resources/skeleton/security/UserProvider.tpl.php 0000644 00000005275 15120141002 0016733 0 ustar 00 <?= "<?php\n" ?> namespace <?= $namespace; ?>; <?= $use_statements; ?> class <?= $class_name ?> implements UserProviderInterface, PasswordUpgraderInterface { /** * Symfony calls this method if you use features like switch_user * or remember_me. * * If you're not using these features, you do not need to implement * this method. * * @throws UserNotFoundException if the user is not found */ public function loadUserByIdentifier($identifier): UserInterface { // Load a User object from your data source or throw UserNotFoundException. // The $identifier argument may not actually be a username: // it is whatever value is being returned by the getUserIdentifier() // method in your User class. throw new \Exception('TODO: fill in loadUserByIdentifier() inside '.__FILE__); } /** * @deprecated since Symfony 5.3, loadUserByIdentifier() is used instead */ public function loadUserByUsername($username): UserInterface { return $this->loadUserByIdentifier($username); } /** * Refreshes the user after being reloaded from the session. * * When a user is logged in, at the beginning of each request, the * User object is loaded from the session and then this method is * called. Your job is to make sure the user's data is still fresh by, * for example, re-querying for fresh User data. * * If your firewall is "stateless: true" (for a pure API), this * method is not called. */ public function refreshUser(UserInterface $user): UserInterface { if (!$user instanceof <?= $user_short_name ?>) { throw new UnsupportedUserException(sprintf('Invalid user class "%s".', get_class($user))); } // Return a User object after making sure its data is "fresh". // Or throw a UsernameNotFoundException if the user no longer exists. throw new \Exception('TODO: fill in refreshUser() inside '.__FILE__); } /** * Tells Symfony to use this provider for this User class. */ public function supportsClass(string $class): bool { return <?= $user_short_name ?>::class === $class || is_subclass_of($class, <?= $user_short_name ?>::class); } /** * Upgrades the hashed password of a user, typically for using a better hash algorithm. */ public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void { // TODO: when hashed passwords are in use, this method should: // 1. persist the new password in the user storage // 2. update the $user object with $user->setPassword($newHashedPassword); } } src/Resources/skeleton/security/Voter.tpl.php 0000644 00000002620 15120141002 0015370 0 ustar 00 <?= "<?php\n" ?> namespace <?= $namespace; ?>; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\Voter; use Symfony\Component\Security\Core\User\UserInterface; class <?= $class_name ?> extends Voter { public const EDIT = 'POST_EDIT'; public const VIEW = 'POST_VIEW'; protected function supports(string $attribute, $subject): bool { // replace with your own logic // https://symfony.com/doc/current/security/voters.html return in_array($attribute, [self::EDIT, self::VIEW]) && $subject instanceof \App\Entity\<?= str_replace('Voter', null, $class_name) ?>; } protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool { $user = $token->getUser(); // if the user is anonymous, do not grant access if (!$user instanceof UserInterface) { return false; } // ... (check conditions and return true to grant permission) ... switch ($attribute) { case self::EDIT: // logic to determine if the user can EDIT // return true or false break; case self::VIEW: // logic to determine if the user can VIEW // return true or false break; } return false; } } src/Resources/skeleton/serializer/Encoder.tpl.php 0000644 00000001376 15120141002 0016161 0 ustar 00 <?= "<?php\n" ?> namespace <?= $namespace; ?>; <?= $use_statements; ?> class <?= $class_name ?> implements EncoderInterface, DecoderInterface { public const FORMAT = '<?= $format ?>'; public function encode($data, string $format, array $context = []): string { // TODO: return your encoded data return ''; } public function supportsEncoding(string $format, array $context = []): bool { return self::FORMAT === $format; } public function decode(string $data, string $format, array $context = []) { // TODO: return your decoded data return ''; } public function supportsDecoding(string $format, array $context = []): bool { return self::FORMAT === $format; } } src/Resources/skeleton/serializer/Normalizer.tpl.php 0000644 00000001530 15120141002 0016714 0 ustar 00 <?= "<?php\n" ?> namespace <?= $namespace; ?>; <?= $use_statements; ?> class <?= $class_name ?> implements NormalizerInterface, CacheableSupportsMethodInterface { private $normalizer; public function __construct(ObjectNormalizer $normalizer) { $this->normalizer = $normalizer; } public function normalize($object, string $format = null, array $context = []): array { $data = $this->normalizer->normalize($object, $format, $context); // TODO: add, edit, or delete some data return $data; } public function supportsNormalization($data, string $format = null, array $context = []): bool { return $data instanceof \App\Entity\<?= str_replace('Normalizer', null, $class_name) ?>; } public function hasCacheableSupportsMethod(): bool { return true; } } src/Resources/skeleton/stimulus/Controller.tpl.php 0000644 00000001015 15120141002 0016427 0 ustar 00 import { Controller } from '@hotwired/stimulus'; /* * The following line makes this controller "lazy": it won't be downloaded until needed * See https://github.com/symfony/stimulus-bridge#lazy-controllers */ /* stimulusFetch: 'lazy' */ export default class extends Controller { <?= $targets ? " static targets = $targets\n" : "" ?> <?php if ($values) { ?> static values = { <?php foreach ($values as $value): ?> <?= $value['name'] ?>: <?= $value['type'] ?>, <?php endforeach; ?> } <?php } ?> // ... } src/Resources/skeleton/test/ApiTestCase.tpl.php 0000644 00000000534 15120141002 0015550 0 ustar 00 <?= "<?php\n" ?> namespace <?= $namespace; ?>; use <?= $api_test_case_fqcn; ?>; class <?= $class_name ?> extends ApiTestCase { public function testSomething(): void { $response = static::createClient()->request('GET', '/'); $this->assertResponseIsSuccessful(); $this->assertJsonContains(['@id' => '/']); } } src/Resources/skeleton/test/Functional.tpl.php 0000644 00000001751 15120141002 0015507 0 ustar 00 <?php /* @deprecated remove this method when removing make:unit-test and make:functional-test */ ?> <?= "<?php\n" ?> namespace <?= $namespace; ?>; <?= $use_statements ?> class <?= $class_name ?> extends <?= $panther_is_available ? 'PantherTestCase' : 'WebTestCase' ?><?= "\n" ?> { public function testSomething(): void { <?php if ($panther_is_available): ?> $client = static::createPantherClient(); <?php else: ?> $client = static::createClient(); <?php endif ?> $crawler = $client->request('GET', '/'); <?php if ($web_assertions_are_available): ?> <?php if (!$panther_is_available): ?> $this->assertResponseIsSuccessful(); <?php endif ?> $this->assertSelectorTextContains('h1', 'Hello World'); <?php else: ?> <?php if (!$panther_is_available): ?> $this->assertSame(200, $client->getResponse()->getStatusCode()); <?php endif ?> $this->assertStringContainsString('Hello World', $crawler->filter('h1')->text()); <?php endif ?> } } src/Resources/skeleton/test/KernelTestCase.tpl.php 0000644 00000001115 15120141002 0016253 0 ustar 00 <?= "<?php\n" ?> namespace <?= $namespace; ?>; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; class <?= $class_name ?> extends KernelTestCase { public function testSomething(): void { $kernel = self::bootKernel(); $this->assertSame('test', $kernel->getEnvironment()); // $routerService = <?= $use_legacy_container_property ? 'self::$container' : 'static::getContainer()'; ?>->get('router'); // $myCustomService = <?= $use_legacy_container_property ? 'self::$container' : 'static::getContainer()'; ?>->get(CustomService::class); } } src/Resources/skeleton/test/PantherTestCase.tpl.php 0000644 00000001024 15120141002 0016433 0 ustar 00 <?= "<?php\n" ?> namespace <?= $namespace; ?>; use Symfony\Component\Panther\PantherTestCase; class <?= $class_name ?> extends PantherTestCase { public function testSomething(): void { $client = static::createPantherClient(); $crawler = $client->request('GET', '/'); <?php if ($web_assertions_are_available): ?> $this->assertSelectorTextContains('h1', 'Hello World'); <?php else: ?> $this->assertStringContainsString('Hello World', $crawler->filter('h1')->text()); <?php endif ?> } } src/Resources/skeleton/test/TestCase.tpl.php 0000644 00000000327 15120141002 0015116 0 ustar 00 <?= "<?php\n" ?> namespace <?= $namespace; ?>; use PHPUnit\Framework\TestCase; class <?= $class_name ?> extends TestCase { public function testSomething(): void { $this->assertTrue(true); } } src/Resources/skeleton/test/Unit.tpl.php 0000644 00000000463 15120141002 0014323 0 ustar 00 <?php /* @deprecated remove this method when removing make:unit-test and make:functional-test */ ?> <?= "<?php\n" ?> namespace <?= $namespace; ?>; <?= $use_statements; ?> class <?= $class_name ?> extends TestCase { public function testSomething(): void { $this->assertTrue(true); } } src/Resources/skeleton/test/WebTestCase.tpl.php 0000644 00000001205 15120141002 0015550 0 ustar 00 <?= "<?php\n" ?> namespace <?= $namespace; ?>; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; class <?= $class_name ?> extends WebTestCase { public function testSomething(): void { $client = static::createClient(); $crawler = $client->request('GET', '/'); <?php if ($web_assertions_are_available): ?> $this->assertResponseIsSuccessful(); $this->assertSelectorTextContains('h1', 'Hello World'); <?php else: ?> $this->assertSame(200, $client->getResponse()->getStatusCode()); $this->assertStringContainsString('Hello World', $crawler->filter('h1')->text()); <?php endif ?> } } src/Resources/skeleton/twig/Extension.tpl.php 0000644 00000001311 15120141002 0015344 0 ustar 00 <?= "<?php\n" ?> namespace <?= $namespace; ?>; <?= $use_statements ?> class <?= $class_name ?> extends AbstractExtension { public function getFilters(): array { return [ // If your filter generates SAFE HTML, you should add a third // parameter: ['is_safe' => ['html']] // Reference: https://twig.symfony.com/doc/3.x/advanced.html#automatic-escaping new TwigFilter('filter_name', [$this, 'doSomething']), ]; } public function getFunctions(): array { return [ new TwigFunction('function_name', [$this, 'doSomething']), ]; } public function doSomething($value) { // ... } } src/Resources/skeleton/validator/Constraint.tpl.php 0000644 00000000775 15120141002 0016544 0 ustar 00 <?= "<?php\n" ?> namespace <?= $namespace; ?>; use Symfony\Component\Validator\Constraint; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class <?= $class_name ?> extends Constraint { /* * Any public properties become valid options for the annotation. * Then, use these in your validator class. */ public $message = 'The value "{{ value }}" is not valid.'; } src/Resources/skeleton/validator/Validator.tpl.php 0000644 00000001136 15120141002 0016335 0 ustar 00 <?= "<?php\n" ?> namespace <?= $namespace; ?>; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; class <?= $class_name ?> extends ConstraintValidator { public function validate($value, Constraint $constraint) { /* @var <?= $constraint_class_name ?> $constraint */ if (null === $value || '' === $value) { return; } // TODO: implement the validation here $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $value) ->addViolation(); } } src/Resources/skeleton/verifyEmail/EmailVerifier.tpl.php 0000644 00000003605 15120141002 0017425 0 ustar 00 <?= "<?php\n" ?> namespace <?= $namespace; ?>; <?= $use_statements; ?> class <?= $class_name; ?><?= "\n" ?> { private <?= $use_typed_properties ? 'VerifyEmailHelperInterface ' : null ?>$verifyEmailHelper; private <?= $use_typed_properties ? 'MailerInterface ' : null ?>$mailer; private <?= $use_typed_properties ? 'EntityManagerInterface ' : null ?>$entityManager; public function __construct(VerifyEmailHelperInterface $helper, MailerInterface $mailer, EntityManagerInterface $manager) { $this->verifyEmailHelper = $helper; $this->mailer = $mailer; $this->entityManager = $manager; } public function sendEmailConfirmation(string $verifyEmailRouteName, UserInterface $user, TemplatedEmail $email): void { $signatureComponents = $this->verifyEmailHelper->generateSignature( $verifyEmailRouteName, $user-><?= $id_getter ?>(), <?php if ($verify_email_anonymously): ?> $user-><?= $email_getter ?>(), ['id' => $user->getId()] <?php else: ?> $user-><?= $email_getter ?>() <?php endif; ?> ); $context = $email->getContext(); $context['signedUrl'] = $signatureComponents->getSignedUrl(); $context['expiresAtMessageKey'] = $signatureComponents->getExpirationMessageKey(); $context['expiresAtMessageData'] = $signatureComponents->getExpirationMessageData(); $email->context($context); $this->mailer->send($email); } /** * @throws VerifyEmailExceptionInterface */ public function handleEmailConfirmation(Request $request, UserInterface $user): void { $this->verifyEmailHelper->validateEmailConfirmation($request->getUri(), $user-><?= $id_getter ?>(), $user-><?= $email_getter?>()); $user->setIsVerified(true); $this->entityManager->persist($user); $this->entityManager->flush(); } } src/Resources/skeleton/Class.tpl.php 0000644 00000000123 15120141002 0013463 0 ustar 00 <?= "<?php\n" ?> namespace <?= $namespace; ?>; class <?= $class_name."\n" ?> { } src/Security/InteractiveSecurityHelper.php 0000644 00000016405 15120141002 0015010 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Security; use Symfony\Bundle\MakerBundle\Str; use Symfony\Bundle\MakerBundle\Validator; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Security\Core\User\UserInterface; /** * @internal */ final class InteractiveSecurityHelper { public function guessFirewallName(SymfonyStyle $io, array $securityData, string $questionText = null): string { $realFirewalls = array_filter( $securityData['security']['firewalls'] ?? [], function ($item) { return !isset($item['security']) || true === $item['security']; } ); if (0 === \count($realFirewalls)) { return 'main'; } if (1 === \count($realFirewalls)) { return key($realFirewalls); } return $io->choice( $questionText ?? 'Which firewall do you want to update?', array_keys($realFirewalls), key($realFirewalls) ); } public function guessUserClass(SymfonyStyle $io, array $providers, string $questionText = null): string { if (1 === \count($providers) && isset(current($providers)['entity'])) { $entityProvider = current($providers); return $entityProvider['entity']['class']; } $userClass = $io->ask( $questionText ?? 'Enter the User class that you want to authenticate (e.g. <fg=yellow>App\\Entity\\User</>)', $this->guessUserClassDefault(), [Validator::class, 'classIsUserInterface'] ); return $userClass; } private function guessUserClassDefault(): string { if (class_exists('App\\Entity\\User') && isset(class_implements('App\\Entity\\User')[UserInterface::class])) { return 'App\\Entity\\User'; } if (class_exists('App\\Security\\User') && isset(class_implements('App\\Security\\User')[UserInterface::class])) { return 'App\\Security\\User'; } return ''; } public function guessUserNameField(SymfonyStyle $io, string $userClass, array $providers): string { if (1 === \count($providers) && isset(current($providers)['entity']) && isset(current($providers)['entity']['property'])) { $entityProvider = current($providers); return $entityProvider['entity']['property']; } if (property_exists($userClass, 'email') && !property_exists($userClass, 'username')) { return 'email'; } if (!property_exists($userClass, 'email') && property_exists($userClass, 'username')) { return 'username'; } $classProperties = []; $reflectionClass = new \ReflectionClass($userClass); foreach ($reflectionClass->getProperties() as $property) { $classProperties[] = $property->name; } if (empty($classProperties)) { throw new \LogicException(sprintf('No properties were found in "%s" entity', $userClass)); } return $io->choice( sprintf('Which field on your <fg=yellow>%s</> class will people enter when logging in?', $userClass), $classProperties, property_exists($userClass, 'username') ? 'username' : (property_exists($userClass, 'email') ? 'email' : null) ); } public function guessEmailField(SymfonyStyle $io, string $userClass): string { if (property_exists($userClass, 'email')) { return 'email'; } $classProperties = []; $reflectionClass = new \ReflectionClass($userClass); foreach ($reflectionClass->getProperties() as $property) { $classProperties[] = $property->name; } return $io->choice( sprintf('Which field on your <fg=yellow>%s</> class holds the email address?', $userClass), $classProperties ); } public function guessPasswordField(SymfonyStyle $io, string $userClass): string { if (property_exists($userClass, 'password')) { return 'password'; } $classProperties = []; $reflectionClass = new \ReflectionClass($userClass); foreach ($reflectionClass->getProperties() as $property) { $classProperties[] = $property->name; } return $io->choice( sprintf('Which field on your <fg=yellow>%s</> class holds the encoded password?', $userClass), $classProperties ); } public function getAuthenticatorClasses(array $firewallData): array { if (isset($firewallData['guard'])) { return array_filter($firewallData['guard']['authenticators'] ?? [], function ($authenticator) { return class_exists($authenticator); }); } if (isset($firewallData['custom_authenticator'])) { $authenticators = $firewallData['custom_authenticator']; if (\is_string($authenticators)) { $authenticators = [$authenticators]; } return array_filter($authenticators, function ($authenticator) { return class_exists($authenticator); }); } return []; } public function guessPasswordSetter(SymfonyStyle $io, string $userClass): string { if (null === ($methodChoices = $this->methodNameGuesser($userClass, 'setPassword'))) { return 'setPassword'; } return $io->choice( sprintf('Which method on your <fg=yellow>%s</> class can be used to set the encoded password (e.g. setPassword())?', $userClass), $methodChoices ); } public function guessEmailGetter(SymfonyStyle $io, string $userClass, string $emailPropertyName): string { $supposedEmailMethodName = sprintf('get%s', Str::asCamelCase($emailPropertyName)); if (null === ($methodChoices = $this->methodNameGuesser($userClass, $supposedEmailMethodName))) { return $supposedEmailMethodName; } return $io->choice( sprintf('Which method on your <fg=yellow>%s</> class can be used to get the email address (e.g. getEmail())?', $userClass), $methodChoices ); } public function guessIdGetter(SymfonyStyle $io, string $userClass): string { if (null === ($methodChoices = $this->methodNameGuesser($userClass, 'getId'))) { return 'getId'; } return $io->choice( sprintf('Which method on your <fg=yellow>%s</> class can be used to get the unique user identifier (e.g. getId())?', $userClass), $methodChoices ); } private function methodNameGuesser(string $className, string $suspectedMethodName): ?array { $reflectionClass = new \ReflectionClass($className); if ($reflectionClass->hasMethod($suspectedMethodName)) { return null; } $classMethods = []; foreach ($reflectionClass->getMethods() as $method) { $classMethods[] = $method->name; } return $classMethods; } } src/Security/SecurityConfigUpdater.php 0000644 00000020230 15120141002 0014114 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Security; use Symfony\Bundle\MakerBundle\Util\YamlSourceManipulator; use Symfony\Component\HttpKernel\Log\Logger; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; /** * @author Ryan Weaver <ryan@symfonycasts.com> * @author Jesse Rushlow <jr@rushlow.dev> * * @internal */ final class SecurityConfigUpdater { /** @var YamlSourceManipulator */ private $manipulator; /** @var Logger|null */ private $ysmLogger; public function __construct(Logger $ysmLogger = null) { $this->ysmLogger = $ysmLogger; } /** * Updates security.yaml contents based on a new User class. */ public function updateForUserClass(string $yamlSource, UserClassConfiguration $userConfig, string $userClass): string { $this->manipulator = new YamlSourceManipulator($yamlSource); if (null !== $this->ysmLogger) { $this->manipulator->setLogger($this->ysmLogger); } $this->normalizeSecurityYamlFile(); $this->updateProviders($userConfig, $userClass); if ($userConfig->hasPassword()) { $this->updatePasswordHashers($userConfig, $userClass); } $contents = $this->manipulator->getContents(); $this->manipulator = null; return $contents; } public function updateForAuthenticator(string $yamlSource, string $firewallName, $chosenEntryPoint, string $authenticatorClass, bool $logoutSetup): string { $this->manipulator = new YamlSourceManipulator($yamlSource); if (null !== $this->ysmLogger) { $this->manipulator->setLogger($this->ysmLogger); } $this->normalizeSecurityYamlFile(); $newData = $this->manipulator->getData(); if (!isset($newData['security']['firewalls'])) { if ($newData['security']) { $newData['security']['_firewalls'] = $this->manipulator->createEmptyLine(); } $newData['security']['firewalls'] = []; } if (!isset($newData['security']['firewalls'][$firewallName])) { $newData['security']['firewalls'][$firewallName] = ['lazy' => true]; } $firewall = $newData['security']['firewalls'][$firewallName]; if (isset($firewall['custom_authenticator'])) { if (\is_array($firewall['custom_authenticator'])) { $firewall['custom_authenticator'][] = $authenticatorClass; } else { $stringValue = $firewall['custom_authenticator']; $firewall['custom_authenticator'] = []; $firewall['custom_authenticator'][] = $stringValue; $firewall['custom_authenticator'][] = $authenticatorClass; } } else { $firewall['custom_authenticator'] = $authenticatorClass; } if (!isset($firewall['entry_point']) && $chosenEntryPoint) { $firewall['entry_point_empty_line'] = $this->manipulator->createEmptyLine(); $firewall['entry_point_comment'] = $this->manipulator->createCommentLine( ' the entry_point start() method determines what happens when an anonymous user accesses a protected page' ); $firewall['entry_point'] = $authenticatorClass; } if (!isset($firewall['logout']) && $logoutSetup) { $firewall['logout'] = ['path' => 'app_logout']; $firewall['logout'][] = $this->manipulator->createCommentLine( ' where to redirect after logout' ); $firewall['logout'][] = $this->manipulator->createCommentLine( ' target: app_any_route' ); } $newData['security']['firewalls'][$firewallName] = $firewall; $this->manipulator->setData($newData); return $this->manipulator->getContents(); } private function normalizeSecurityYamlFile(): void { if (!isset($this->manipulator->getData()['security'])) { $newData = $this->manipulator->getData(); $newData['security'] = []; $this->manipulator->setData($newData); } } private function updateProviders(UserClassConfiguration $userConfig, string $userClass): void { $this->removeMemoryProviderIfIsSingleConfigured(); $newData = $this->manipulator->getData(); if ($newData['security'] && !\array_key_exists('providers', $newData['security'])) { $newData['security']['_providers'] = $this->manipulator->createEmptyLine(); } $newData['security']['providers']['__'] = $this->manipulator->createCommentLine( ' used to reload user from session & other features (e.g. switch_user)' ); if ($userConfig->isEntity()) { $newData['security']['providers']['app_user_provider'] = [ 'entity' => [ 'class' => $userClass, 'property' => $userConfig->getIdentityPropertyName(), ], ]; } else { if (!$userConfig->getUserProviderClass()) { throw new \LogicException('User provider class must be set for non-entity user.'); } $newData['security']['providers']['app_user_provider'] = [ 'id' => $userConfig->getUserProviderClass(), ]; } $this->manipulator->setData($newData); } private function updatePasswordHashers(UserClassConfiguration $userConfig, string $userClass): void { $newData = $this->manipulator->getData(); if (isset($newData['security']['encoders'])) { throw new \RuntimeException('Password Encoders are no longer supported by MakerBundle. Please update your "config/packages/security.yaml" file to use Password Hashers instead.'); } // The security-bundle recipe sets the password hasher via Flex. If it exists, move on... if (isset($newData['security']['password_hashers'][PasswordAuthenticatedUserInterface::class])) { return; } // by convention, password_hashers are put before the user provider option $providersIndex = array_search('providers', array_keys($newData['security'])); if (false === $providersIndex) { $newData['security'] = ['password_hashers' => []] + $newData['security']; } else { $newData['security'] = array_merge( \array_slice($newData['security'], 0, $providersIndex), ['password_hashers' => []], \array_slice($newData['security'], $providersIndex) ); } $newData['security']['password_hashers'][$userClass] = [ 'algorithm' => 'auto', ]; $newData['security']['password_hashers']['_'] = $this->manipulator->createEmptyLine(); $this->manipulator->setData($newData); } private function removeMemoryProviderIfIsSingleConfigured(): void { if (!$this->isSingleInMemoryProviderConfigured()) { return; } $newData = $this->manipulator->getData(); $memoryProviderName = array_keys($newData['security']['providers'])[0]; $newData['security']['providers'] = []; foreach ($newData['security']['firewalls'] as &$firewall) { if (($firewall['provider'] ?? null) === $memoryProviderName) { $firewall['provider'] = 'app_user_provider'; } } $this->manipulator->setData($newData); } private function isSingleInMemoryProviderConfigured(): bool { if (!isset($this->manipulator->getData()['security']['providers'])) { return false; } $providersConfig = $this->manipulator->getData()['security']['providers']; if (1 !== \count($providersConfig)) { return false; } $firstProviderConfig = array_values($providersConfig)[0]; return \array_key_exists('memory', $firstProviderConfig); } } src/Security/SecurityControllerBuilder.php 0000644 00000007017 15120141002 0015024 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Security; use Symfony\Bundle\MakerBundle\Util\ClassSourceManipulator; use Symfony\Bundle\MakerBundle\Util\PhpCompatUtil; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; /** * @internal */ final class SecurityControllerBuilder { private $phpCompatUtil; public function __construct(PhpCompatUtil $phpCompatUtil) { $this->phpCompatUtil = $phpCompatUtil; } public function addLoginMethod(ClassSourceManipulator $manipulator): void { $loginMethodBuilder = $manipulator->createMethodBuilder('login', 'Response', false); // @legacy Refactor when annotations are no longer supported if ($this->phpCompatUtil->canUseAttributes()) { $loginMethodBuilder->addAttribute($manipulator->buildAttributeNode(Route::class, ['path' => '/login', 'name' => 'app_login'])); } else { $loginMethodBuilder->setDocComment(<<< 'EOT' /** * @Route("/login", name="app_login") */ EOT ); } $manipulator->addUseStatementIfNecessary(Response::class); $manipulator->addUseStatementIfNecessary(Route::class); $manipulator->addUseStatementIfNecessary(AuthenticationUtils::class); $loginMethodBuilder->addParam( (new \PhpParser\Builder\Param('authenticationUtils'))->setTypeHint('AuthenticationUtils') ); $manipulator->addMethodBody($loginMethodBuilder, <<<'CODE' <?php // if ($this->getUser()) { // return $this->redirectToRoute('target_path'); // } CODE ); $loginMethodBuilder->addStmt($manipulator->createMethodLevelBlankLine()); $manipulator->addMethodBody($loginMethodBuilder, <<<'CODE' <?php // get the login error if there is one $error = $authenticationUtils->getLastAuthenticationError(); // last username entered by the user $lastUsername = $authenticationUtils->getLastUsername(); CODE ); $loginMethodBuilder->addStmt($manipulator->createMethodLevelBlankLine()); $manipulator->addMethodBody($loginMethodBuilder, <<<'CODE' <?php return $this->render( 'security/login.html.twig', [ 'last_username' => $lastUsername, 'error' => $error, ] ); CODE ); $manipulator->addMethodBuilder($loginMethodBuilder); } public function addLogoutMethod(ClassSourceManipulator $manipulator): void { $logoutMethodBuilder = $manipulator->createMethodBuilder('logout', 'void', false); // @legacy Refactor when annotations are no longer supported if ($this->phpCompatUtil->canUseAttributes()) { $logoutMethodBuilder->addAttribute($manipulator->buildAttributeNode(Route::class, ['path' => '/logout', 'name' => 'app_logout'])); } else { $logoutMethodBuilder->setDocComment(<<< 'EOT' /** * @Route("/logout", name="app_logout") */ EOT ); } $manipulator->addUseStatementIfNecessary(Route::class); $manipulator->addMethodBody($logoutMethodBuilder, <<<'CODE' <?php throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.'); CODE ); $manipulator->addMethodBuilder($logoutMethodBuilder); } } src/Security/UserClassBuilder.php 0000644 00000023522 15120141002 0013054 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Security; use PhpParser\Node; use Symfony\Bundle\MakerBundle\Util\ClassSourceManipulator; use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\UserInterface; /** * Adds logic to implement UserInterface to an existing User class. * * @internal */ final class UserClassBuilder { public function addUserInterfaceImplementation(ClassSourceManipulator $manipulator, UserClassConfiguration $userClassConfig): void { $manipulator->addInterface(UserInterface::class); $this->addGetUsername($manipulator, $userClassConfig); $this->addGetRoles($manipulator, $userClassConfig); $this->addPasswordImplementation($manipulator, $userClassConfig); $this->addEraseCredentials($manipulator, $userClassConfig); } private function addPasswordImplementation(ClassSourceManipulator $manipulator, UserClassConfiguration $userClassConfig): void { // @legacy Drop conditional when Symfony 5.4 is no longer supported if (60000 > Kernel::VERSION_ID) { // Add methods required to fulfill the UserInterface contract $this->addGetPassword($manipulator, $userClassConfig); $this->addGetSalt($manipulator, $userClassConfig); } if (!$userClassConfig->hasPassword()) { return; } $manipulator->addInterface(PasswordAuthenticatedUserInterface::class); $this->addGetPassword($manipulator, $userClassConfig); } private function addGetUsername(ClassSourceManipulator $manipulator, UserClassConfiguration $userClassConfig): void { if ($userClassConfig->isEntity()) { // add entity property $manipulator->addEntityField( $userClassConfig->getIdentityPropertyName(), [ 'type' => 'string', // https://github.com/FriendsOfSymfony/FOSUserBundle/issues/1919 'length' => 180, 'unique' => true, ] ); } else { // add normal property $manipulator->addProperty($userClassConfig->getIdentityPropertyName()); $manipulator->addGetter( $userClassConfig->getIdentityPropertyName(), 'string', true ); $manipulator->addSetter( $userClassConfig->getIdentityPropertyName(), 'string', false ); } // define getUsername (if it was defined above, this will override) $manipulator->addAccessorMethod( $userClassConfig->getIdentityPropertyName(), 'getUserIdentifier', 'string', false, [ 'A visual identifier that represents this user.', '', '@see UserInterface', ], true ); // @legacy Drop when Symfony 5.4 is no longer supported. if (method_exists(UserInterface::class, 'getSalt')) { // also add the deprecated getUsername method $manipulator->addAccessorMethod( $userClassConfig->getIdentityPropertyName(), 'getUsername', 'string', false, ['@deprecated since Symfony 5.3, use getUserIdentifier instead'], true ); } } private function addGetRoles(ClassSourceManipulator $manipulator, UserClassConfiguration $userClassConfig): void { if ($userClassConfig->isEntity()) { // add entity property $manipulator->addEntityField( 'roles', [ 'type' => 'json', ] ); } else { // add normal property $manipulator->addProperty( 'roles', [], new Node\Expr\Array_([], ['kind' => Node\Expr\Array_::KIND_SHORT]) ); $manipulator->addGetter( 'roles', 'array', false ); $manipulator->addSetter( 'roles', 'array', false ); } // define getRoles (if it was defined above, this will override) $builder = $manipulator->createMethodBuilder( 'getRoles', 'array', false, ['@see UserInterface'] ); // $roles = $this->roles $builder->addStmt( new Node\Stmt\Expression(new Node\Expr\Assign( new Node\Expr\Variable('roles'), new Node\Expr\PropertyFetch(new Node\Expr\Variable('this'), 'roles') )) ); // comment line $builder->addStmt( $manipulator->createMethodLevelCommentNode( 'guarantee every user at least has ROLE_USER' ) ); // $roles[] = 'ROLE_USER'; $builder->addStmt( new Node\Stmt\Expression( new Node\Expr\Assign( new Node\Expr\ArrayDimFetch( new Node\Expr\Variable('roles') ), new Node\Scalar\String_('ROLE_USER') ) ) ); $builder->addStmt($manipulator->createMethodLevelBlankLine()); // return array_unique($roles); $builder->addStmt( new Node\Stmt\Return_( new Node\Expr\FuncCall( new Node\Name('array_unique'), [new Node\Expr\Variable('roles')] ) ) ); $manipulator->addMethodBuilder($builder); } private function addGetPassword(ClassSourceManipulator $manipulator, UserClassConfiguration $userClassConfig): void { if (!$userClassConfig->hasPassword()) { // add an empty method only $builder = $manipulator->createMethodBuilder( 'getPassword', 'string', true, [ 'This method can be removed in Symfony 6.0 - is not needed for apps that do not check user passwords.', '', '@see PasswordAuthenticatedUserInterface', ] ); $builder->addStmt( new Node\Stmt\Return_( new Node\Expr\ConstFetch( new Node\Name('null') ) ) ); $manipulator->addMethodBuilder($builder); return; } $propertyDocs = '@var string The hashed password'; if ($userClassConfig->isEntity()) { // add entity property $manipulator->addEntityField( 'password', [ 'type' => 'string', ], [$propertyDocs] ); } else { // add normal property $manipulator->addProperty('password', [$propertyDocs]); $manipulator->addGetter( 'password', 'string', true ); $manipulator->addSetter( 'password', 'string', false ); } // define getPassword (if it was defined above, this will override) $manipulator->addAccessorMethod( 'password', 'getPassword', 'string', false, [ '@see PasswordAuthenticatedUserInterface', ] ); } private function addGetSalt(ClassSourceManipulator $manipulator, UserClassConfiguration $userClassConfig): void { if ($userClassConfig->hasPassword()) { $methodDescription = [ 'Returning a salt is only needed, if you are not using a modern', 'hashing algorithm (e.g. bcrypt or sodium) in your security.yaml.', ]; } else { $methodDescription = [ 'This method can be removed in Symfony 6.0 - is not needed for apps that do not check user passwords.', ]; } // add getSalt(): ?string - always returning null $builder = $manipulator->createMethodBuilder( 'getSalt', 'string', true, array_merge( $methodDescription, [ '', '@see UserInterface', ] ) ); $builder->addStmt( new Node\Stmt\Return_( new Node\Expr\ConstFetch( new Node\Name('null') ) ) ); $manipulator->addMethodBuilder($builder); } private function addEraseCredentials(ClassSourceManipulator $manipulator, UserClassConfiguration $userClassConfig): void { // add eraseCredentials: always empty $builder = $manipulator->createMethodBuilder( 'eraseCredentials', null, false, ['@see UserInterface'] ); $builder->addStmt( $manipulator->createMethodLevelCommentNode( 'If you store any temporary, sensitive data on the user, clear it here' ) ); $builder->addStmt( $manipulator->createMethodLevelCommentNode( '$this->plainPassword = null;' ) ); $manipulator->addMethodBuilder($builder); } } src/Security/UserClassConfiguration.php 0000644 00000003371 15120141002 0014275 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Security; /** * Configuration about the user's new User class. * * @internal */ final class UserClassConfiguration { private $isEntity; private $identityPropertyName; private $hasPassword; private $useArgon2 = false; private $userProviderClass; public function __construct(bool $isEntity, string $identityPropertyName, bool $hasPassword) { $this->isEntity = $isEntity; $this->identityPropertyName = $identityPropertyName; $this->hasPassword = $hasPassword; } public function isEntity(): bool { return $this->isEntity; } public function getIdentityPropertyName(): string { return $this->identityPropertyName; } public function hasPassword(): bool { return $this->hasPassword; } /** * @deprecated since MakerBundle 1.12 */ public function useArgon2(bool $shouldUse): void { $this->useArgon2 = $shouldUse; } /** * @deprecated since MakerBundle 1.12 */ public function shouldUseArgon2(): bool { return $this->useArgon2; } public function getUserProviderClass(): string { return $this->userProviderClass; } public function setUserProviderClass(string $userProviderClass): void { if ($this->isEntity()) { throw new \LogicException('No custom user class allowed for entity user.'); } $this->userProviderClass = $userProviderClass; } } src/Test/MakerTestCase.php 0000644 00000013515 15120141002 0011445 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Test; use Composer\Semver\Semver; use PHPUnit\Framework\TestCase; use Symfony\Bundle\MakerBundle\MakerInterface; use Symfony\Bundle\MakerBundle\Str; use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Process\Process; abstract class MakerTestCase extends TestCase { /** * @var KernelInterface */ private $kernel; /** * @dataProvider getTestDetails */ public function testExecute(MakerTestDetails $makerTestDetails) { $this->executeMakerCommand($makerTestDetails); } abstract public function getTestDetails(); abstract protected function getMakerClass(): string; protected function createMakerTest(): MakerTestDetails { return new MakerTestDetails($this->getMakerInstance($this->getMakerClass())); } protected function executeMakerCommand(MakerTestDetails $testDetails) { if (!class_exists(Process::class)) { throw new \LogicException('The MakerTestCase cannot be run as the Process component is not installed. Try running "compose require --dev symfony/process".'); } if (!$testDetails->isSupportedByCurrentPhpVersion()) { $this->markTestSkipped(); } $testEnv = MakerTestEnvironment::create($testDetails); // prepare environment to test $testEnv->prepareDirectory(); if (!$this->hasRequiredDependencyVersions($testDetails, $testEnv)) { $this->markTestSkipped('Some dependencies versions are too low'); } $makerRunner = new MakerTestRunner($testEnv); foreach ($testDetails->getPreRunCallbacks() as $preRunCallback) { $preRunCallback($makerRunner); } $callback = $testDetails->getRunCallback(); $callback($makerRunner); // run tests $files = $testEnv->getGeneratedFilesFromOutputText(); foreach ($files as $file) { $this->assertTrue($testEnv->fileExists($file), sprintf('The file "%s" does not exist after generation', $file)); if ('.php' === substr($file, -4)) { $csProcess = $testEnv->runPhpCSFixer($file); $this->assertTrue($csProcess->isSuccessful(), sprintf( "File '%s' has a php-cs problem: %s\n", $file, $csProcess->getErrorOutput()."\n".$csProcess->getOutput() )); } if ('.twig' === substr($file, -5)) { $csProcess = $testEnv->runTwigCSLint($file); $this->assertTrue($csProcess->isSuccessful(), sprintf('File "%s" has a twig-cs problem: %s', $file, $csProcess->getErrorOutput()."\n".$csProcess->getOutput())); } } } protected function assertContainsCount(string $needle, string $haystack, int $count) { $this->assertEquals(1, substr_count($haystack, $needle), sprintf('Found more than %d occurrences of "%s" in "%s"', $count, $needle, $haystack)); } private function getMakerInstance(string $makerClass): MakerInterface { if (null === $this->kernel) { $this->kernel = $this->createKernel(); $this->kernel->boot(); } // a cheap way to guess the service id $serviceId = $serviceId ?? sprintf('maker.maker.%s', Str::asSnakeCase((new \ReflectionClass($makerClass))->getShortName())); return $this->kernel->getContainer()->get($serviceId); } protected function createKernel(): KernelInterface { return new MakerTestKernel('dev', true); } private function hasRequiredDependencyVersions(MakerTestDetails $testDetails, MakerTestEnvironment $testEnv): bool { if (empty($testDetails->getRequiredPackageVersions())) { return true; } $installedPackages = json_decode($testEnv->readFile('vendor/composer/installed.json'), true); $packageVersions = []; foreach ($installedPackages['packages'] ?? $installedPackages as $installedPackage) { $packageVersions[$installedPackage['name']] = $installedPackage['version_normalized']; } foreach ($testDetails->getRequiredPackageVersions() as $requiredPackageData) { $name = $requiredPackageData['name']; $versionConstraint = $requiredPackageData['version_constraint']; if (!isset($packageVersions[$name])) { throw new \Exception(sprintf('Package "%s" is required in the test project at version "%s" but it is not installed?', $name, $versionConstraint)); } if (!Semver::satisfies($packageVersions[$name], $versionConstraint)) { return false; } } return true; } public static function assertStringContainsString(string $needle, string $haystack, string $message = ''): void { if (method_exists(TestCase::class, 'assertStringContainsString')) { parent::assertStringContainsString($needle, $haystack, $message); } else { // legacy for older phpunit versions (e.g. older php version on CI) self::assertContains($needle, $haystack, $message); } } public static function assertStringNotContainsString(string $needle, string $haystack, string $message = ''): void { if (method_exists(TestCase::class, 'assertStringNotContainsString')) { parent::assertStringNotContainsString($needle, $haystack, $message); } else { // legacy for older phpunit versions (e.g. older php version on CI) self::assertNotContains($needle, $haystack, $message); } } } src/Test/MakerTestDetails.php 0000644 00000006635 15120141002 0012164 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Test; use Symfony\Bundle\MakerBundle\DependencyBuilder; use Symfony\Bundle\MakerBundle\MakerInterface; final class MakerTestDetails { private $maker; private $runCallback; private $preRunCallbacks = []; private $extraDependencies = []; private $rootNamespace = 'App'; private $requiredPhpVersion; private $requiredPackageVersions = []; public function __construct(MakerInterface $maker) { $this->maker = $maker; } public function run(\Closure $callback): self { $this->runCallback = $callback; return $this; } public function preRun(\Closure $callback): self { $this->preRunCallbacks[] = $callback; return $this; } public function getRootNamespace() { return $this->rootNamespace; } public function changeRootNamespace(string $rootNamespace): self { $this->rootNamespace = trim($rootNamespace, '\\'); return $this; } public function addExtraDependencies(string ...$packages): self { $this->extraDependencies += $packages; return $this; } public function setRequiredPhpVersion(int $version): self { $this->requiredPhpVersion = $version; return $this; } public function addRequiredPackageVersion(string $packageName, string $versionConstraint): self { $this->requiredPackageVersions[] = ['name' => $packageName, 'version_constraint' => $versionConstraint]; return $this; } public function getUniqueCacheDirectoryName(): string { // for cache purposes, only the dependencies are important! // You can change it ONLY if you don't have another way to implement it return 'maker_'.strtolower($this->getRootNamespace()).'_'.md5(serialize($this->getDependencies())); } public function getMaker(): MakerInterface { return $this->maker; } public function getDependencies(): array { $depBuilder = $this->getDependencyBuilder(); return array_merge( $depBuilder->getAllRequiredDependencies(), $depBuilder->getAllRequiredDevDependencies(), $this->extraDependencies ); } public function getExtraDependencies(): array { return $this->extraDependencies; } public function getDependencyBuilder(): DependencyBuilder { $depBuilder = new DependencyBuilder(); $this->maker->configureDependencies($depBuilder); return $depBuilder; } public function isSupportedByCurrentPhpVersion(): bool { return null === $this->requiredPhpVersion || \PHP_VERSION_ID >= $this->requiredPhpVersion; } public function getRequiredPackageVersions(): array { return $this->requiredPackageVersions; } public function getRunCallback(): \Closure { if (!$this->runCallback) { throw new \Exception('Don\'t forget to call ->run()'); } return $this->runCallback; } /** * @return \Closure[] */ public function getPreRunCallbacks(): array { return $this->preRunCallbacks; } } src/Test/MakerTestEnvironment.php 0000644 00000035372 15120141002 0013103 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Test; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Process\InputStream; /** * @author Sadicov Vladimir <sadikoff@gmail.com> * @author Nicolas Philippe <nikophil@gmail.com> * * @internal */ final class MakerTestEnvironment { private $testDetails; private $fs; private $rootPath; private $cachePath; private $flexPath; private $path; /** * @var MakerTestProcess */ private $runnedMakerProcess; private function __construct(MakerTestDetails $testDetails) { $this->testDetails = $testDetails; $this->fs = new Filesystem(); $this->rootPath = realpath(__DIR__.'/../../'); $cachePath = $this->rootPath.'/tests/tmp/cache'; if (!$this->fs->exists($cachePath)) { $this->fs->mkdir($cachePath); } $this->cachePath = realpath($cachePath); $targetVersion = $this->getTargetSkeletonVersion(); $this->flexPath = $this->cachePath.'/flex_project'.$targetVersion; $directoryName = $targetVersion ?: 'current'; if (str_ends_with($directoryName, '.*')) { $directoryName = substr($directoryName, 0, -2); } $this->path = $this->cachePath.\DIRECTORY_SEPARATOR.$testDetails->getUniqueCacheDirectoryName().'_'.$directoryName; } public static function create(MakerTestDetails $testDetails): self { return new self($testDetails); } public function getPath(): string { return $this->path; } public function readFile(string $path): string { if (!file_exists($this->path.'/'.$path)) { throw new \InvalidArgumentException(sprintf('Cannot find file "%s"', $path)); } return file_get_contents($this->path.'/'.$path); } private function changeRootNamespaceIfNeeded(): void { if ('App' === ($rootNamespace = $this->testDetails->getRootNamespace())) { return; } $replacements = [ [ 'filename' => 'composer.json', 'find' => '"App\\\\": "src/"', 'replace' => '"'.$rootNamespace.'\\\\": "src/"', ], [ 'filename' => 'src/Kernel.php', 'find' => 'namespace App', 'replace' => 'namespace '.$rootNamespace, ], [ 'filename' => 'bin/console', 'find' => 'use App\\Kernel', 'replace' => 'use '.$rootNamespace.'\\Kernel', ], [ 'filename' => 'public/index.php', 'find' => 'use App\\Kernel', 'replace' => 'use '.$rootNamespace.'\\Kernel', ], [ 'filename' => 'config/services.yaml', 'find' => 'App\\', 'replace' => $rootNamespace.'\\', ], [ 'filename' => '.env.test', 'find' => 'KERNEL_CLASS=\'App\Kernel\'', 'replace' => 'KERNEL_CLASS=\''.$rootNamespace.'\Kernel\'', ], ]; if ($this->fs->exists($this->path.'/config/packages/doctrine.yaml')) { $replacements[] = [ 'filename' => 'config/packages/doctrine.yaml', 'find' => 'App', 'replace' => $rootNamespace, ]; } $this->processReplacements($replacements, $this->path); $this->runCommand('composer dump-autoload'); } public function prepareDirectory(): void { if (!$this->fs->exists($this->flexPath)) { $this->buildFlexSkeleton(); } if (!$this->fs->exists($this->path)) { try { // lets do some magic here git is faster than copy MakerTestProcess::create( '\\' === \DIRECTORY_SEPARATOR ? 'git clone %FLEX_PATH% %APP_PATH%' : 'git clone "$FLEX_PATH" "$APP_PATH"', \dirname($this->flexPath), [ 'FLEX_PATH' => $this->flexPath, 'APP_PATH' => $this->path, ] ) ->run(); // install any missing dependencies $dependencies = $this->determineMissingDependencies(); if ($dependencies) { MakerTestProcess::create(sprintf('composer require %s', implode(' ', $dependencies)), $this->path) ->run(); } $this->changeRootNamespaceIfNeeded(); file_put_contents($this->path.'/.gitignore', "var/cache/\nvendor/\n"); MakerTestProcess::create('git diff --quiet || ( git config user.name "symfony" && git config user.email "test@symfony.com" && git add . && git commit -a -m "second commit" )', $this->path )->run(); } catch (\Exception $e) { $this->fs->remove($this->path); throw $e; } } else { MakerTestProcess::create('git reset --hard && git clean -fd', $this->path)->run(); } } public function runCommand(string $command): MakerTestProcess { return MakerTestProcess::create($command, $this->path)->run(); } public function runMaker(array $inputs, string $argumentsString = '', bool $allowedToFail = false): MakerTestProcess { // Let's remove cache $this->fs->remove($this->path.'/var/cache'); $testProcess = $this->createInteractiveCommandProcess( $this->testDetails->getMaker()::getCommandName(), $inputs, $argumentsString ); $this->runnedMakerProcess = $testProcess->run($allowedToFail); return $this->runnedMakerProcess; } public function getGeneratedFilesFromOutputText(): array { $output = $this->runnedMakerProcess->getOutput(); $matches = []; preg_match_all('#(created|updated): (]8;;[^]*\\\)?(.*?)(]8;;\\\)?\n#iu', $output, $matches, \PREG_PATTERN_ORDER); return array_map('trim', $matches[3]); } public function fileExists(string $file): bool { return $this->fs->exists($this->path.'/'.$file); } public function runPhpCSFixer(string $file): MakerTestProcess { if (!file_exists(__DIR__.'/../../tools/php-cs-fixer/vendor/bin/php-cs-fixer')) { throw new \Exception('php-cs-fixer not found: run: "composer install --working-dir=tools/php-cs-fixer".'); } return MakerTestProcess::create( sprintf('php tools/php-cs-fixer/vendor/bin/php-cs-fixer --config=%s fix --dry-run --diff %s', __DIR__.'/../Resources/test/.php_cs.test', $this->path.'/'.$file), $this->rootPath, ['PHP_CS_FIXER_IGNORE_ENV' => '1'] )->run(true); } public function runTwigCSLint(string $file): MakerTestProcess { if (!file_exists(__DIR__.'/../../tools/twigcs/vendor/bin/twigcs')) { throw new \Exception('twigcs not found: run: "composer install --working-dir=tools/twigcs".'); } return MakerTestProcess::create(sprintf('php tools/twigcs/vendor/bin/twigcs --config ./tools/twigcs/.twig_cs.dist %s', $this->path.'/'.$file), $this->rootPath) ->run(true); } private function buildFlexSkeleton(): void { $targetVersion = $this->getTargetSkeletonVersion(); $versionString = $targetVersion ? sprintf(':%s', $targetVersion) : ''; MakerTestProcess::create( sprintf('composer create-project symfony/skeleton%s flex_project%s --prefer-dist --no-progress', $versionString, $targetVersion), $this->cachePath )->run(); $rootPath = str_replace('\\', '\\\\', realpath(__DIR__.'/../..')); // processes any changes needed to the Flex project $replacements = [ [ 'filename' => 'config/bundles.php', 'find' => "Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],", 'replace' => "Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],\n Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],", ], [ // ugly way to autoload Maker & any other vendor libs needed in the command 'filename' => 'composer.json', 'find' => '"App\\\Tests\\\": "tests/"', 'replace' => sprintf( '"App\\\Tests\\\": "tests/",'."\n".' "Symfony\\\Bundle\\\MakerBundle\\\": "%s/src/",'."\n".' "PhpParser\\\": "%s/vendor/nikic/php-parser/lib/PhpParser/"', // escape \ for Windows $rootPath, $rootPath ), ], ]; $this->processReplacements($replacements, $this->flexPath); if ($_SERVER['MAKER_ALLOW_DEV_DEPS_IN_APP'] ?? false) { MakerTestProcess::create('composer config minimum-stability dev', $this->flexPath)->run(); MakerTestProcess::create('composer config prefer-stable true', $this->flexPath)->run(); } // fetch a few packages needed for testing MakerTestProcess::create('composer require phpunit browser-kit symfony/css-selector --prefer-dist --no-progress --no-suggest', $this->flexPath) ->run(); if ('\\' !== \DIRECTORY_SEPARATOR) { $this->fs->remove($this->flexPath.'/vendor/symfony/phpunit-bridge'); $this->fs->symlink($rootPath.'/vendor/symfony/phpunit-bridge', $this->flexPath.'/vendor/symfony/phpunit-bridge'); } $replacements = [ // temporarily ignoring indirect deprecations - see #237 [ 'filename' => '.env.test', 'find' => 'SYMFONY_DEPRECATIONS_HELPER=999999', 'replace' => 'SYMFONY_DEPRECATIONS_HELPER=max[self]=0', ], // do not explicitly set the PHPUnit version [ 'filename' => 'phpunit.xml.dist', 'find' => '<server name="SYMFONY_PHPUNIT_VERSION" value="9.5" />', 'replace' => '', ], ]; $this->processReplacements($replacements, $this->flexPath); // end of temp code file_put_contents($this->flexPath.'/.gitignore', "var/cache/\n"); // Force adding vendor/ dir to Git repo in case users exclude it in global .gitignore MakerTestProcess::create('git init && git config user.name "symfony" && git config user.email "test@symfony.com" && git add . && git add vendor/ -f && git commit -a -m "first commit"', $this->flexPath )->run(); } private function processReplacements(array $replacements, string $rootDir): void { foreach ($replacements as $replacement) { $this->processReplacement($rootDir, $replacement['filename'], $replacement['find'], $replacement['replace']); } } public function processReplacement(string $rootDir, string $filename, string $find, string $replace, bool $allowNotFound = false): void { $path = realpath($rootDir.'/'.$filename); if (!$this->fs->exists($path)) { if ($allowNotFound) { return; } throw new \Exception(sprintf('Could not find file "%s" to process replacements inside "%s"', $filename, $rootDir)); } $contents = file_get_contents($path); if (false === strpos($contents, $find)) { if ($allowNotFound) { return; } throw new \Exception(sprintf('Could not find "%s" inside "%s"', $find, $filename)); } file_put_contents($path, str_replace($find, $replace, $contents)); } public function createInteractiveCommandProcess(string $commandName, array $userInputs, string $argumentsString = ''): MakerTestProcess { // We don't need ansi coloring in tests! $process = MakerTestProcess::create( sprintf('php bin/console %s %s --no-ansi', $commandName, $argumentsString), $this->path, [ 'SHELL_INTERACTIVE' => '1', ], 10 ); if ($userInputs) { $inputStream = new InputStream(); // start the command with some input $inputStream->write(current($userInputs)."\n"); $inputStream->onEmpty(function () use ($inputStream, &$userInputs) { $nextInput = next($userInputs); if (false === $nextInput) { $inputStream->close(); } else { $inputStream->write($nextInput."\n"); } }); $process->setInput($inputStream); } return $process; } public function getSymfonyVersionInApp(): int { $contents = file_get_contents($this->getPath().'/vendor/symfony/http-kernel/Kernel.php'); $position = strpos($contents, 'VERSION_ID = '); return (int) substr($contents, $position + 13, 5); } public function doesClassExistInApp(string $class): bool { $classMap = require $this->getPath().'/vendor/composer/autoload_classmap.php'; return isset($classMap[$class]); } /** * Executes the DependencyBuilder for the Maker command inside the * actual project, so we know exactly what dependencies we need or * don't need. */ private function determineMissingDependencies(): array { $depBuilder = $this->testDetails->getDependencyBuilder(); file_put_contents($this->path.'/dep_builder', serialize($depBuilder)); file_put_contents($this->path.'/dep_runner.php', '<?php require __DIR__."/vendor/autoload.php"; $depBuilder = unserialize(file_get_contents("dep_builder")); $missingDependencies = array_merge( $depBuilder->getMissingDependencies(), $depBuilder->getMissingDevDependencies() ); echo json_encode($missingDependencies); '); $process = MakerTestProcess::create('php dep_runner.php', $this->path)->run(); $data = json_decode($process->getOutput(), true); if (null === $data) { throw new \Exception('Could not determine dependencies'); } unlink($this->path.'/dep_builder'); unlink($this->path.'/dep_runner.php'); return array_merge($data, $this->testDetails->getExtraDependencies()); } private function getTargetSkeletonVersion(): ?string { return $_SERVER['SYMFONY_VERSION'] ?? ''; } } src/Test/MakerTestKernel.php 0000644 00000004336 15120141002 0012013 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Test; use Symfony\Bundle\FrameworkBundle\FrameworkBundle; use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; use Symfony\Bundle\MakerBundle\DependencyInjection\CompilerPass\MakeCommandRegistrationPass; use Symfony\Bundle\MakerBundle\MakerBundle; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; class MakerTestKernel extends Kernel implements CompilerPassInterface { use MicroKernelTrait; private $testRootDir; public function __construct(string $environment, bool $debug) { $this->testRootDir = sys_get_temp_dir().'/'.uniqid('sf_maker_', true); parent::__construct($environment, $debug); } public function registerBundles(): iterable { return [ new FrameworkBundle(), new MakerBundle(), ]; } protected function configureRoutes(RoutingConfigurator $routes) { } protected function configureRouting(RoutingConfigurator $routes) { } protected function configureContainer(ContainerBuilder $c, LoaderInterface $loader) { $c->loadFromExtension('framework', [ 'secret' => 123, 'router' => [ 'utf8' => true, ], ]); } public function getProjectDir(): string { return $this->getRootDir(); } public function getRootDir(): string { return $this->testRootDir; } public function process(ContainerBuilder $container) { // makes all makers public to help the tests foreach ($container->findTaggedServiceIds(MakeCommandRegistrationPass::MAKER_TAG) as $id => $tags) { $defn = $container->getDefinition($id); $defn->setPublic(true); } } } src/Test/MakerTestProcess.php 0000644 00000003447 15120141002 0012213 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Test; use Symfony\Component\Process\Process; /** * @author Sadicov Vladimir <sadikoff@gmail.com> * * @internal */ final class MakerTestProcess { private $process; private function __construct($commandLine, $cwd, array $envVars, $timeout) { $this->process = \is_string($commandLine) ? Process::fromShellCommandline($commandLine, $cwd, null, null, $timeout) : new Process($commandLine, $cwd, null, null, $timeout); $this->process->setEnv($envVars); } public static function create($commandLine, $cwd, array $envVars = [], $timeout = null): self { return new self($commandLine, $cwd, $envVars, $timeout); } public function setInput($input): self { $this->process->setInput($input); return $this; } public function run($allowToFail = false, array $envVars = []): self { $this->process->run(null, $envVars); if (!$allowToFail && !$this->process->isSuccessful()) { throw new \Exception(sprintf('Error running command: "%s". Output: "%s". Error: "%s"', $this->process->getCommandLine(), $this->process->getOutput(), $this->process->getErrorOutput())); } return $this; } public function isSuccessful(): bool { return $this->process->isSuccessful(); } public function getOutput(): string { return $this->process->getOutput(); } public function getErrorOutput(): string { return $this->process->getErrorOutput(); } } src/Test/MakerTestRunner.php 0000644 00000021337 15120141002 0012044 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Test; use PHPUnit\Framework\ExpectationFailedException; use Symfony\Bundle\MakerBundle\Util\ClassSourceManipulator; use Symfony\Bundle\MakerBundle\Util\YamlSourceManipulator; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Finder\Finder; use Symfony\Component\Yaml\Yaml; use Twig\Environment; use Twig\Loader\FilesystemLoader; class MakerTestRunner { private $environment; private $filesystem; private $executedMakerProcess; public function __construct(MakerTestEnvironment $environment) { $this->environment = $environment; $this->filesystem = new Filesystem(); } public function runMaker(array $inputs, string $argumentsString = '', bool $allowedToFail = false): string { $this->executedMakerProcess = $this->environment->runMaker($inputs, $argumentsString, $allowedToFail); return $this->executedMakerProcess->getOutput(); } public function copy(string $source, string $destination) { $path = __DIR__.'/../../tests/fixtures/'.$source; if (!file_exists($path)) { throw new \Exception(sprintf('Cannot find file "%s"', $path)); } if (is_file($path)) { $this->filesystem->copy($path, $this->getPath($destination), true); return; } // handle a directory copy $finder = new Finder(); $finder->in($path)->files(); foreach ($finder as $file) { $this->filesystem->copy($file->getPathname(), $this->getPath($file->getRelativePathname()), true); } } /** * When using an authenticator "fixtures" file, this adjusts it to support Symfony 5.2/5.3. */ public function adjustAuthenticatorForLegacyPassportInterface(string $filename): void { // no adjustment needed on 5.4 and higher if ($this->getSymfonyVersion() >= 50400) { return; } $this->replaceInFile( $filename, '\\Passport;', "\\Passport;\nuse Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;" ); $this->replaceInFile( $filename, ': Passport', ': PassportInterface' ); } public function renderTemplateFile(string $source, string $destination, array $variables): void { $twig = new Environment( new FilesystemLoader(__DIR__.'/../../tests/fixtures') ); $rendered = $twig->render($source, $variables); $this->filesystem->mkdir(\dirname($this->getPath($destination))); file_put_contents($this->getPath($destination), $rendered); } public function getPath(string $filename): string { return $this->environment->getPath().'/'.$filename; } public function readYaml(string $filename): array { return Yaml::parse(file_get_contents($this->getPath($filename))); } public function getExecutedMakerProcess(): MakerTestProcess { if (!$this->executedMakerProcess) { throw new \Exception('Maker process has not been executed yet.'); } return $this->executedMakerProcess; } public function modifyYamlFile(string $filename, \Closure $callback) { $path = $this->getPath($filename); $manipulator = new YamlSourceManipulator(file_get_contents($path)); $newData = $callback($manipulator->getData()); if (!\is_array($newData)) { throw new \Exception('The modifyYamlFile() callback must return the final array of data'); } $manipulator->setData($newData); file_put_contents($path, $manipulator->getContents()); } public function runConsole(string $command, array $inputs, string $arguments = '') { $process = $this->environment->createInteractiveCommandProcess( $command, $inputs, $arguments ); $process->run(); } public function runProcess(string $command): void { MakerTestProcess::create($command, $this->environment->getPath())->run(); } public function replaceInFile(string $filename, string $find, string $replace, bool $allowNotFound = false): void { $this->environment->processReplacement( $this->environment->getPath(), $filename, $find, $replace, $allowNotFound ); } public function removeFromFile(string $filename, string $find, bool $allowNotFound = false): void { $this->environment->processReplacement( $this->environment->getPath(), $filename, $find, '', $allowNotFound ); } public function configureDatabase(bool $createSchema = true): void { $this->replaceInFile( '.env', 'postgresql://symfony:ChangeMe@127.0.0.1:5432/app?serverVersion=13&charset=utf8', getenv('TEST_DATABASE_DSN') ); // Flex includes a recipe to suffix the dbname w/ "_test" - lets keep // things simple for these tests and not do that. $this->modifyYamlFile('config/packages/doctrine.yaml', function (array $config) { if (isset($config['when@test']['doctrine']['dbal']['dbname_suffix'])) { unset($config['when@test']['doctrine']['dbal']['dbname_suffix']); } return $config; }); // @legacy DoctrineBundle 2.4 recipe uses when@test instead of a test/doctrine.yaml config if ($this->filesystem->exists('config/packages/test/doctrine.yaml')) { $this->removeFromFile( 'config/packages/test/doctrine.yaml', "dbname_suffix: '_test%env(default::TEST_TOKEN)%'" ); } // this looks silly, but it's the only way to drop the database *for sure*, // as doctrine:database:drop will error if there is no database // also, skip for SQLITE, as it does not support --if-not-exists if (0 !== strpos(getenv('TEST_DATABASE_DSN'), 'sqlite://')) { $this->runConsole('doctrine:database:create', [], '--env=test --if-not-exists'); } $this->runConsole('doctrine:database:drop', [], '--env=test --force'); $this->runConsole('doctrine:database:create', [], '--env=test'); if ($createSchema) { $this->runConsole('doctrine:schema:create', [], '--env=test'); } } public function updateSchema(): void { $this->runConsole('doctrine:schema:update', [], '--env=test --force'); } public function runTests(): void { $internalTestProcess = MakerTestProcess::create( sprintf('php %s', $this->getPath('/bin/phpunit')), $this->environment->getPath()) ->run(true) ; if ($internalTestProcess->isSuccessful()) { return; } throw new ExpectationFailedException(sprintf("Error while running the PHPUnit tests *in* the project: \n\n %s \n\n Command Output: %s", $internalTestProcess->getErrorOutput()."\n".$internalTestProcess->getOutput(), $this->getExecutedMakerProcess()->getErrorOutput()."\n".$this->getExecutedMakerProcess()->getOutput())); } public function writeFile(string $filename, string $contents): void { $this->filesystem->mkdir(\dirname($this->getPath($filename))); file_put_contents($this->getPath($filename), $contents); } public function addToAutoloader(string $namespace, string $path) { $this->replaceInFile( 'composer.json', '"App\\\Tests\\\": "tests/",', sprintf('"App\\\Tests\\\": "tests/",'."\n".' "%s": "%s",', $namespace, $path) ); $this->environment->runCommand('composer dump-autoload'); } public function deleteFile(string $filename): void { $this->filesystem->remove($this->getPath($filename)); } public function manipulateClass(string $filename, \Closure $callback): void { $contents = file_get_contents($this->getPath($filename)); $manipulator = new ClassSourceManipulator($contents, true, false, true, true); $callback($manipulator); file_put_contents($this->getPath($filename), $manipulator->getSourceCode()); } public function getSymfonyVersion(): int { return $this->environment->getSymfonyVersionInApp(); } public function doesClassExist(string $class): bool { return $this->environment->doesClassExistInApp($class); } } src/Util/AutoloaderUtil.php 0000644 00000005475 15120141002 0011713 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Util; use Composer\Autoload\ClassLoader; /** * @author Ryan Weaver <weaverryan@gmail.com> * * @internal */ class AutoloaderUtil { /** * @var ComposerAutoloaderFinder */ private $autoloaderFinder; public function __construct(ComposerAutoloaderFinder $autoloaderFinder) { $this->autoloaderFinder = $autoloaderFinder; } /** * Returns the relative path to where a new class should live. * * @throws \Exception */ public function getPathForFutureClass(string $className): ?string { $classLoader = $this->getClassLoader(); // lookup is obviously modeled off of Composer's autoload logic foreach ($classLoader->getPrefixesPsr4() as $prefix => $paths) { if (0 === strpos($className, $prefix)) { return $paths[0].'/'.str_replace('\\', '/', substr($className, \strlen($prefix))).'.php'; } } foreach ($classLoader->getPrefixes() as $prefix => $paths) { if (0 === strpos($className, $prefix)) { return $paths[0].'/'.str_replace('\\', '/', $className).'.php'; } } if ($classLoader->getFallbackDirsPsr4()) { return $classLoader->getFallbackDirsPsr4()[0].'/'.str_replace('\\', '/', $className).'.php'; } if ($classLoader->getFallbackDirs()) { return $classLoader->getFallbackDirs()[0].'/'.str_replace('\\', '/', $className).'.php'; } return null; } public function getNamespacePrefixForClass(string $className): string { foreach ($this->getClassLoader()->getPrefixesPsr4() as $prefix => $paths) { if (0 === strpos($className, $prefix)) { return $prefix; } } return ''; } /** * Returns if the namespace is configured by composer autoloader. */ public function isNamespaceConfiguredToAutoload(string $namespace): bool { $namespace = trim($namespace, '\\').'\\'; $classLoader = $this->getClassLoader(); foreach ($classLoader->getPrefixesPsr4() as $prefix => $paths) { if (0 === strpos($namespace, $prefix)) { return true; } } foreach ($classLoader->getPrefixes() as $prefix => $paths) { if (0 === strpos($namespace, $prefix)) { return true; } } return false; } private function getClassLoader(): ClassLoader { return $this->autoloaderFinder->getClassLoader(); } } src/Util/ClassDetails.php 0000644 00000003442 15120141002 0011321 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Util; /** * @internal */ final class ClassDetails { private $fullClassName; public function __construct(string $fullClassName) { $this->fullClassName = $fullClassName; } /** * Get list of property names except "id" for use in a make:form context. */ public function getFormFields(): array { $properties = $this->getProperties(); $fields = array_diff($properties, ['id']); $fieldsWithTypes = []; foreach ($fields as $field) { $fieldsWithTypes[$field] = null; } return $fieldsWithTypes; } private function getProperties(): array { $reflect = new \ReflectionClass($this->fullClassName); $props = $reflect->getProperties(); $propertiesList = []; foreach ($props as $prop) { $propertiesList[] = $prop->getName(); } return $propertiesList; } public function getPath(): string { return (new \ReflectionClass($this->fullClassName))->getFileName(); } /** * An imperfect, but simple way to check for the presence of an annotation. * * @param string $annotation The annotation - e.g. @UniqueEntity */ public function doesDocBlockContainAnnotation(string $annotation): bool { $docComment = (new \ReflectionClass($this->fullClassName))->getDocComment(); if (false === $docComment) { return false; } return false !== strpos($docComment, $annotation); } } src/Util/ClassNameDetails.php 0000644 00000002612 15120141002 0012120 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Util; use Symfony\Bundle\MakerBundle\Str; final class ClassNameDetails { private $fullClassName; private $namespacePrefix; private $suffix; public function __construct(string $fullClassName, string $namespacePrefix, string $suffix = null) { $this->fullClassName = $fullClassName; $this->namespacePrefix = trim($namespacePrefix, '\\'); $this->suffix = $suffix; } public function getFullName(): string { return $this->fullClassName; } public function getShortName(): string { return Str::getShortClassName($this->fullClassName); } /** * Returns the original class name the user entered (after * being cleaned up). * * For example, assuming the namespace is App\Entity: * App\Entity\Admin\User => Admin\User */ public function getRelativeName(): string { return str_replace($this->namespacePrefix.'\\', '', $this->fullClassName); } public function getRelativeNameWithoutSuffix(): string { return Str::removeSuffix($this->getRelativeName(), $this->suffix); } } src/Util/ClassNameValue.php 0000644 00000001716 15120141002 0011613 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Util; use Symfony\Bundle\MakerBundle\Str; /** * @internal */ final class ClassNameValue { private $typeHint; private $fullClassName; public function __construct(string $typeHint, string $fullClassName) { $this->typeHint = $typeHint; $this->fullClassName = $fullClassName; } public function getShortName(): string { if ($this->isSelf()) { return Str::getShortClassName($this->fullClassName); } return $this->typeHint; } public function isSelf(): bool { return 'self' === $this->typeHint; } public function __toString() { return $this->getShortName(); } } src/Util/ClassSourceManipulator.php 0000644 00000151271 15120141002 0013414 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Util; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping\Column; use Doctrine\ORM\Mapping\Embedded; use Doctrine\ORM\Mapping\JoinColumn; use Doctrine\ORM\Mapping\ManyToMany; use Doctrine\ORM\Mapping\ManyToOne; use Doctrine\ORM\Mapping\OneToMany; use Doctrine\ORM\Mapping\OneToOne; use PhpParser\Builder; use PhpParser\BuilderHelpers; use PhpParser\Comment\Doc; use PhpParser\Lexer; use PhpParser\Node; use PhpParser\NodeTraverser; use PhpParser\NodeVisitor; use PhpParser\Parser; use Symfony\Bundle\MakerBundle\ConsoleStyle; use Symfony\Bundle\MakerBundle\Doctrine\BaseCollectionRelation; use Symfony\Bundle\MakerBundle\Doctrine\BaseRelation; use Symfony\Bundle\MakerBundle\Doctrine\RelationManyToMany; use Symfony\Bundle\MakerBundle\Doctrine\RelationManyToOne; use Symfony\Bundle\MakerBundle\Doctrine\RelationOneToMany; use Symfony\Bundle\MakerBundle\Doctrine\RelationOneToOne; use Symfony\Bundle\MakerBundle\Str; /** * @internal */ final class ClassSourceManipulator { private const CONTEXT_OUTSIDE_CLASS = 'outside_class'; private const CONTEXT_CLASS = 'class'; private const CONTEXT_CLASS_METHOD = 'class_method'; private $overwrite; private $useAnnotations; private $fluentMutators; private $useAttributesForDoctrineMapping; private $parser; private $lexer; private $printer; /** @var ConsoleStyle|null */ private $io; private $sourceCode; private $oldStmts; private $oldTokens; private $newStmts; private $pendingComments = []; public function __construct(string $sourceCode, bool $overwrite = false, bool $useAnnotations = true, bool $fluentMutators = true, bool $useAttributesForDoctrineMapping = false) { $this->overwrite = $overwrite; $this->useAnnotations = $useAnnotations; $this->fluentMutators = $fluentMutators; $this->useAttributesForDoctrineMapping = $useAttributesForDoctrineMapping; $this->lexer = new Lexer\Emulative([ 'usedAttributes' => [ 'comments', 'startLine', 'endLine', 'startTokenPos', 'endTokenPos', ], ]); $this->parser = new Parser\Php7($this->lexer); $this->printer = new PrettyPrinter(); $this->setSourceCode($sourceCode); } public function setIo(ConsoleStyle $io): void { $this->io = $io; } public function getSourceCode(): string { return $this->sourceCode; } public function addEntityField(string $propertyName, array $columnOptions, array $comments = []): void { $typeHint = $this->getEntityTypeHint($columnOptions['type']); $nullable = $columnOptions['nullable'] ?? false; $isId = (bool) ($columnOptions['id'] ?? false); $attributes = []; if ($this->useAttributesForDoctrineMapping) { $attributes[] = $this->buildAttributeNode(Column::class, $columnOptions, 'ORM'); } else { $comments[] = $this->buildAnnotationLine('@ORM\Column', $columnOptions); } $defaultValue = null; if ('array' === $typeHint) { $defaultValue = new Node\Expr\Array_([], ['kind' => Node\Expr\Array_::KIND_SHORT]); } $this->addProperty($propertyName, $comments, $defaultValue, $attributes); $this->addGetter( $propertyName, $typeHint, // getter methods always have nullable return values // because even though these are required in the db, they may not be set yet true ); // don't generate setters for id fields if (!$isId) { $this->addSetter($propertyName, $typeHint, $nullable); } } public function addEmbeddedEntity(string $propertyName, string $className): void { $typeHint = $this->addUseStatementIfNecessary($className); $annotations = []; $attributes = []; if (!$this->useAttributesForDoctrineMapping) { $annotations = [ $this->buildAnnotationLine( '@ORM\\Embedded', [ 'class' => new ClassNameValue($className, $typeHint), ] ), ]; } else { $attributes = [ $this->buildAttributeNode( Embedded::class, ['class' => new ClassNameValue($className, $typeHint)], 'ORM' ), ]; } $this->addProperty($propertyName, $annotations, null, $attributes); // logic to avoid re-adding the same ArrayCollection line $addEmbedded = true; if ($this->getConstructorNode()) { // We print the constructor to a string, then // look for "$this->propertyName = " $constructorString = $this->printer->prettyPrint([$this->getConstructorNode()]); if (false !== strpos($constructorString, sprintf('$this->%s = ', $propertyName))) { $addEmbedded = false; } } if ($addEmbedded) { $this->addStatementToConstructor( new Node\Stmt\Expression(new Node\Expr\Assign( new Node\Expr\PropertyFetch(new Node\Expr\Variable('this'), $propertyName), new Node\Expr\New_(new Node\Name($typeHint)) )) ); } $this->addGetter($propertyName, $typeHint, false); $this->addSetter($propertyName, $typeHint, false); } public function addManyToOneRelation(RelationManyToOne $manyToOne): void { $this->addSingularRelation($manyToOne); } public function addOneToOneRelation(RelationOneToOne $oneToOne): void { $this->addSingularRelation($oneToOne); } public function addOneToManyRelation(RelationOneToMany $oneToMany): void { $this->addCollectionRelation($oneToMany); } public function addManyToManyRelation(RelationManyToMany $manyToMany): void { $this->addCollectionRelation($manyToMany); } public function addInterface(string $interfaceName): void { $this->addUseStatementIfNecessary($interfaceName); $this->getClassNode()->implements[] = new Node\Name(Str::getShortClassName($interfaceName)); $this->updateSourceCodeFromNewStmts(); } /** * @param string $trait the fully-qualified trait name */ public function addTrait(string $trait): void { $importedClassName = $this->addUseStatementIfNecessary($trait); /** @var Node\Stmt\TraitUse[] $traitNodes */ $traitNodes = $this->findAllNodes(function ($node) { return $node instanceof Node\Stmt\TraitUse; }); foreach ($traitNodes as $node) { if ($node->traits[0]->toString() === $importedClassName) { return; } } $traitNodes[] = new Node\Stmt\TraitUse([new Node\Name($importedClassName)]); $classNode = $this->getClassNode(); if (!empty($classNode->stmts) && 1 === \count($traitNodes)) { $traitNodes[] = $this->createBlankLineNode(self::CONTEXT_CLASS); } // avoid all the use traits in class for unshift all the new UseTrait // in the right order. foreach ($classNode->stmts as $key => $node) { if ($node instanceof Node\Stmt\TraitUse) { unset($classNode->stmts[$key]); } } array_unshift($classNode->stmts, ...$traitNodes); $this->updateSourceCodeFromNewStmts(); } public function addAccessorMethod(string $propertyName, string $methodName, $returnType, bool $isReturnTypeNullable, array $commentLines = [], $typeCast = null): void { $this->addCustomGetter($propertyName, $methodName, $returnType, $isReturnTypeNullable, $commentLines, $typeCast); } public function addGetter(string $propertyName, $returnType, bool $isReturnTypeNullable, array $commentLines = []): void { $methodName = ('bool' === $returnType ? 'is' : 'get').Str::asCamelCase($propertyName); $this->addCustomGetter($propertyName, $methodName, $returnType, $isReturnTypeNullable, $commentLines); } public function addSetter(string $propertyName, $type, bool $isNullable, array $commentLines = []): void { $builder = $this->createSetterNodeBuilder($propertyName, $type, $isNullable, $commentLines); $builder->addStmt( new Node\Stmt\Expression(new Node\Expr\Assign( new Node\Expr\PropertyFetch(new Node\Expr\Variable('this'), $propertyName), new Node\Expr\Variable($propertyName) )) ); $this->makeMethodFluent($builder); $this->addMethod($builder->getNode()); } /** * @param Node[] $params */ public function addConstructor(array $params, string $methodBody): void { if (null !== $this->getConstructorNode()) { throw new \LogicException('Constructor already exists.'); } $methodBuilder = $this->createMethodBuilder('__construct', null, false); $this->addMethodParams($methodBuilder, $params); $this->addMethodBody($methodBuilder, $methodBody); $this->addNodeAfterProperties($methodBuilder->getNode()); $this->updateSourceCodeFromNewStmts(); } /** * @param Node[] $params */ public function addMethodBuilder(Builder\Method $methodBuilder, array $params = [], string $methodBody = null): void { $this->addMethodParams($methodBuilder, $params); if ($methodBody) { $this->addMethodBody($methodBuilder, $methodBody); } $this->addMethod($methodBuilder->getNode()); } public function addMethodBody(Builder\Method $methodBuilder, string $methodBody): void { $nodes = $this->parser->parse($methodBody); $methodBuilder->addStmts($nodes); } public function createMethodBuilder(string $methodName, $returnType, bool $isReturnTypeNullable, array $commentLines = []): Builder\Method { $methodNodeBuilder = (new Builder\Method($methodName)) ->makePublic(); if (null !== $returnType) { if (class_exists($returnType) || interface_exists($returnType)) { $returnType = $this->addUseStatementIfNecessary($returnType); } $methodNodeBuilder->setReturnType($isReturnTypeNullable ? new Node\NullableType($returnType) : $returnType); } if ($commentLines) { $methodNodeBuilder->setDocComment($this->createDocBlock($commentLines)); } return $methodNodeBuilder; } public function createMethodLevelCommentNode(string $comment) { return $this->createSingleLineCommentNode($comment, self::CONTEXT_CLASS_METHOD); } public function createMethodLevelBlankLine() { return $this->createBlankLineNode(self::CONTEXT_CLASS_METHOD); } /** * @param array<Node\Attribute|Node\AttributeGroup> $attributes */ public function addProperty(string $name, array $annotationLines = [], $defaultValue = null, array $attributes = []): void { if ($this->propertyExists($name)) { // we never overwrite properties return; } $newPropertyBuilder = (new Builder\Property($name))->makePrivate(); if ($this->useAttributesForDoctrineMapping) { foreach ($attributes as $attribute) { $newPropertyBuilder->addAttribute($attribute); } } elseif ($annotationLines && $this->useAnnotations) { $newPropertyBuilder->setDocComment($this->createDocBlock($annotationLines)); } if (null !== $defaultValue) { $newPropertyBuilder->setDefault($defaultValue); } $newPropertyNode = $newPropertyBuilder->getNode(); $this->addNodeAfterProperties($newPropertyNode); } public function addAttributeToClass(string $attributeClass, array $options): void { $this->addUseStatementIfNecessary($attributeClass); $classNode = $this->getClassNode(); $classNode->attrGroups[] = new Node\AttributeGroup([$this->buildAttributeNode($attributeClass, $options)]); $this->updateSourceCodeFromNewStmts(); } public function addAnnotationToClass(string $annotationClass, array $options): void { $annotationClassAlias = $this->addUseStatementIfNecessary($annotationClass); $docComment = $this->getClassNode()->getDocComment(); $docLines = $docComment ? explode("\n", $docComment->getText()) : []; if (0 === \count($docLines)) { $docLines = ['/**', ' */']; } elseif (1 === \count($docLines)) { // /** inline doc syntax */ // imperfect way to try to find where to split the lines $endOfOpening = strpos($docLines[0], '* '); $endingPosition = strrpos($docLines[0], ' *', $endOfOpening); $extraComments = trim(substr($docLines[0], $endOfOpening + 2, $endingPosition - $endOfOpening - 2)); $newDocLines = [ substr($docLines[0], 0, $endOfOpening + 1), ]; if ($extraComments) { $newDocLines[] = ' * '.$extraComments; } $newDocLines[] = substr($docLines[0], $endingPosition); $docLines = $newDocLines; } array_splice( $docLines, \count($docLines) - 1, 0, ' * '.$this->buildAnnotationLine('@'.$annotationClassAlias, $options) ); $docComment = new Doc(implode("\n", $docLines)); $this->getClassNode()->setDocComment($docComment); $this->updateSourceCodeFromNewStmts(); } private function addCustomGetter(string $propertyName, string $methodName, $returnType, bool $isReturnTypeNullable, array $commentLines = [], $typeCast = null): void { $propertyFetch = new Node\Expr\PropertyFetch(new Node\Expr\Variable('this'), $propertyName); if (null !== $typeCast) { switch ($typeCast) { case 'string': $propertyFetch = new Node\Expr\Cast\String_($propertyFetch); break; default: // implement other cases if/when the library needs them throw new \Exception('Not implemented'); } } $getterNodeBuilder = (new Builder\Method($methodName)) ->makePublic() ->addStmt( new Node\Stmt\Return_($propertyFetch) ); if (null !== $returnType) { $getterNodeBuilder->setReturnType($isReturnTypeNullable ? new Node\NullableType($returnType) : $returnType); } if ($commentLines) { $getterNodeBuilder->setDocComment($this->createDocBlock($commentLines)); } $this->addMethod($getterNodeBuilder->getNode()); } private function createSetterNodeBuilder(string $propertyName, $type, bool $isNullable, array $commentLines = []): Builder\Method { $methodName = 'set'.Str::asCamelCase($propertyName); $setterNodeBuilder = (new Builder\Method($methodName))->makePublic(); if ($commentLines) { $setterNodeBuilder->setDocComment($this->createDocBlock($commentLines)); } $paramBuilder = new Builder\Param($propertyName); if (null !== $type) { $paramBuilder->setTypeHint($isNullable ? new Node\NullableType($type) : $type); } $setterNodeBuilder->addParam($paramBuilder->getNode()); return $setterNodeBuilder; } /** * @param string $annotationClass The annotation: e.g. "@ORM\Column" * @param array $options Key-value pair of options for the annotation */ private function buildAnnotationLine(string $annotationClass, array $options): string { $formattedOptions = array_map(function ($option, $value) { if (\is_array($value)) { if (!isset($value[0])) { return sprintf('%s={%s}', $option, implode(', ', array_map(function ($val, $key) { return sprintf('"%s" = %s', $key, $this->quoteAnnotationValue($val)); }, $value, array_keys($value)))); } return sprintf('%s={%s}', $option, implode(', ', array_map(function ($val) { return $this->quoteAnnotationValue($val); }, $value))); } return sprintf('%s=%s', $option, $this->quoteAnnotationValue($value)); }, array_keys($options), array_values($options)); return sprintf('%s(%s)', $annotationClass, implode(', ', $formattedOptions)); } private function quoteAnnotationValue($value) { if (\is_bool($value)) { return $value ? 'true' : 'false'; } if (null === $value) { return 'null'; } if (\is_int($value) || '0' === $value) { return $value; } if ($value instanceof ClassNameValue) { return sprintf('%s::class', $value->getShortName()); } if (\is_array($value)) { throw new \Exception('Invalid value: loop before quoting.'); } if (\function_exists('enum_exists')) { // do we have an enum ? if (\is_object($value) && enum_exists(\get_class($value))) { $value = $value->value; } } return sprintf('"%s"', $value); } private function addSingularRelation(BaseRelation $relation): void { $typeHint = $this->addUseStatementIfNecessary($relation->getTargetClassName()); if ($relation->getTargetClassName() == $this->getThisFullClassName()) { $typeHint = 'self'; } $annotationOptions = [ 'targetEntity' => new ClassNameValue($typeHint, $relation->getTargetClassName()), ]; if ($relation->isOwning()) { // sometimes, we don't map the inverse relation if ($relation->getMapInverseRelation()) { $annotationOptions['inversedBy'] = $relation->getTargetPropertyName(); } } else { $annotationOptions['mappedBy'] = $relation->getTargetPropertyName(); } if ($relation instanceof RelationOneToOne) { $annotationOptions['cascade'] = ['persist', 'remove']; } $annotations = []; $attributes = []; if (!$this->useAttributesForDoctrineMapping) { $annotations = [ $this->buildAnnotationLine( $relation instanceof RelationManyToOne ? '@ORM\\ManyToOne' : '@ORM\\OneToOne', $annotationOptions ), ]; } else { $attributes = [ $this->buildAttributeNode( $relation instanceof RelationManyToOne ? ManyToOne::class : OneToOne::class, $annotationOptions, 'ORM' ), ]; } if (!$relation->isNullable() && $relation->isOwning()) { if (!$this->useAttributesForDoctrineMapping) { $annotations[] = $this->buildAnnotationLine('@ORM\\JoinColumn', [ 'nullable' => false, ]); } else { $attributes[] = $this->buildAttributeNode(JoinColumn::class, ['nullable' => false], 'ORM'); } } $this->addProperty($relation->getPropertyName(), $annotations, null, $attributes); $this->addGetter( $relation->getPropertyName(), $relation->getCustomReturnType() ?: $typeHint, // getter methods always have nullable return values // unless this has been customized explicitly $relation->getCustomReturnType() ? $relation->isCustomReturnTypeNullable() : true ); if ($relation->shouldAvoidSetter()) { return; } $setterNodeBuilder = $this->createSetterNodeBuilder( $relation->getPropertyName(), $typeHint, // make the type-hint nullable always for ManyToOne to allow the owning // side to be set to null, which is needed for orphanRemoval // (specifically: when you set the inverse side, the generated // code will *also* set the owning side to null - so it needs to be allowed) // e.g. $userAvatarPhoto->setUser(null); $relation instanceof RelationOneToOne ? $relation->isNullable() : true ); // set the *owning* side of the relation // OneToOne is the only "singular" relation type that // may be the inverse side if ($relation instanceof RelationOneToOne && !$relation->isOwning()) { $this->addNodesToSetOtherSideOfOneToOne($relation, $setterNodeBuilder); } $setterNodeBuilder->addStmt( new Node\Stmt\Expression(new Node\Expr\Assign( new Node\Expr\PropertyFetch(new Node\Expr\Variable('this'), $relation->getPropertyName()), new Node\Expr\Variable($relation->getPropertyName()) )) ); $this->makeMethodFluent($setterNodeBuilder); $this->addMethod($setterNodeBuilder->getNode()); } private function addCollectionRelation(BaseCollectionRelation $relation): void { $typeHint = $relation->isSelfReferencing() ? 'self' : $this->addUseStatementIfNecessary($relation->getTargetClassName()); $arrayCollectionTypeHint = $this->addUseStatementIfNecessary(ArrayCollection::class); $collectionTypeHint = $this->addUseStatementIfNecessary(Collection::class); $annotationOptions = [ 'targetEntity' => new ClassNameValue($typeHint, $relation->getTargetClassName()), ]; if ($relation->isOwning()) { // sometimes, we don't map the inverse relation if ($relation->getMapInverseRelation()) { $annotationOptions['inversedBy'] = $relation->getTargetPropertyName(); } } else { $annotationOptions['mappedBy'] = $relation->getTargetPropertyName(); } if ($relation->getOrphanRemoval()) { $annotationOptions['orphanRemoval'] = true; } $annotations = []; $attributes = []; if (!$this->useAttributesForDoctrineMapping) { $annotations = [ $this->buildAnnotationLine( $relation instanceof RelationManyToMany ? '@ORM\\ManyToMany' : '@ORM\\OneToMany', $annotationOptions ), ]; } else { $attributes = [ $this->buildAttributeNode( $relation instanceof RelationManyToMany ? ManyToMany::class : OneToMany::class, $annotationOptions, 'ORM' ), ]; } $this->addProperty($relation->getPropertyName(), $annotations, null, $attributes); // logic to avoid re-adding the same ArrayCollection line $addArrayCollection = true; if ($this->getConstructorNode()) { // We print the constructor to a string, then // look for "$this->propertyName = " $constructorString = $this->printer->prettyPrint([$this->getConstructorNode()]); if (false !== strpos($constructorString, sprintf('$this->%s = ', $relation->getPropertyName()))) { $addArrayCollection = false; } } if ($addArrayCollection) { $this->addStatementToConstructor( new Node\Stmt\Expression(new Node\Expr\Assign( new Node\Expr\PropertyFetch(new Node\Expr\Variable('this'), $relation->getPropertyName()), new Node\Expr\New_(new Node\Name($arrayCollectionTypeHint)) )) ); } $this->addGetter( $relation->getPropertyName(), $collectionTypeHint, false, // add @return that advertises this as a collection of specific objects [sprintf('@return %s<int, %s>', $collectionTypeHint, $typeHint)] ); $argName = Str::pluralCamelCaseToSingular($relation->getPropertyName()); // adder method $adderNodeBuilder = (new Builder\Method($relation->getAdderMethodName()))->makePublic(); $paramBuilder = new Builder\Param($argName); $paramBuilder->setTypeHint($typeHint); $adderNodeBuilder->addParam($paramBuilder->getNode()); // if (!$this->avatars->contains($avatar)) $containsMethodCallNode = new Node\Expr\MethodCall( new Node\Expr\PropertyFetch(new Node\Expr\Variable('this'), $relation->getPropertyName()), 'contains', [new Node\Expr\Variable($argName)] ); $ifNotContainsStmt = new Node\Stmt\If_( new Node\Expr\BooleanNot($containsMethodCallNode) ); $adderNodeBuilder->addStmt($ifNotContainsStmt); // append the item $ifNotContainsStmt->stmts[] = new Node\Stmt\Expression( new Node\Expr\Assign( new Node\Expr\ArrayDimFetch( new Node\Expr\PropertyFetch(new Node\Expr\Variable('this'), $relation->getPropertyName()) ), new Node\Expr\Variable($argName) )); // set the owning side of the relationship if (!$relation->isOwning()) { $ifNotContainsStmt->stmts[] = new Node\Stmt\Expression( new Node\Expr\MethodCall( new Node\Expr\Variable($argName), $relation->getTargetSetterMethodName(), [new Node\Expr\Variable('this')] ) ); } $this->makeMethodFluent($adderNodeBuilder); $this->addMethod($adderNodeBuilder->getNode()); /* * Remover */ $removerNodeBuilder = (new Builder\Method($relation->getRemoverMethodName()))->makePublic(); $paramBuilder = new Builder\Param($argName); $paramBuilder->setTypeHint($typeHint); $removerNodeBuilder->addParam($paramBuilder->getNode()); // $this->avatars->removeElement($avatar) $removeElementCall = new Node\Expr\MethodCall( new Node\Expr\PropertyFetch(new Node\Expr\Variable('this'), $relation->getPropertyName()), 'removeElement', [new Node\Expr\Variable($argName)] ); // set the owning side of the relationship if ($relation->isOwning()) { // $this->avatars->removeElement($avatar); $removerNodeBuilder->addStmt(BuilderHelpers::normalizeStmt($removeElementCall)); } else { // if ($this->avatars->removeElement($avatar)) $ifRemoveElementStmt = new Node\Stmt\If_($removeElementCall); $removerNodeBuilder->addStmt($ifRemoveElementStmt); if ($relation instanceof RelationOneToMany) { // OneToMany: $student->setCourse(null); /* * // set the owning side to null (unless already changed) * if ($student->getCourse() === $this) { * $student->setCourse(null); * } */ $ifRemoveElementStmt->stmts[] = $this->createSingleLineCommentNode( 'set the owning side to null (unless already changed)', self::CONTEXT_CLASS_METHOD ); // if ($student->getCourse() === $this) { $ifNode = new Node\Stmt\If_(new Node\Expr\BinaryOp\Identical( new Node\Expr\MethodCall( new Node\Expr\Variable($argName), $relation->getTargetGetterMethodName() ), new Node\Expr\Variable('this') )); // $student->setCourse(null); $ifNode->stmts = [ new Node\Stmt\Expression(new Node\Expr\MethodCall( new Node\Expr\Variable($argName), $relation->getTargetSetterMethodName(), [new Node\Arg($this->createNullConstant())] )), ]; $ifRemoveElementStmt->stmts[] = $ifNode; } elseif ($relation instanceof RelationManyToMany) { // $student->removeCourse($this); $ifRemoveElementStmt->stmts[] = new Node\Stmt\Expression( new Node\Expr\MethodCall( new Node\Expr\Variable($argName), $relation->getTargetRemoverMethodName(), [new Node\Expr\Variable('this')] ) ); } else { throw new \Exception('Unknown relation type'); } } $this->makeMethodFluent($removerNodeBuilder); $this->addMethod($removerNodeBuilder->getNode()); } private function addStatementToConstructor(Node\Stmt $stmt): void { if (!$this->getConstructorNode()) { $constructorNode = (new Builder\Method('__construct'))->makePublic()->getNode(); // add call to parent::__construct() if there is a need to try { $ref = new \ReflectionClass($this->getThisFullClassName()); if ($ref->getParentClass() && $ref->getParentClass()->getConstructor()) { $constructorNode->stmts[] = new Node\Stmt\Expression( new Node\Expr\StaticCall(new Node\Name('parent'), new Node\Identifier('__construct')) ); } } catch (\ReflectionException $e) { } $this->addNodeAfterProperties($constructorNode); } $constructorNode = $this->getConstructorNode(); $constructorNode->stmts[] = $stmt; $this->updateSourceCodeFromNewStmts(); } /** * @throws \Exception */ private function getConstructorNode(): ?Node\Stmt\ClassMethod { foreach ($this->getClassNode()->stmts as $classNode) { if ($classNode instanceof Node\Stmt\ClassMethod && '__construct' == $classNode->name) { return $classNode; } } return null; } /** * @return string The alias to use when referencing this class */ public function addUseStatementIfNecessary(string $class): string { $shortClassName = Str::getShortClassName($class); if ($this->isInSameNamespace($class)) { return $shortClassName; } $namespaceNode = $this->getNamespaceNode(); $targetIndex = null; $addLineBreak = false; $lastUseStmtIndex = null; foreach ($namespaceNode->stmts as $index => $stmt) { if ($stmt instanceof Node\Stmt\Use_) { // I believe this is an array to account for use statements with {} foreach ($stmt->uses as $use) { $alias = $use->alias ? $use->alias->name : $use->name->getLast(); // the use statement already exists? Don't add it again if ($class === (string) $use->name) { return $alias; } if ($alias === $shortClassName) { // we have a conflicting alias! // to be safe, use the fully-qualified class name // everywhere and do not add another use statement return '\\'.$class; } } // if $class is alphabetically before this use statement, place it before // only set $targetIndex the first time you find it if (null === $targetIndex && Str::areClassesAlphabetical($class, (string) $stmt->uses[0]->name)) { $targetIndex = $index; } $lastUseStmtIndex = $index; } elseif ($stmt instanceof Node\Stmt\Class_) { if (null !== $targetIndex) { // we already found where to place the use statement break; } // we hit the class! If there were any use statements, // then put this at the bottom of the use statement list if (null !== $lastUseStmtIndex) { $targetIndex = $lastUseStmtIndex + 1; } else { $targetIndex = $index; $addLineBreak = true; } break; } } if (null === $targetIndex) { throw new \Exception('Could not find a class!'); } $newUseNode = (new Builder\Use_($class, Node\Stmt\Use_::TYPE_NORMAL))->getNode(); array_splice( $namespaceNode->stmts, $targetIndex, 0, $addLineBreak ? [$newUseNode, $this->createBlankLineNode(self::CONTEXT_OUTSIDE_CLASS)] : [$newUseNode] ); $this->updateSourceCodeFromNewStmts(); return $shortClassName; } /** * Builds a PHPParser attribute node. * * @param string $attributeClass The attribute class which should be used for the attribute E.g. #[Column()] * @param array $options The named arguments for the attribute ($key = argument name, $value = argument value) * @param ?string $attributePrefix If a prefix is provided, the node is built using the prefix. E.g. #[ORM\Column()] */ public function buildAttributeNode(string $attributeClass, array $options, ?string $attributePrefix = null): Node\Attribute { $options = $this->sortOptionsByClassConstructorParameters($options, $attributeClass); $context = $this; $nodeArguments = array_map(static function ($option, $value) use ($context) { return new Node\Arg($context->buildNodeExprByValue($value), false, false, [], new Node\Identifier($option)); }, array_keys($options), array_values($options)); $class = $attributePrefix ? sprintf('%s\\%s', $attributePrefix, Str::getShortClassName($attributeClass)) : Str::getShortClassName($attributeClass); return new Node\Attribute( new Node\Name($class), $nodeArguments ); } private function updateSourceCodeFromNewStmts(): void { $newCode = $this->printer->printFormatPreserving( $this->newStmts, $this->oldStmts, $this->oldTokens ); // replace the 3 "fake" items that may be in the code (allowing for different indentation) $newCode = preg_replace('/(\ |\t)*private\ \$__EXTRA__LINE;/', '', $newCode); $newCode = preg_replace('/use __EXTRA__LINE;/', '', $newCode); $newCode = preg_replace('/(\ |\t)*\$__EXTRA__LINE;/', '', $newCode); // process comment lines foreach ($this->pendingComments as $i => $comment) { // sanity check $placeholder = sprintf('$__COMMENT__VAR_%d;', $i); if (false === strpos($newCode, $placeholder)) { // this can happen if a comment is createSingleLineCommentNode() // is called, but then that generated code is ultimately not added continue; } $newCode = str_replace($placeholder, '// '.$comment, $newCode); } $this->pendingComments = []; $this->setSourceCode($newCode); } private function setSourceCode(string $sourceCode): void { $this->sourceCode = $sourceCode; $this->oldStmts = $this->parser->parse($sourceCode); $this->oldTokens = $this->lexer->getTokens(); $traverser = new NodeTraverser(); $traverser->addVisitor(new NodeVisitor\CloningVisitor()); $traverser->addVisitor(new NodeVisitor\NameResolver(null, [ 'replaceNodes' => false, ])); $this->newStmts = $traverser->traverse($this->oldStmts); } private function getClassNode(): Node\Stmt\Class_ { $node = $this->findFirstNode(function ($node) { return $node instanceof Node\Stmt\Class_; }); if (!$node) { throw new \Exception('Could not find class node'); } return $node; } private function getNamespaceNode(): Node\Stmt\Namespace_ { $node = $this->findFirstNode(function ($node) { return $node instanceof Node\Stmt\Namespace_; }); if (!$node) { throw new \Exception('Could not find namespace node'); } return $node; } private function findFirstNode(callable $filterCallback): ?Node { $traverser = new NodeTraverser(); $visitor = new NodeVisitor\FirstFindingVisitor($filterCallback); $traverser->addVisitor($visitor); $traverser->traverse($this->newStmts); return $visitor->getFoundNode(); } private function findLastNode(callable $filterCallback, array $ast): ?Node { $traverser = new NodeTraverser(); $visitor = new NodeVisitor\FindingVisitor($filterCallback); $traverser->addVisitor($visitor); $traverser->traverse($ast); $nodes = $visitor->getFoundNodes(); $node = end($nodes); return false === $node ? null : $node; } /** * @return Node[] */ private function findAllNodes(callable $filterCallback): array { $traverser = new NodeTraverser(); $visitor = new NodeVisitor\FindingVisitor($filterCallback); $traverser->addVisitor($visitor); $traverser->traverse($this->newStmts); return $visitor->getFoundNodes(); } private function createBlankLineNode(string $context) { switch ($context) { case self::CONTEXT_OUTSIDE_CLASS: return (new Builder\Use_('__EXTRA__LINE', Node\Stmt\Use_::TYPE_NORMAL)) ->getNode(); case self::CONTEXT_CLASS: return (new Builder\Property('__EXTRA__LINE')) ->makePrivate() ->getNode(); case self::CONTEXT_CLASS_METHOD: return new Node\Expr\Variable('__EXTRA__LINE'); default: throw new \Exception('Unknown context: '.$context); } } private function createSingleLineCommentNode(string $comment, string $context): Node\Stmt { $this->pendingComments[] = $comment; switch ($context) { case self::CONTEXT_OUTSIDE_CLASS: // just not needed yet throw new \Exception('not supported'); case self::CONTEXT_CLASS: // just not needed yet throw new \Exception('not supported'); case self::CONTEXT_CLASS_METHOD: return BuilderHelpers::normalizeStmt(new Node\Expr\Variable(sprintf('__COMMENT__VAR_%d', \count($this->pendingComments) - 1))); default: throw new \Exception('Unknown context: '.$context); } } private function createDocBlock(array $commentLines): string { $docBlock = "/**\n"; foreach ($commentLines as $commentLine) { if ($commentLine) { $docBlock .= " * $commentLine\n"; } else { // avoid the empty, extra space on blank lines $docBlock .= " *\n"; } } $docBlock .= "\n */"; return $docBlock; } private function addMethod(Node\Stmt\ClassMethod $methodNode): void { $classNode = $this->getClassNode(); $methodName = $methodNode->name; $existingIndex = null; if ($this->methodExists($methodName)) { if (!$this->overwrite) { $this->writeNote(sprintf( 'Not generating <info>%s::%s()</info>: method already exists', Str::getShortClassName($this->getThisFullClassName()), $methodName )); return; } // record, so we can overwrite in the same place $existingIndex = $this->getMethodIndex($methodName); } $newStatements = []; // put new method always at the bottom if (!empty($classNode->stmts)) { $newStatements[] = $this->createBlankLineNode(self::CONTEXT_CLASS); } $newStatements[] = $methodNode; if (null === $existingIndex) { // add them to the end! $classNode->stmts = array_merge($classNode->stmts, $newStatements); } else { array_splice( $classNode->stmts, $existingIndex, 1, $newStatements ); } $this->updateSourceCodeFromNewStmts(); } private function makeMethodFluent(Builder\Method $methodBuilder): void { if (!$this->fluentMutators) { return; } $methodBuilder ->addStmt($this->createBlankLineNode(self::CONTEXT_CLASS_METHOD)) ->addStmt(new Node\Stmt\Return_(new Node\Expr\Variable('this'))); $methodBuilder->setReturnType('self'); } private function getEntityTypeHint($doctrineType): ?string { switch ($doctrineType) { case 'string': case 'text': case 'guid': case 'bigint': case 'decimal': return 'string'; case 'array': case 'simple_array': case 'json': case 'json_array': return 'array'; case 'boolean': return 'bool'; case 'integer': case 'smallint': return 'int'; case 'float': return 'float'; case 'datetime': case 'datetimetz': case 'date': case 'time': return '\\'.\DateTimeInterface::class; case 'datetime_immutable': case 'datetimetz_immutable': case 'date_immutable': case 'time_immutable': return '\\'.\DateTimeImmutable::class; case 'dateinterval': return '\\'.\DateInterval::class; case 'object': return 'object'; case 'binary': case 'blob': default: return null; } } private function isInSameNamespace($class): bool { $namespace = substr($class, 0, strrpos($class, '\\')); return $this->getNamespaceNode()->name->toCodeString() === $namespace; } private function getThisFullClassName(): string { return (string) $this->getClassNode()->namespacedName; } /** * Adds this new node where a new property should go. * * Useful for adding properties, or adding a constructor. */ private function addNodeAfterProperties(Node $newNode): void { $classNode = $this->getClassNode(); // try to add after last property $targetNode = $this->findLastNode(function ($node) { return $node instanceof Node\Stmt\Property; }, [$classNode]); // otherwise, try to add after the last constant if (!$targetNode) { $targetNode = $this->findLastNode(function ($node) { return $node instanceof Node\Stmt\ClassConst; }, [$classNode]); } // otherwise, try to add after the last trait if (!$targetNode) { $targetNode = $this->findLastNode(function ($node) { return $node instanceof Node\Stmt\TraitUse; }, [$classNode]); } // add the new property after this node if ($targetNode) { $index = array_search($targetNode, $classNode->stmts); array_splice( $classNode->stmts, $index + 1, 0, [$this->createBlankLineNode(self::CONTEXT_CLASS), $newNode] ); $this->updateSourceCodeFromNewStmts(); return; } // put right at the beginning of the class // add an empty line, unless the class is totally empty if (!empty($classNode->stmts)) { array_unshift($classNode->stmts, $this->createBlankLineNode(self::CONTEXT_CLASS)); } array_unshift($classNode->stmts, $newNode); $this->updateSourceCodeFromNewStmts(); } private function createNullConstant(): Node\Expr\ConstFetch { return new Node\Expr\ConstFetch(new Node\Name('null')); } private function addNodesToSetOtherSideOfOneToOne(RelationOneToOne $relation, Builder\Method $setterNodeBuilder): void { if (!$relation->isNullable()) { $setterNodeBuilder->addStmt($this->createSingleLineCommentNode( 'set the owning side of the relation if necessary', self::CONTEXT_CLASS_METHOD )); // if ($user->getUserProfile() !== $this) { $ifNode = new Node\Stmt\If_(new Node\Expr\BinaryOp\NotIdentical( new Node\Expr\MethodCall( new Node\Expr\Variable($relation->getPropertyName()), $relation->getTargetGetterMethodName() ), new Node\Expr\Variable('this') )); // $user->setUserProfile($this); $ifNode->stmts = [ new Node\Stmt\Expression(new Node\Expr\MethodCall( new Node\Expr\Variable($relation->getPropertyName()), $relation->getTargetSetterMethodName(), [new Node\Arg(new Node\Expr\Variable('this'))] )), ]; $setterNodeBuilder->addStmt($ifNode); $setterNodeBuilder->addStmt($this->createBlankLineNode(self::CONTEXT_CLASS_METHOD)); return; } // at this point, we know the relation is nullable $setterNodeBuilder->addStmt($this->createSingleLineCommentNode( 'unset the owning side of the relation if necessary', self::CONTEXT_CLASS_METHOD )); // if ($user !== null && $user->getUserProfile() !== $this) $ifNode = new Node\Stmt\If_(new Node\Expr\BinaryOp\BooleanAnd( new Node\Expr\BinaryOp\Identical( new Node\Expr\Variable($relation->getPropertyName()), $this->createNullConstant() ), new Node\Expr\BinaryOp\NotIdentical( new Node\Expr\PropertyFetch( new Node\Expr\Variable('this'), $relation->getPropertyName() ), $this->createNullConstant() ) )); $ifNode->stmts = [ // $this->user->setUserProfile(null) new Node\Stmt\Expression(new Node\Expr\MethodCall( new Node\Expr\PropertyFetch( new Node\Expr\Variable('this'), $relation->getPropertyName() ), $relation->getTargetSetterMethodName(), [new Node\Arg($this->createNullConstant())] )), ]; $setterNodeBuilder->addStmt($ifNode); $setterNodeBuilder->addStmt($this->createBlankLineNode(self::CONTEXT_CLASS_METHOD)); $setterNodeBuilder->addStmt($this->createSingleLineCommentNode( 'set the owning side of the relation if necessary', self::CONTEXT_CLASS_METHOD )); // if ($user === null && $this->user !== null) $ifNode = new Node\Stmt\If_(new Node\Expr\BinaryOp\BooleanAnd( new Node\Expr\BinaryOp\NotIdentical( new Node\Expr\Variable($relation->getPropertyName()), $this->createNullConstant() ), new Node\Expr\BinaryOp\NotIdentical( new Node\Expr\MethodCall( new Node\Expr\Variable($relation->getPropertyName()), $relation->getTargetGetterMethodName() ), new Node\Expr\Variable('this') ) )); $ifNode->stmts = [ new Node\Stmt\Expression(new Node\Expr\MethodCall( new Node\Expr\Variable($relation->getPropertyName()), $relation->getTargetSetterMethodName(), [new Node\Arg(new Node\Expr\Variable('this'))] )), ]; $setterNodeBuilder->addStmt($ifNode); $setterNodeBuilder->addStmt($this->createBlankLineNode(self::CONTEXT_CLASS_METHOD)); } private function methodExists(string $methodName): bool { return false !== $this->getMethodIndex($methodName); } private function getMethodIndex(string $methodName) { foreach ($this->getClassNode()->stmts as $i => $node) { if ($node instanceof Node\Stmt\ClassMethod && strtolower($node->name->toString()) === strtolower($methodName)) { return $i; } } return false; } private function propertyExists(string $propertyName): bool { foreach ($this->getClassNode()->stmts as $i => $node) { if ($node instanceof Node\Stmt\Property && $node->props[0]->name->toString() === $propertyName) { return true; } } return false; } private function writeNote(string $note): void { if (null !== $this->io) { $this->io->text($note); } } private function addMethodParams(Builder\Method $methodBuilder, array $params): void { foreach ($params as $param) { $methodBuilder->addParam($param); } } /** * builds a PHPParser Expr Node based on the value given in $value * throws an Exception when the given $value is not resolvable by this method. * * @param mixed $value * * @throws \Exception */ private function buildNodeExprByValue($value): Node\Expr { switch (\gettype($value)) { case 'string': $nodeValue = new Node\Scalar\String_($value); break; case 'integer': $nodeValue = new Node\Scalar\LNumber($value); break; case 'double': $nodeValue = new Node\Scalar\DNumber($value); break; case 'boolean': $nodeValue = new Node\Expr\ConstFetch(new Node\Name($value ? 'true' : 'false')); break; case 'array': $context = $this; $arrayItems = array_map(static function ($key, $value) use ($context) { return new Node\Expr\ArrayItem( $context->buildNodeExprByValue($value), !\is_int($key) ? $context->buildNodeExprByValue($key) : null ); }, array_keys($value), array_values($value)); $nodeValue = new Node\Expr\Array_($arrayItems, ['kind' => Node\Expr\Array_::KIND_SHORT]); break; default: $nodeValue = null; } if (null === $nodeValue) { if ($value instanceof ClassNameValue) { $nodeValue = new Node\Expr\ConstFetch( new Node\Name( sprintf('%s::class', $value->isSelf() ? 'self' : $value->getShortName()) ) ); } else { throw new \Exception(sprintf('Cannot build a node expr for value of type "%s"', \gettype($value))); } } return $nodeValue; } /** * sort the given options based on the constructor parameters for the given $classString * this prevents code inspections warnings for IDEs like intellij/phpstorm. * * option keys that are not found in the constructor will be added at the end of the sorted array */ private function sortOptionsByClassConstructorParameters(array $options, string $classString): array { if ('ORM\\' === substr($classString, 0, 4)) { $classString = sprintf('Doctrine\\ORM\\Mapping\\%s', substr($classString, 4)); } $constructorParameterNames = array_map(static function (\ReflectionParameter $reflectionParameter) { return $reflectionParameter->getName(); }, (new \ReflectionClass($classString))->getConstructor()->getParameters()); $sorted = []; foreach ($constructorParameterNames as $name) { if (\array_key_exists($name, $options)) { $sorted[$name] = $options[$name]; unset($options[$name]); } } return array_merge($sorted, $options); } } src/Util/ComposeFileManipulator.php 0000644 00000007632 15120141002 0013374 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Util; use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException; use Symfony\Component\Yaml\Dumper; /** * Manipulate Docker Compose Files. * * @author Jesse Rushlow <jr@rushlow.dev> * * @internal * @final */ class ComposeFileManipulator { public const COMPOSE_FILE_VERSION = '3.7'; /** @var YamlSourceManipulator */ private $manipulator; public function __construct(string $contents) { if ('' === $contents) { $this->manipulator = new YamlSourceManipulator( (new Dumper())->dump($this->getBasicStructure(), 2) ); } else { $this->manipulator = new YamlSourceManipulator($contents); } $this->checkComposeFileVersion(); } public function getComposeData(): array { return $this->manipulator->getData(); } public function getDataString(): string { return $this->manipulator->getContents(); } public function serviceExists(string $name): bool { $data = $this->manipulator->getData(); if (\array_key_exists('services', $data)) { return \array_key_exists($name, $data['services']); } return false; } public function addDockerService(string $name, array $details): void { $data = $this->manipulator->getData(); $data['services'][$name] = $details; $this->manipulator->setData($data); } public function removeDockerService(string $name): void { $data = $this->manipulator->getData(); unset($data['services'][$name]); $this->manipulator->setData($data); } public function exposePorts(string $service, array $ports): void { $portData = []; $portData[] = sprintf('%s To allow the host machine to access the ports below, modify the lines below.', YamlSourceManipulator::COMMENT_PLACEHOLDER_VALUE); $portData[] = sprintf('%s For example, to allow the host to connect to port 3306 on the container, you would change', YamlSourceManipulator::COMMENT_PLACEHOLDER_VALUE); $portData[] = sprintf('%s "3306" to "3306:3306". Where the first port is exposed to the host and the second is the container port.', YamlSourceManipulator::COMMENT_PLACEHOLDER_VALUE); $portData[] = sprintf('%s See https://docs.docker.com/compose/compose-file/compose-file-v3/#ports for more information.', YamlSourceManipulator::COMMENT_PLACEHOLDER_VALUE); foreach ($ports as $port) { $portData[] = $port; } $data = $this->manipulator->getData(); $data['services'][$service]['ports'] = $portData; $this->manipulator->setData($data); } public function addVolume(string $service, string $hostPath, string $containerPath): void { $data = $this->manipulator->getData(); $data['services'][$service]['volumes'][] = sprintf('%s:%s', $hostPath, $containerPath); $this->manipulator->setData($data); } private function getBasicStructure(string $version = self::COMPOSE_FILE_VERSION): array { return [ 'version' => $version, 'services' => [], ]; } private function checkComposeFileVersion(): void { $data = $this->manipulator->getData(); if (empty($data['version'])) { throw new RuntimeCommandException('docker-compose.yaml file version is not set.'); } if (2.0 > (float) $data['version']) { throw new RuntimeCommandException(sprintf('docker-compose.yaml version %s is not supported. Please update your docker-compose.yaml file to the latest version.', $data['version'])); } } } src/Util/ComposerAutoloaderFinder.php 0000644 00000006411 15120141002 0013704 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Util; use Composer\Autoload\ClassLoader; use Symfony\Component\Debug\DebugClassLoader; use Symfony\Component\ErrorHandler\DebugClassLoader as ErrorHandlerDebugClassLoader; /** * @internal */ class ComposerAutoloaderFinder { private $rootNamespace; /** * @var ClassLoader|null */ private $classLoader = null; public function __construct(string $rootNamespace) { $this->rootNamespace = [ 'psr0' => rtrim($rootNamespace, '\\'), 'psr4' => rtrim($rootNamespace, '\\').'\\', ]; } public function getClassLoader(): ClassLoader { if (null === $this->classLoader) { $this->classLoader = $this->findComposerClassLoader(); } if (null === $this->classLoader) { throw new \Exception("Could not find a Composer autoloader that autoloads from '{$this->rootNamespace['psr4']}'"); } return $this->classLoader; } private function findComposerClassLoader(): ?ClassLoader { $autoloadFunctions = spl_autoload_functions(); foreach ($autoloadFunctions as $autoloader) { if (!\is_array($autoloader)) { continue; } $classLoader = $this->extractComposerClassLoader($autoloader); if (null === $classLoader) { continue; } $finalClassLoader = $this->locateMatchingClassLoader($classLoader); if (null !== $finalClassLoader) { return $finalClassLoader; } } return null; } private function extractComposerClassLoader(array $autoloader): ?ClassLoader { if (isset($autoloader[0]) && \is_object($autoloader[0])) { if ($autoloader[0] instanceof ClassLoader) { return $autoloader[0]; } if ( ($autoloader[0] instanceof DebugClassLoader || $autoloader[0] instanceof ErrorHandlerDebugClassLoader) && \is_array($autoloader[0]->getClassLoader()) && $autoloader[0]->getClassLoader()[0] instanceof ClassLoader) { return $autoloader[0]->getClassLoader()[0]; } } return null; } private function locateMatchingClassLoader(ClassLoader $classLoader): ?ClassLoader { $makerClassLoader = null; foreach ($classLoader->getPrefixesPsr4() as $prefix => $paths) { if ('Symfony\\Bundle\\MakerBundle\\' === $prefix) { $makerClassLoader = $classLoader; } if (0 === strpos($this->rootNamespace['psr4'], $prefix)) { return $classLoader; } } foreach ($classLoader->getPrefixes() as $prefix => $paths) { if (0 === strpos($this->rootNamespace['psr0'], $prefix)) { return $classLoader; } } // Nothing found? Try the class loader where we found MakerBundle return $makerClassLoader; } } src/Util/MakerFileLinkFormatter.php 0000644 00000003124 15120141002 0013304 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Util; use Symfony\Component\Console\Formatter\OutputFormatterStyle; use Symfony\Component\HttpKernel\Debug\FileLinkFormatter; /** * @internal */ final class MakerFileLinkFormatter { private $fileLinkFormatter; public function __construct(FileLinkFormatter $fileLinkFormatter = null) { // Since nullable types are not available in 7.0; can be removed when php >= 7.1 required if (0 == \func_num_args()) { throw new \LogicException('$fileLinkFormatter argument is required'); } $this->fileLinkFormatter = $fileLinkFormatter; } public function makeLinkedPath(string $absolutePath, string $relativePath): string { if (!$this->fileLinkFormatter) { return $relativePath; } if (!$formatted = $this->fileLinkFormatter->format($absolutePath, 1)) { return $relativePath; } // workaround for difficulties parsing linked file paths in appveyor if (getenv('MAKER_DISABLE_FILE_LINKS')) { return $relativePath; } $outputFormatterStyle = new OutputFormatterStyle(); if (method_exists(OutputFormatterStyle::class, 'setHref')) { $outputFormatterStyle->setHref($formatted); } return $outputFormatterStyle->apply($relativePath); } } src/Util/PhpCompatUtil.php 0000644 00000003267 15120141002 0011504 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Util; use Symfony\Bundle\MakerBundle\FileManager; /** * @author Jesse Rushlow <jr@rushlow.dev> * * @internal */ class PhpCompatUtil { /** @var FileManager */ private $fileManager; public function __construct(FileManager $fileManager) { $this->fileManager = $fileManager; } public function canUseAttributes(): bool { $version = $this->getPhpVersion(); return version_compare($version, '8alpha', '>='); } public function canUseTypedProperties(): bool { $version = $this->getPhpVersion(); return version_compare($version, '7.4', '>='); } public function canUseUnionTypes(): bool { $version = $this->getPhpVersion(); return version_compare($version, '8alpha', '>='); } protected function getPhpVersion(): string { $rootDirectory = $this->fileManager->getRootDirectory(); $composerLockPath = sprintf('%s/composer.lock', $rootDirectory); if (!$this->fileManager->fileExists($composerLockPath)) { return \PHP_VERSION; } $lockFileContents = json_decode($this->fileManager->getFileContents($composerLockPath), true); if (empty($lockFileContents['platform-overrides']) || empty($lockFileContents['platform-overrides']['php'])) { return \PHP_VERSION; } return $lockFileContents['platform-overrides']['php']; } } src/Util/PrettyPrinter.php 0000644 00000003351 15120141002 0011600 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Util; use PhpParser\Node\Stmt; use PhpParser\PrettyPrinter\Standard; /** * @internal */ final class PrettyPrinter extends Standard { /** * Overridden to fix indentation problem with tabs. * * If the original source code uses tabs, then the tokenizer * will see this as "1" indent level, and will indent new lines * with just 1 space. By changing 1 indent to 4, we effectively * "correct" this problem when printing. * * For code that is even further indented (e.g. 8 spaces), * the printer uses the first indentation (here corrected * from 1 space to 4) and already (without needing any other * changes) adds 4 spaces onto that. This is why we don't * also need to handle indent levels of 5, 9, etc: these * do not occur (at least in the code we generate); */ protected function setIndentLevel(int $level): void { if (1 === $level) { $level = 4; } parent::setIndentLevel($level); } /** * Overridden to change coding standards. * * Before: * public function getFoo() : string * * After * public function getFoo(): string */ protected function pStmt_ClassMethod(Stmt\ClassMethod $node) { $classMethod = parent::pStmt_ClassMethod($node); if ($node->returnType) { $classMethod = str_replace(') :', '):', $classMethod); } return $classMethod; } } src/Util/TemplateComponentGenerator.php 0000644 00000005220 15120141002 0014247 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Util; use ReflectionClass; use ReflectionException; /** * @author Jesse Rushlow <jr@rushlow.dev> * * @internal */ final class TemplateComponentGenerator { private $phpCompatUtil; public function __construct(PhpCompatUtil $phpCompatUtil) { $this->phpCompatUtil = $phpCompatUtil; } /** @legacy Annotation Support can be dropped w/ Symfony 6 LTS */ public function generateRouteForControllerMethod(string $routePath, string $routeName, array $methods = [], bool $indent = true, bool $trailingNewLine = true): string { if ($this->phpCompatUtil->canUseAttributes()) { $attribute = sprintf('%s#[Route(\'%s\', name: \'%s\'', $indent ? ' ' : null, $routePath, $routeName); if (!empty($methods)) { $attribute .= ', methods: ['; foreach ($methods as $method) { $attribute .= sprintf('\'%s\', ', $method); } $attribute = rtrim($attribute, ', '); $attribute .= ']'; } $attribute .= sprintf(')]%s', $trailingNewLine ? "\n" : null); return $attribute; } $annotation = sprintf('%s/**%s', $indent ? ' ' : null, "\n"); $annotation .= sprintf('%s * @Route("%s", name="%s"', $indent ? ' ' : null, $routePath, $routeName); if (!empty($methods)) { $annotation .= ', methods={'; foreach ($methods as $method) { $annotation .= sprintf('"%s", ', $method); } $annotation = rtrim($annotation, ', '); $annotation .= '}'; } $annotation .= sprintf(')%s', "\n"); $annotation .= sprintf('%s */%s', $indent ? ' ' : null, $trailingNewLine ? "\n" : null); return $annotation; } public function getPropertyType(ClassNameDetails $classNameDetails): ?string { if (!$this->phpCompatUtil->canUseTypedProperties()) { return null; } return sprintf('%s ', $classNameDetails->getShortName()); } /** * @throws ReflectionException */ public function repositoryHasAddRemoveMethods(string $repositoryFullClassName): bool { $reflectedComponents = new ReflectionClass($repositoryFullClassName); return $reflectedComponents->hasMethod('add') && $reflectedComponents->hasMethod('remove'); } } src/Util/UseStatementGenerator.php 0000644 00000004727 15120141002 0013245 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Util; /** * Converts fully qualified class names into sorted use statements for templates. * * @author Jesse Rushlow <jr@rushlow.dev> * * @internal */ final class UseStatementGenerator implements \Stringable { private $classesToBeImported; /** * For use statements that contain aliases, the $classesToBeImported array * may contain an array(s) like [\Some\Class::class => 'ZYX']. The generated * use statement would appear as "use Some\Class::class as 'ZXY'". It is ok * to mix non-aliases classes with aliases. * * @param string[]|array<string, string> $classesToBeImported */ public function __construct(array $classesToBeImported) { $this->classesToBeImported = $classesToBeImported; } public function __toString(): string { $transformed = []; $aliases = []; foreach ($this->classesToBeImported as $key => $class) { if (\is_array($class)) { $aliasClass = key($class); $aliases[$aliasClass] = $class[$aliasClass]; $class = $aliasClass; } $transformed[$key] = str_replace('\\', ' ', $class); } asort($transformed); $statements = ''; foreach ($transformed as $key => $class) { $importedClass = $this->classesToBeImported[$key]; if (!\is_array($importedClass)) { $statements .= sprintf("use %s;\n", $importedClass); continue; } $aliasClass = key($importedClass); $statements .= sprintf("use %s as %s;\n", $aliasClass, $aliases[$aliasClass]); } return $statements; } /** * @param string|string[]|array<string, string> $className */ public function addUseStatement($className): void { if (\is_array($className)) { $this->classesToBeImported = array_merge($this->classesToBeImported, $className); return; } // Let's not add the class again if it already exists. if (\in_array($className, $this->classesToBeImported, true)) { return; } $this->classesToBeImported[] = $className; } } src/Util/YamlManipulationFailedException.php 0000644 00000000700 15120141002 0015207 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Util; /** * Thrown whenever YamlSourceManipulator cannot change contents successfully. */ class YamlManipulationFailedException extends \RuntimeException { } src/Util/YamlSourceManipulator.php 0000644 00000137272 15120141002 0013256 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Util; use Psr\Log\LoggerInterface; use Symfony\Component\Yaml\Exception\ParseException; use Symfony\Component\Yaml\Yaml; /** * A class that modifies YAML source, while keeping comments & formatting. * * This is designed to work for the most common syntaxes, but not * all YAML syntaxes. If content cannot be updated safely, an * exception is thrown. */ class YamlSourceManipulator { public const EMPTY_LINE_PLACEHOLDER_VALUE = '__EMPTY_LINE__'; public const COMMENT_PLACEHOLDER_VALUE = '__COMMENT__'; public const UNSET_KEY_FLAG = '__MAKER_VALUE_UNSET'; public const ARRAY_FORMAT_MULTILINE = 'multi'; public const ARRAY_FORMAT_INLINE = 'inline'; public const ARRAY_TYPE_SEQUENCE = 'sequence'; public const ARRAY_TYPE_HASH = 'hash'; /** * @var LoggerInterface|null */ private $logger; private $contents; private $currentData; private $currentPosition = 0; private $previousPath = []; private $currentPath = []; private $depth = 0; private $indentationForDepths = []; private $arrayFormatForDepths = []; private $arrayTypeForDepths = []; public function __construct(string $contents) { $this->contents = $contents; $this->currentData = Yaml::parse($contents); if (!\is_array($this->currentData)) { throw new \InvalidArgumentException('Only YAML with a top-level array structure is supported'); } } public function setLogger(LoggerInterface $logger) { $this->logger = $logger; } public function getData(): array { return $this->currentData; } public function getContents(): string { return $this->contents; } public function setData(array $newData) { $this->currentPath = []; $this->previousPath = []; $this->currentPosition = 0; $this->depth = -1; $this->indentationForDepths = []; $this->arrayFormatForDepths = []; $this->arrayTypeForDepths = []; $this->updateData($newData); $this->replaceSpecialMetadataCharacters(); // update the data now that the special chars have been removed $this->currentData = Yaml::parse($this->contents); // remove special metadata keys that were replaced $newData = $this->removeMetadataKeys($newData); // Before comparing, re-index any sequences on the new data. // The current data will already use sequential indexes $newData = $this->normalizeSequences($newData); if ($newData !== $this->currentData) { throw new YamlManipulationFailedException(sprintf('Failed updating YAML contents: the process was successful, but something was not updated. Expected new data: %s. Actual new data: %s', var_export($newData, true), var_export($this->currentData, true))); } } public function createEmptyLine(): string { return self::EMPTY_LINE_PLACEHOLDER_VALUE; } public function createCommentLine(string $comment): string { return self::COMMENT_PLACEHOLDER_VALUE.$comment; } private function updateData(array $newData) { ++$this->depth; if (0 === $this->depth) { $this->indentationForDepths[$this->depth] = 0; $this->arrayFormatForDepths[$this->depth] = self::ARRAY_FORMAT_MULTILINE; } else { // match the current indentation to start $this->indentationForDepths[$this->depth] = $this->indentationForDepths[$this->depth - 1]; // advancing is especially important if this is an inline array: // get into the [] or {} $this->arrayFormatForDepths[$this->depth] = $this->guessNextArrayTypeAndAdvance(); } $currentData = $this->getCurrentData(); $this->arrayTypeForDepths[$this->depth] = $this->isHash($currentData) ? self::ARRAY_TYPE_HASH : self::ARRAY_TYPE_SEQUENCE; $this->log(sprintf( 'Changing array type & format via updateData() (type=%s, format=%s)', $this->arrayTypeForDepths[$this->depth], $this->arrayFormatForDepths[$this->depth] )); foreach ($currentData as $key => $currentVal) { // path setting is mostly duplicated at the bottom of this method $this->previousPath = $this->currentPath; if (!isset($this->previousPath[$this->depth])) { // if there is no previous flag at this level, mark it with a null $this->previousPath[$this->depth] = null; } $this->currentPath[$this->depth] = $key; // advance from the end of the previous value to the // start of the key, which may include whitespace or, for // example, some closing array syntax - } or ] - from the // previous value $this->advanceBeyondEndOfPreviousKey($key); $this->log('START key', true); // 1) was this key removed from the new data? if (!\array_key_exists($key, $newData)) { $this->log('Removing key'); $this->removeKeyFromYaml($key, $currentData[$key]); // manually update our current data now that the key is gone unset($currentData[$key]); // because the item was removed, reset the current path // to the previous path, so the next iteration doesn't // expect the previous path to have this removed key $this->currentPath = $this->previousPath; continue; } /* * 2) are there new keys in the new data before this key? * * To determine this, we look at the position of the key inside the * current data and compare it to the position of that same key in * the new data. While they are not equal, we loop. Inside the loop, * the new key is added to the current data *before* $key. Thanks to * this, on each loop, the currentDataIndex will increase until it * matches the new data */ while (($currentDataIndex = array_search($key, array_keys($currentData))) !== array_search($key, array_keys($newData))) { // loop until the current key is found at the same position in current & new data $newKey = array_keys($newData)[$currentDataIndex]; $newVal = $newData[$newKey]; $this->log('Adding new key: '.$newKey); $this->addNewKeyToYaml($newKey, $newVal); // refresh the current array data because we added an item // we can't just add the key manually, as it may have been // we can't just add the key manually, as it may have been // added in the middle $currentData = $this->getCurrentData(1); } // 3) Key already exists in YAML // advance the position to the end of this key $this->advanceBeyondKey($key); $newVal = $newData[$key]; // if the current data is an array, we should keep // walking through that data, even if it didn't change, // so that we can advance the current position if (\is_array($currentData[$key]) && \is_array($newVal)) { $this->log('Calling updateData() on next level'); $this->updateData($newVal); continue; } // 3a) value did NOT change if ($currentData[$key] === $newVal) { $this->log('value did not change'); $this->advanceBeyondValue($newVal); continue; } // 3b) value DID change $this->log(sprintf('updating value to {%s}', \is_array($newVal) ? '<array>' : $newVal)); $this->changeValueInYaml($newVal); } // Bonus! are there new keys in the data after this key... // and this is the final key? // Edge case: if the last item on a multi-line array has a comment, // we want to move to the end of the line, beyond that comment if (\count($currentData) < \count($newData) && $this->isCurrentArrayMultiline()) { $this->advanceBeyondMultilineArrayLastItem($currentData, $newData); } if (0 === $this->indentationForDepths[$this->depth] && $this->depth > 1) { $ident = $this->getPreferredIndentationSize(); $previousDepth = $this->depth - 1; $this->indentationForDepths[$this->depth] = ($ident + $this->indentationForDepths[$previousDepth]); } while (\count($currentData) < \count($newData)) { $newKey = array_keys($newData)[\count($currentData)]; // manually move the paths forward // mostly duplicated above $this->previousPath = $this->currentPath; if (!isset($this->previousPath[$this->depth])) { // if there is no previous flag at this level, mark it with a null $this->previousPath[$this->depth] = null; } $this->currentPath[$this->depth] = $newKey; $newVal = $newData[$newKey]; $this->log('Adding new key to end of array'); $this->addNewKeyToYaml($newKey, $newVal); // refresh manually so the while sees it above $currentData = $this->getCurrentData(1); } $this->decrementDepth(); } /** * Adds a new key to current position in the YAML. * * The position should be set *right* where this new key * should be inserted. * * @param mixed $key * @param mixed $value */ private function addNewKeyToYaml($key, $value) { $extraOffset = 0; $firstItemInArray = false; if (empty($this->getCurrentData(1))) { // The array that we're appending is empty: // First, fix the "type" - it could be changing from a sequence to a hash or vice versa $this->arrayTypeForDepths[$this->depth] = \is_int($key) ? self::ARRAY_TYPE_SEQUENCE : self::ARRAY_TYPE_HASH; // we prefer multi-line, so let's convert to it! $this->arrayFormatForDepths[$this->depth] = self::ARRAY_FORMAT_MULTILINE; // if this is an inline empty array (is there any other), we need to // remove the empty array characters = {} or [] // we are already 1 character beyond the starting { or [ - so, rewind before it --$this->currentPosition; // now, rewind any spaces to get back to the : after the key while (' ' === substr($this->contents, $this->currentPosition - 1, 1)) { --$this->currentPosition; } // determine an extra offset to "skip" when reconstructing the string $endingArrayPosition = $this->findPositionOfNextCharacter(['}', ']']); $extraOffset = $endingArrayPosition - $this->currentPosition; // increase the indentation of *this* level $this->manuallyIncrementIndentation(); $firstItemInArray = true; } elseif ($this->isPositionAtBeginningOfArray()) { $firstItemInArray = true; // the array is not empty, but we are prepending an element if ($this->isCurrentArrayMultiline()) { // indentation will be set to low, except for root level if ($this->depth > 0) { $this->manuallyIncrementIndentation(); } } else { // we're at the start of an inline array // advance beyond any whitespace so that our new key // uses the same whitespace that was originally after // the { or [ $this->advanceBeyondWhitespace(); } } if (\is_int($key)) { if ($this->isCurrentArrayMultiline()) { if ($this->isCurrentArraySequence()) { $newYamlValue = '- '.$this->convertToYaml($value); } else { // this is an associative array, but an indexed key // is being added. We can't use the "- " format $newYamlValue = sprintf( '%s: %s', $key, $this->convertToYaml($value) ); } } else { $newYamlValue = $this->convertToYaml($value); } } else { $newYamlValue = $this->convertToYaml([$key => $value]); } if (0 === $this->currentPosition) { // if we're at the beginning of the file, the situation is special: // no previous blank line is needed, but we DO need to add a blank // line after, because the remainder of the content expects the // current position the start at the beginning of a new line $newYamlValue = $newYamlValue."\n"; } else { if ($this->isCurrentArrayMultiline()) { // because we're inside a multi-line array, put this item // onto the *next* line & indent it $newYamlValue = "\n".$this->indentMultilineYamlArray($newYamlValue); } else { if ($firstItemInArray) { // avoid the starting "," if first item in array // but, DO add an ending "," $newYamlValue = $newYamlValue.', '; } else { $newYamlValue = ', '.$newYamlValue; } } } $newContents = substr($this->contents, 0, $this->currentPosition) .$newYamlValue .substr($this->contents, $this->currentPosition + $extraOffset); // manually bump the position: we didn't really move forward // any in the existing string, we just added our own new content $this->currentPosition = $this->currentPosition + \strlen($newYamlValue); if (0 === $this->depth) { $newData = $this->currentData; $newData = $this->appendToArrayAtCurrentPath($key, $value, $newData); } else { // first, append to the "local" array: the little array we're currently working on $newLocalData = $this->getCurrentData(1); $newLocalData = $this->appendToArrayAtCurrentPath($key, $value, $newLocalData); // second, set this new array inside the full data $newData = $this->currentData; $newData = $this->setValueAtCurrentPath($newLocalData, $newData, 1); } $this->updateContents( $newContents, $newData, $this->currentPosition ); } private function removeKeyFromYaml($key, $currentVal) { $endKeyPosition = $this->getEndOfKeyPosition($key); $endKeyPosition = $this->findEndPositionOfValue($currentVal, $endKeyPosition); if ($this->isCurrentArrayMultiline()) { $nextNewLine = $this->findNextLineBreak($endKeyPosition); // it's possible we're at the end of the file so there are no more \n if (false !== $nextNewLine) { $endKeyPosition = $nextNewLine; } } else { // find next ending character - , } or ] while (!\in_array($currentChar = substr($this->contents, $endKeyPosition, 1), [',', ']', '}'])) { ++$endKeyPosition; } // if a sequence or hash is ending, and the character before it is a space, keep that if ((']' === $currentChar || '}' === $currentChar) && ' ' === substr($this->contents, $endKeyPosition - 1, 1)) { --$endKeyPosition; } } $newPositionBump = 0; $extraContent = ''; if (1 === \count($this->getCurrentData(1))) { // the key being removed is the *only* key // we need to close the new, empty array $extraContent = ' []'; // when processing arrays normally, the position is set // after the opening character. Move this here manually $newPositionBump = 2; // if it *was* multiline, the indentation is now lost if ($this->isCurrentArrayMultiline()) { $this->indentationForDepths[$this->depth] = $this->indentationForDepths[$this->depth - 1]; } // it is now definitely a sequence $this->arrayTypeForDepths[$this->depth] = self::ARRAY_TYPE_SEQUENCE; // it is now inline $this->arrayFormatForDepths[$this->depth] = self::ARRAY_FORMAT_INLINE; } $newContents = substr($this->contents, 0, $this->currentPosition) .$extraContent .substr($this->contents, $endKeyPosition); $newData = $this->currentData; $newData = $this->removeKeyAtCurrentPath($newData); // instead of passing the new +2 position below, we do it here // manually. This is because this it's not a real position move, // we manually (above) added some new chars that didn't exist before $this->currentPosition = $this->currentPosition + $newPositionBump; $this->updateContents( $newContents, $newData, // position is unchanged: just some content was removed $this->currentPosition ); } /** * Replaces the value at the current position with this value. * * The position should be set right at the start of this value * (i.e. after its key). * * @param mixed $value The new value to set into YAML */ private function changeValueInYaml($value) { $originalVal = $this->getCurrentData(); $endValuePosition = $this->findEndPositionOfValue($originalVal); $isMultilineValue = null !== $this->findPositionOfMultilineCharInLine($this->currentPosition); // In case of multiline, $value is converted as plain string like "Foo\nBar" // We need to keep it "as is" $newYamlValue = $isMultilineValue ? rtrim($value, "\n") : $this->convertToYaml($value); if ((!\is_array($originalVal) && \is_array($value)) || ($this->isMultilineString($originalVal) && $this->isMultilineString($value)) ) { // we're converting from a scalar to a (multiline) array // this means we need to break onto the next line // increase(override) the indentation $newYamlValue = "\n".$this->indentMultilineYamlArray($newYamlValue, ($this->indentationForDepths[$this->depth] + $this->getPreferredIndentationSize())); } elseif ($this->isCurrentArrayMultiline() && $this->isCurrentArraySequence()) { // we are a multi-line sequence, so drop to next line, indent and add "- " in front $newYamlValue = "\n".$this->indentMultilineYamlArray('- '.$newYamlValue); } else { // empty space between key & value $newYamlValue = ' '.$newYamlValue; } $newPosition = $this->currentPosition + \strlen($newYamlValue); $isNextContentComment = $this->isPreviousLineComment($newPosition); if ($isNextContentComment) { ++$newPosition; } if ($isMultilineValue) { // strlen(" |") $newPosition -= 2; } $newContents = substr($this->contents, 0, $this->currentPosition) .($isMultilineValue ? ' |' : '') .$newYamlValue /* * If the next line is a comment, this means we probably had * a structure that looks like this: * access_control: * # - { path: ^/admin, roles: ROLE_ADMIN } * * In this odd case, we need to know that the next line * is a comment, so we can add an extra line break. * Otherwise, the result is something like: * access_control: * - { path: /foo, roles: ROLE_USER } # - { path: ^/admin, roles: ROLE_ADMIN } */ .($isNextContentComment ? "\n" : '') .substr($this->contents, $endValuePosition); $newData = $this->currentData; $newData = $this->setValueAtCurrentPath($value, $newData); $this->updateContents( $newContents, $newData, $newPosition ); } private function advanceBeyondKey($key) { $this->log(sprintf('Advancing position beyond key "%s"', $key)); $this->advanceCurrentPosition($this->getEndOfKeyPosition($key)); } private function advanceBeyondEndOfPreviousKey($key) { $this->log('Advancing position beyond PREV key'); $this->advanceCurrentPosition($this->getEndOfPreviousKeyPosition($key)); } private function advanceBeyondMultilineArrayLastItem(array $currentData, array $newData) { $this->log('Trying to advance beyond the last item in a multiline array'); $this->advanceBeyondWhitespace(); if ('#' === substr($this->contents, $this->currentPosition, 1)) { $this->log('The line ends with a comment, going to EOL'); $this->advanceToEndOfLine(); return; } $nextLineBreak = $this->findNextLineBreak($this->currentPosition); if ('}' === trim(substr($this->contents, $this->currentPosition, $nextLineBreak - $this->currentPosition))) { $this->log('The line ends with an array closing brace, going to EOL'); $this->advanceToEndOfLine(); } } private function advanceBeyondValue($value) { if (\is_array($value)) { throw new \LogicException('Do not pass an array to this method'); } $this->log(sprintf('Advancing position beyond value "%s"', $value)); $this->advanceCurrentPosition($this->findEndPositionOfValue($value)); } private function getEndOfKeyPosition($key) { preg_match($this->getKeyRegex($key), $this->contents, $matches, \PREG_OFFSET_CAPTURE, $this->currentPosition); if (empty($matches)) { // for integers, the key may not be explicitly printed if (\is_int($key)) { return $this->currentPosition; } throw new YamlManipulationFailedException(sprintf('Cannot find the key "%s"', $key)); } return $matches[0][1] + \strlen($matches[0][0]); } /** * Finds the end position of the key that comes *before* this key. */ private function getEndOfPreviousKeyPosition($key): int { preg_match($this->getKeyRegex($key), $this->contents, $matches, \PREG_OFFSET_CAPTURE, $this->currentPosition); if (empty($matches)) { // for integers, the key may not be explicitly printed if (\is_int($key)) { return $this->currentPosition; } $cursor = $this->currentPosition; while ('-' !== substr($this->contents, $cursor - 1, 1) && -1 !== $cursor) { --$cursor; } if ($cursor >= 0) { return $cursor; } throw new YamlManipulationFailedException(sprintf('Cannot find the key "%s"', $key)); } $startOfKey = $matches[0][1]; // if we're at the start of the file, we're done! if (0 === $startOfKey) { return 0; } /* * Now, walk backwards: so that the position is before any * whitespace, commas or line breaks. Basically, we want to go * back to the first character *after* the previous key started. */ // walk back any spaces while (' ' === substr($this->contents, $startOfKey - 1, 1)) { --$startOfKey; } // find either a line break or a , that is the end of the previous key while (\in_array(($char = substr($this->contents, $startOfKey - 1, 1)), [',', "\n"])) { --$startOfKey; } // look for \r\n if ("\r" === substr($this->contents, $startOfKey - 1, 1)) { --$startOfKey; } // if we're at the start of a line, if the prev line is a comment, move before it if ($this->isCharLineBreak(substr($this->contents, $startOfKey, 1))) { // move one (or two) forward so the code below finds the *previous* line ++$startOfKey; if ($this->isCharLineBreak(substr($this->contents, $startOfKey, 1))) { ++$startOfKey; } /* * In a multi-line array, the previous line(s) could be 100% comments. * In that situation, we want to rewind to *before* the comments, so * that those comments are attached to the current key and move with it. */ while ($this->isPreviousLineComment($startOfKey)) { --$startOfKey; // if this is a \n\r, we need to go back an extra char if ("\r" === substr($this->contents, $startOfKey - 1, 1)) { --$startOfKey; } while (!$this->isCharLineBreak(substr($this->contents, $startOfKey - 1, 1))) { --$startOfKey; // we've reached the start of the file! if (0 === $startOfKey) { break; } } } if (0 !== $startOfKey) { // move backwards one onto the previous line --$startOfKey; } // look for \n\r situation if ("\r" === substr($this->contents, $startOfKey - 1, 1)) { --$startOfKey; } } return $startOfKey; } private function getKeyRegex($key) { return sprintf('#(?<!\w)\$?%s\'?( )*:#', preg_quote($key)); } private function updateContents(string $newContents, array $newData, int $newPosition) { $this->log('updateContents()'); // validate the data try { $parsedContentsData = Yaml::parse($newContents); // normalize indexes on sequences to avoid comparison problems $parsedContentsData = $this->normalizeSequences($parsedContentsData); $newData = $this->normalizeSequences($newData); if ($parsedContentsData !== $newData) { throw new YamlManipulationFailedException(sprintf('Content was updated, but updated content does not match expected data. Original source: "%s", updated source: "%s", updated data: %s', $this->contents, $newContents, var_export($newData, true))); } } catch (ParseException $e) { throw new YamlManipulationFailedException(sprintf('Could not update YAML: a parse error occurred in the new content: "%s"', $newContents)); } // must be called before changing the contents $this->advanceCurrentPosition($newPosition); $this->contents = $newContents; $this->currentData = $newData; } private function convertToYaml($data) { $indent = $this->depth > 0 && isset($this->indentationForDepths[$this->depth]) ? intdiv($this->indentationForDepths[$this->depth], $this->depth) : 4; $newDataString = Yaml::dump($data, 4, $indent); // new line is appended: remove it $newDataString = rtrim($newDataString, "\n"); return $newDataString; } /** * Adds a new item (with the given key) to the $data array at the correct position. * * The $data should be the simple array that should be updated and that * the current path is pointing to. The current path is used * to determine *where* in the array to put the new item (so that it's * placed in the middle when necessary). */ private function appendToArrayAtCurrentPath($key, $value, array $data): array { if ($this->isPositionAtBeginningOfArray()) { // this should be prepended return [$key => $value] + $data; } $offset = array_search($this->previousPath[$this->depth], array_keys($data)); // if the target is currently the end of the array, just append if ($offset === (\count($data) - 1)) { $data[$key] = $value; return $data; } return array_merge( \array_slice($data, 0, $offset + 1), [$key => $value], \array_slice($data, $offset + 1, null) ); } private function setValueAtCurrentPath($value, array $data, int $limitLevels = 0) { // create a reference $dataRef = &$data; // start depth at $limitLevels (instead of 0) to properly detect when to set the key $depth = $limitLevels; $path = \array_slice($this->currentPath, 0, \count($this->currentPath) - $limitLevels); foreach ($path as $key) { if (!\array_key_exists($key, $dataRef)) { throw new \LogicException(sprintf('Could not find the key "%s" from the current path "%s" in data "%s"', $key, implode(', ', $path), var_export($data, true))); } if ($depth === $this->depth) { // we're at the correct depth! if (self::UNSET_KEY_FLAG === $value) { unset($dataRef[$key]); // if this is a sequence, reindex the keys if ($this->isCurrentArraySequence()) { $dataRef = array_values($dataRef); } } else { $dataRef[$key] = $value; } return $data; } // get a deeper reference $dataRef = &$dataRef[$key]; ++$depth; } throw new \LogicException('The value was not updated.'); } private function removeKeyAtCurrentPath(array $data): array { return $this->setValueAtCurrentPath(self::UNSET_KEY_FLAG, $data); } /** * Returns the value in the current data that is currently * being looked at. * * This could fail if the currentPath is for new data. * * @param int $limitLevels If set to 1, the data 1 level up will be returned * * @return mixed */ private function getCurrentData(int $limitLevels = 0) { $data = $this->currentData; $path = \array_slice($this->currentPath, 0, \count($this->currentPath) - $limitLevels); foreach ($path as $key) { if (!\array_key_exists($key, $data)) { throw new \LogicException(sprintf('Could not find the key "%s" from the current path "%s" in data "%s"', $key, implode(', ', $path), var_export($this->currentData, true))); } $data = $data[$key]; } return $data; } private function findEndPositionOfValue($value, $offset = null) { if (\is_array($value)) { $currentPosition = $this->currentPosition; $this->log('Walking across array to find end position of array'); $this->updateData($value); $endKeyPosition = $this->currentPosition; $this->currentPosition = $currentPosition; return $endKeyPosition; } if (\is_scalar($value) || null === $value) { $offset = null === $offset ? $this->currentPosition : $offset; if (\is_bool($value)) { // (?i) & (?-i) opens/closes case insensitive match $pattern = sprintf('(?i)%s(?-i)', $value ? 'true' : 'false'); } elseif (null === $value) { $pattern = '(~|NULL|null|\n)'; } else { // Multiline value ends with \n. // If we remove this character, the next property will ne merged with this value $quotedValue = preg_quote(rtrim($value, "\n"), '#'); $patternValue = $quotedValue; // Iterates until we find a new line char or we reach end of file if (null !== $this->findPositionOfMultilineCharInLine($offset)) { $patternValue = str_replace(["\r\n", "\n"], '\r?\n\s*', $quotedValue); } $pattern = sprintf('\'?"?%s\'?"?', $patternValue); } // a value like "foo:" can simply end a file // this means the value is null if ($offset === \strlen($this->contents)) { return $offset; } preg_match(sprintf('#%s#', $pattern), $this->contents, $matches, \PREG_OFFSET_CAPTURE, $offset); if (empty($matches)) { throw new YamlManipulationFailedException(sprintf('Cannot find the original value "%s"', $value)); } $position = $matches[0][1] + \strlen($matches[0][0]); // edge case where there is a comment between the current position // and the value we're looking for AND that comment contains an // exact string match for the value we're looking for if ($this->isFinalLineComment(substr($this->contents, $this->currentPosition, $position - $this->currentPosition))) { return $this->findEndPositionOfValue($value, $position); } if (null === $value && "\n" === $matches[0][0] && !$this->isCurrentLineComment($position)) { $this->log('Zero-length null value, next line not a comment, take a step back'); --$position; } return $position; } // there are other possible values, but we don't support them throw new YamlManipulationFailedException(sprintf('Unsupported Yaml value of type "%s"', \gettype($value))); } private function advanceCurrentPosition(int $newPosition) { $this->log(sprintf('advanceCurrentPosition() from %d to %d', $this->currentPosition, $newPosition), true); $originalPosition = $this->currentPosition; $this->currentPosition = $newPosition; // if we're not changing, or moving backwards, don't count indent // changes if ($newPosition <= $originalPosition) { return; } /* * A bit of a workaround. At times, this function will be called when the * position is at the beginning of the line: so, one character *after* * a line break. In that case, if there are a group of spaces at the * beginning of this first line, they *should* be used to calculate the new * indentation. To force this, if we detect this situation, we move one * character backwards, so that the first line is considered a valid line * to look for indentation. */ if ($this->isCharLineBreak(substr($this->contents, $originalPosition - 1, 1))) { --$originalPosition; } // look for empty lines and track the current indentation $advancedContent = substr($this->contents, $originalPosition, $newPosition - $originalPosition); $previousIndentation = $this->indentationForDepths[$this->depth]; $newIndentation = $previousIndentation; if ("\n" === $advancedContent) { $this->log('Just a linebreak, no indent changes'); return; } if (false !== strpos($advancedContent, "\n")) { $lines = explode("\n", $advancedContent); if (!empty($lines)) { $lastLine = $lines[\count($lines) - 1]; $lastLine = trim($lastLine, "\r"); $indentation = 0; while (' ' === substr($lastLine, $indentation, 1)) { ++$indentation; } $newIndentation = $indentation; } } $this->log(sprintf('Calculating new indentation: changing from %d to %d', $this->indentationForDepths[$this->depth], $newIndentation), true); $this->indentationForDepths[$this->depth] = $newIndentation; } private function decrementDepth() { $this->log('Moving up 1 level of depth'); unset($this->indentationForDepths[$this->depth]); unset($this->arrayFormatForDepths[$this->depth]); unset($this->arrayTypeForDepths[$this->depth]); unset($this->currentPath[$this->depth]); unset($this->previousPath[$this->depth]); --$this->depth; } private function getCurrentIndentation(int $override = null): string { $indent = $override ?? $this->indentationForDepths[$this->depth]; return str_repeat(' ', $indent); } private function log(string $message, $includeContent = false) { if (null === $this->logger) { return; } $context = [ 'key' => isset($this->currentPath[$this->depth]) ? $this->currentPath[$this->depth] : 'n/a', 'depth' => $this->depth, 'position' => $this->currentPosition, 'indentation' => $this->indentationForDepths[$this->depth], 'type' => $this->arrayTypeForDepths[$this->depth], 'format' => $this->arrayFormatForDepths[$this->depth], ]; if ($includeContent) { $context['content'] = sprintf( '>%s<', str_replace(["\r\n", "\n"], ['\r\n', '\n'], substr($this->contents, $this->currentPosition, 50)) ); } $this->logger->debug($message, $context); } private function isCurrentArrayMultiline(): bool { return self::ARRAY_FORMAT_MULTILINE === $this->arrayFormatForDepths[$this->depth]; } private function isCurrentArraySequence(): bool { return self::ARRAY_TYPE_SEQUENCE === $this->arrayTypeForDepths[$this->depth]; } /** * Attempts to guess if the array at the current position * is a multi-line array or an inline array. */ private function guessNextArrayTypeAndAdvance(): string { while (true) { if ($this->isEOF()) { throw new \LogicException('Could not determine array type'); } // get the next char & advance immediately $nextCharacter = substr($this->contents, $this->currentPosition, 1); // advance, but without advanceCurrentPosition() // because we are either moving along one line until [ { // or we are finding a line break and stopping: indentation // should not be calculated ++$this->currentPosition; if ($this->isCharLineBreak($nextCharacter)) { return self::ARRAY_FORMAT_MULTILINE; } if ('[' === $nextCharacter || '{' === $nextCharacter) { return self::ARRAY_FORMAT_INLINE; } } } /** * Advance until you find *one* of the characters in $chars. */ private function findPositionOfNextCharacter(array $chars) { $currentPosition = $this->currentPosition; while (true) { if ($this->isEOF($currentPosition)) { throw new \LogicException(sprintf('Could not find any characters: %s', implode(', ', $chars))); } // get the next char & advance immediately $nextCharacter = substr($this->contents, $currentPosition, 1); ++$currentPosition; if (\in_array($nextCharacter, $chars)) { return $currentPosition; } } } private function advanceBeyondWhitespace() { while (' ' === substr($this->contents, $this->currentPosition, 1)) { if ($this->isEOF()) { return; } ++$this->currentPosition; } } private function advanceToEndOfLine() { $newPosition = $this->currentPosition; while (!$this->isCharLineBreak(substr($this->contents, $newPosition, 1))) { if ($this->isEOF($newPosition)) { // found the end of the file! break; } ++$newPosition; } $this->advanceCurrentPosition($newPosition); } /** * Duplicated from Symfony's Inline::isHash(). * * Returns true if the value must be rendered as a hash, * which includes an indexed array, if the indexes are * not sequential. */ private function isHash($value): bool { if ($value instanceof \stdClass || $value instanceof \ArrayObject) { return true; } $expectedKey = 0; foreach ($value as $key => $val) { if ($key !== $expectedKey++) { return true; } } return false; } private function normalizeSequences(array $data) { // https://stackoverflow.com/questions/173400/how-to-check-if-php-array-is-associative-or-sequential/4254008#4254008 $hasStringKeys = function (array $array) { return \count(array_filter(array_keys($array), 'is_string')) > 0; }; foreach ($data as $key => $val) { if (!\is_array($val)) { continue; } if (!$hasStringKeys($val)) { // avoid indexed arrays with non-sequential keys // e.g. if a key was removed. This causes comparison issues $val = array_values($val); $data[$key] = $val; } $data[$key] = $this->normalizeSequences($val); } return $data; } private function removeMetadataKeys(array $data) { foreach ($data as $key => $val) { if (\is_array($val)) { $data[$key] = $this->removeMetadataKeys($val); continue; } if (self::EMPTY_LINE_PLACEHOLDER_VALUE === $val) { unset($data[$key]); } if (null !== $val && 0 === strpos($val, self::COMMENT_PLACEHOLDER_VALUE)) { unset($data[$key]); } } return $data; } private function replaceSpecialMetadataCharacters() { while (preg_match('#\n.*'.self::EMPTY_LINE_PLACEHOLDER_VALUE.'.*\n#', $this->contents, $matches)) { $this->contents = str_replace($matches[0], "\n\n", $this->contents); } while (preg_match('#\n(\s*).*\''.self::COMMENT_PLACEHOLDER_VALUE.'(.*)\'#', $this->contents, $matches)) { $fullMatch = $matches[0]; $indentation = $matches[1]; $comment = $matches[2]; $this->contents = str_replace( $fullMatch, sprintf("\n%s#%s", $indentation, $comment), $this->contents ); } } /** * Try to guess a preferred indentation level. */ private function getPreferredIndentationSize(): int { return isset($this->indentationForDepths[1]) && $this->indentationForDepths[1] > 0 ? $this->indentationForDepths[1] : 4; } /** * For the array currently being processed, are we currently * handling the first key inside of it? */ private function isPositionAtBeginningOfArray(): bool { return null === $this->previousPath[$this->depth]; } private function manuallyIncrementIndentation() { $this->indentationForDepths[$this->depth] = $this->indentationForDepths[$this->depth] + $this->getPreferredIndentationSize(); } private function isEOF(int $position = null) { $position = null === $position ? $this->currentPosition : $position; return $position === \strlen($this->contents); } private function isPreviousLineComment(int $position): bool { $line = $this->getPreviousLine($position); if (null === $line) { return false; } return $this->isLineComment($line); } private function isCurrentLineComment(int $position): bool { $line = $this->getCurrentLine($position); if (null === $line) { return false; } return $this->isLineComment($line); } private function isLineComment(string $line): bool { // adopted from Parser::isCurrentLineComment() from symfony/yaml $ltrimmedLine = ltrim($line, ' '); return '' !== $ltrimmedLine && '#' === $ltrimmedLine[0]; } private function isFinalLineComment(string $content): bool { if (!$content) { return false; } $content = str_replace("\r", "\n", $content); $lines = explode("\n", $content); $line = end($lines); return $this->isLineComment($line); } private function getPreviousLine(int $position) { // find the previous \n by finding the last one in the content up to the position $endPos = strrpos(substr($this->contents, 0, $position), "\n"); if (false === $endPos) { // there is no previous line return null; } $startPos = strrpos(substr($this->contents, 0, $endPos), "\n"); if (false === $startPos) { // we're at the beginning of the file $startPos = 0; } else { // move 1 past the line break ++$startPos; } $previousLine = substr($this->contents, $startPos, $endPos - $startPos); return trim($previousLine, "\r"); } private function getCurrentLine(int $position) { $startPos = strrpos(substr($this->contents, 0, $position), "\n") + 1; $endPos = strpos($this->contents, "\n", $startPos); $this->log(sprintf('Looking for current line from %d to %d', $startPos, $endPos)); $line = substr($this->contents, $startPos, $endPos - $startPos); return trim($line, "\r"); } private function findNextLineBreak(int $position) { $nextNPos = strpos($this->contents, "\n", $position); $nextRPos = strpos($this->contents, "\r", $position); if (false === $nextNPos) { return false; } if (false === $nextRPos) { return $nextNPos; } // find the first possible line break character $nextLinePos = min($nextNPos, $nextRPos); // check for a \r\n situation if (($nextLinePos + 1) === $nextNPos) { ++$nextLinePos; } return $nextLinePos; } private function isCharLineBreak(string $char): bool { return "\n" === $char || "\r" === $char; } /** * Takes an unindented multi-line YAML string and indents it so * it can be inserted into the current position. * * Usually an empty line needs to be prepended to this result before * adding to the content. */ private function indentMultilineYamlArray(string $yaml, int $indentOverride = null): string { $indent = $this->getCurrentIndentation($indentOverride); // But, if the *value* is an array, then ITS children will // also need to be indented artificially by the same amount $yaml = str_replace("\n", "\n".$indent, $yaml); if ($this->isMultilineString($yaml)) { // Remove extra indentation in case of blank line in multiline string $yaml = str_replace("\n".$indent."\n", "\n\n", $yaml); } // now indent this level return $indent.$yaml; } private function findPositionOfMultilineCharInLine(int $position): ?int { $cursor = $position; while (!$this->isCharLineBreak($currentChar = substr($this->contents, $cursor + 1, 1)) && !$this->isEOF($cursor)) { if ('|' === $currentChar) { return $cursor; } ++$cursor; } return null; } private function isMultilineString($value): bool { return \is_string($value) && false !== strpos($value, "\n"); } } src/ApplicationAwareMakerInterface.php 0000644 00000001076 15120141002 0014056 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle; use Symfony\Component\Console\Application; /** * Implement this interface if your Maker needs access to the Application. * * @author Ryan Weaver <ryan@knpuniversity.com> */ interface ApplicationAwareMakerInterface { public function setApplication(Application $application); } src/ConsoleStyle.php 0000644 00000002127 15120141002 0010453 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; /** * @author Javier Eguiluz <javier.eguiluz@gmail.com> * @author Ryan Weaver <weaverryan@gmail.com> */ final class ConsoleStyle extends SymfonyStyle { private $output; public function __construct(InputInterface $input, OutputInterface $output) { $this->output = $output; parent::__construct($input, $output); } public function success($message): void { $this->writeln('<fg=green;options=bold,underscore>OK</> '.$message); } public function comment($message): void { $this->text($message); } public function getOutput(): OutputInterface { return $this->output; } } src/DependencyBuilder.php 0000644 00000010153 15120141002 0011413 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle; final class DependencyBuilder { private $dependencies = []; private $devDependencies = []; private $minimumPHPVersion = 70100; /** * Add a dependency that will be reported if the given class is missing. * * If the dependency is *optional*, then it will only be reported to * the user if other required dependencies are missing. An example * is the "validator" when trying to work with forms. */ public function addClassDependency(string $class, string $package, bool $required = true, bool $devDependency = false): void { if ($devDependency) { $this->devDependencies[] = [ 'class' => $class, 'name' => $package, 'required' => $required, ]; } else { $this->dependencies[] = [ 'class' => $class, 'name' => $package, 'required' => $required, ]; } } public function requirePHP71(): void { // no-op - MakerBundle now required PHP 7.1 } /** * @internal */ public function getMissingDependencies(): array { return $this->calculateMissingDependencies($this->dependencies); } /** * @internal */ public function getMissingDevDependencies(): array { return $this->calculateMissingDependencies($this->devDependencies); } /** * @internal */ public function getAllRequiredDependencies(): array { return $this->getRequiredDependencyNames($this->dependencies); } /** * @internal */ public function getAllRequiredDevDependencies(): array { return $this->getRequiredDependencyNames($this->devDependencies); } /** * @internal */ public function getMissingPackagesMessage(string $commandName, $message = null): string { $packages = $this->getMissingDependencies(); $packagesDev = $this->getMissingDevDependencies(); if (empty($packages) && empty($packagesDev)) { return ''; } $packagesCount = \count($packages) + \count($packagesDev); $message = sprintf( "Missing package%s: %s, run:\n", $packagesCount > 1 ? 's' : '', $message ?: sprintf('to use the %s command', $commandName) ); if (!empty($packages)) { $message .= sprintf("\ncomposer require %s", implode(' ', $packages)); } if (!empty($packagesDev)) { $message .= sprintf("\ncomposer require %s --dev", implode(' ', $packagesDev)); } return $message; } /** * @internal */ public function isPhpVersionSatisfied(): bool { return \PHP_VERSION_ID >= $this->minimumPHPVersion; } private function getRequiredDependencyNames(array $dependencies): array { $packages = []; foreach ($dependencies as $package) { if (!$package['required']) { continue; } $packages[] = $package['name']; } return array_unique($packages); } private function calculateMissingDependencies(array $dependencies): array { $missingPackages = []; $missingOptionalPackages = []; foreach ($dependencies as $package) { if (class_exists($package['class']) || interface_exists($package['class']) || trait_exists($package['class'])) { continue; } if (true === $package['required']) { $missingPackages[] = $package['name']; } else { $missingOptionalPackages[] = $package['name']; } } if (empty($missingPackages)) { return []; } return array_unique(array_merge($missingPackages, $missingOptionalPackages)); } } src/EventRegistry.php 0000644 00000014577 15120141002 0010656 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle; use Symfony\Component\Console\Event\ConsoleCommandEvent; use Symfony\Component\Console\Event\ConsoleErrorEvent; use Symfony\Component\Console\Event\ConsoleTerminateEvent; use Symfony\Component\EventDispatcher\Event as LegacyEvent; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent; use Symfony\Component\HttpKernel\Event\ControllerEvent; use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\Event\FilterControllerArgumentsEvent; use Symfony\Component\HttpKernel\Event\FilterControllerEvent; use Symfony\Component\HttpKernel\Event\FilterResponseEvent; use Symfony\Component\HttpKernel\Event\FinishRequestEvent; use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent; use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; use Symfony\Component\HttpKernel\Event\PostResponseEvent; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\Event\TerminateEvent; use Symfony\Component\HttpKernel\Event\ViewEvent; use Symfony\Component\Security\Core\Event\AuthenticationFailureEvent; use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent; use Symfony\Component\Security\Http\Event\InteractiveLoginEvent; use Symfony\Component\Security\Http\Event\SwitchUserEvent; use Symfony\Contracts\EventDispatcher\Event; /** * @internal */ class EventRegistry { // list of *known* events to always include (if they exist) private static $newEventsMap = [ 'kernel.exception' => ExceptionEvent::class, 'kernel.request' => RequestEvent::class, 'kernel.response' => ResponseEvent::class, 'kernel.view' => ViewEvent::class, 'kernel.controller_arguments' => ControllerArgumentsEvent::class, 'kernel.controller' => ControllerEvent::class, 'kernel.terminate' => TerminateEvent::class, ]; private static $eventsMap = [ 'console.command' => ConsoleCommandEvent::class, 'console.terminate' => ConsoleTerminateEvent::class, 'console.error' => ConsoleErrorEvent::class, 'kernel.request' => GetResponseEvent::class, 'kernel.exception' => GetResponseForExceptionEvent::class, 'kernel.view' => GetResponseForControllerResultEvent::class, 'kernel.controller' => FilterControllerEvent::class, 'kernel.controller_arguments' => FilterControllerArgumentsEvent::class, 'kernel.response' => FilterResponseEvent::class, 'kernel.terminate' => PostResponseEvent::class, 'kernel.finish_request' => FinishRequestEvent::class, 'security.authentication.success' => AuthenticationSuccessEvent::class, 'security.authentication.failure' => AuthenticationFailureEvent::class, 'security.interactive_login' => InteractiveLoginEvent::class, 'security.switch_user' => SwitchUserEvent::class, ]; private $eventDispatcher; public function __construct(EventDispatcherInterface $eventDispatcher) { $this->eventDispatcher = $eventDispatcher; // Loop through the new event classes foreach (self::$newEventsMap as $eventName => $newEventClass) { // Check if the new event classes exist, if so replace the old one with the new. if (isset(self::$eventsMap[$eventName]) && class_exists($newEventClass)) { self::$eventsMap[$eventName] = $newEventClass; } } } /** * Returns all known event names in the system. */ public function getAllActiveEvents(): array { $activeEvents = []; foreach (self::$eventsMap as $eventName => $eventClass) { if (!class_exists($eventClass)) { continue; } $activeEvents[] = $eventName; } $listeners = $this->eventDispatcher->getListeners(); // Check if these listeners are part of the new events. foreach (array_keys($listeners) as $listenerKey) { if (isset(self::$newEventsMap[$listenerKey])) { unset($listeners[$listenerKey]); } if (!isset(self::$eventsMap[$listenerKey])) { self::$eventsMap[$listenerKey] = $this->getEventClassName($listenerKey); } } $activeEvents = array_unique(array_merge($activeEvents, array_keys($listeners))); asort($activeEvents); return $activeEvents; } /** * Attempts to get the event class for a given event. */ public function getEventClassName(string $event): ?string { // if the event is already a class name, use it if (class_exists($event)) { return $event; } if (isset(self::$eventsMap[$event])) { return self::$eventsMap[$event]; } $listeners = $this->eventDispatcher->getListeners($event); if (empty($listeners)) { return null; } foreach ($listeners as $listener) { if (!\is_array($listener) || 2 !== \count($listener)) { continue; } $reflectionMethod = new \ReflectionMethod($listener[0], $listener[1]); $args = $reflectionMethod->getParameters(); if (!$args) { continue; } if (null !== $type = $args[0]->getType()) { $type = $type instanceof \ReflectionNamedType ? $type->getName() : $type->__toString(); if (LegacyEvent::class === $type && class_exists(Event::class)) { return Event::class; } // ignore an "object" type-hint if ('object' === $type) { continue; } return $type; } } return null; } public function listActiveEvents(array $events): array { foreach ($events as $key => $event) { $events[$key] = sprintf('%s (<fg=yellow>%s</>)', $event, self::$eventsMap[$event]); } return $events; } } src/FileManager.php 0000644 00000015241 15120141002 0010203 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle; use Symfony\Bundle\MakerBundle\Util\AutoloaderUtil; use Symfony\Bundle\MakerBundle\Util\MakerFileLinkFormatter; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Finder\Finder; /** * @author Javier Eguiluz <javier.eguiluz@gmail.com> * @author Ryan Weaver <weaverryan@gmail.com> * * @internal */ class FileManager { private $fs; private $autoloaderUtil; private $makerFileLinkFormatter; private $rootDirectory; /** @var SymfonyStyle */ private $io; private $twigDefaultPath; public function __construct( Filesystem $fs, AutoloaderUtil $autoloaderUtil, MakerFileLinkFormatter $makerFileLinkFormatter, string $rootDirectory, string $twigDefaultPath = null ) { // move FileManagerTest stuff // update EntityRegeneratorTest to mock the autoloader $this->fs = $fs; $this->autoloaderUtil = $autoloaderUtil; $this->makerFileLinkFormatter = $makerFileLinkFormatter; $this->rootDirectory = rtrim($this->realPath($this->normalizeSlashes($rootDirectory)), '/'); $this->twigDefaultPath = $twigDefaultPath ? rtrim($this->relativizePath($twigDefaultPath), '/') : null; } public function setIO(SymfonyStyle $io): void { $this->io = $io; } public function parseTemplate(string $templatePath, array $parameters): string { ob_start(); extract($parameters, \EXTR_SKIP); include $templatePath; return ob_get_clean(); } public function dumpFile(string $filename, string $content): void { $absolutePath = $this->absolutizePath($filename); $newFile = !$this->fileExists($filename); $existingContent = $newFile ? '' : file_get_contents($absolutePath); $comment = $newFile ? '<fg=blue>created</>' : '<fg=yellow>updated</>'; if ($existingContent === $content) { $comment = '<fg=green>no change</>'; } $this->fs->dumpFile($absolutePath, $content); $relativePath = $this->relativizePath($filename); if ($this->io) { $this->io->comment(sprintf( '%s: %s', $comment, $this->makerFileLinkFormatter->makeLinkedPath($absolutePath, $relativePath) )); } } public function fileExists($path): bool { return file_exists($this->absolutizePath($path)); } /** * Attempts to make the path relative to the root directory. * * @param string $absolutePath * * @throws \Exception */ public function relativizePath($absolutePath): string { $absolutePath = $this->normalizeSlashes($absolutePath); // see if the path is even in the root if (false === strpos($absolutePath, $this->rootDirectory)) { return $absolutePath; } $absolutePath = $this->realPath($absolutePath); // str_replace but only the first occurrence $relativePath = ltrim(implode('', explode($this->rootDirectory, $absolutePath, 2)), '/'); if (0 === strpos($relativePath, './')) { $relativePath = substr($relativePath, 2); } return is_dir($absolutePath) ? rtrim($relativePath, '/').'/' : $relativePath; } public function getFileContents(string $path): string { if (!$this->fileExists($path)) { throw new \InvalidArgumentException(sprintf('Cannot find file "%s"', $path)); } return file_get_contents($this->absolutizePath($path)); } public function createFinder(string $in): Finder { $finder = new Finder(); $finder->in($this->absolutizePath($in)); return $finder; } public function isPathInVendor(string $path): bool { return 0 === strpos($this->normalizeSlashes($path), $this->normalizeSlashes($this->rootDirectory.'/vendor/')); } public function absolutizePath($path): string { if (0 === strpos($path, '/')) { return $path; } // support windows drive paths: C:\ or C:/ if (1 === strpos($path, ':\\') || 1 === strpos($path, ':/')) { return $path; } return sprintf('%s/%s', $this->rootDirectory, $path); } /** * @throws \Exception */ public function getRelativePathForFutureClass(string $className): ?string { $path = $this->autoloaderUtil->getPathForFutureClass($className); return null === $path ? null : $this->relativizePath($path); } public function getNamespacePrefixForClass(string $className): string { return $this->autoloaderUtil->getNamespacePrefixForClass($className); } public function isNamespaceConfiguredToAutoload(string $namespace): bool { return $this->autoloaderUtil->isNamespaceConfiguredToAutoload($namespace); } public function getRootDirectory(): string { return $this->rootDirectory; } public function getPathForTemplate(string $filename): string { if (null === $this->twigDefaultPath) { throw new \RuntimeException('Cannot get path for template: is Twig installed?'); } return $this->twigDefaultPath.'/'.$filename; } /** * Resolve '../' in paths (like real_path), but for non-existent files. * * @param string $absolutePath */ private function realPath($absolutePath): string { $finalParts = []; $currentIndex = -1; $absolutePath = $this->normalizeSlashes($absolutePath); foreach (explode('/', $absolutePath) as $pathPart) { if ('..' === $pathPart) { // we need to remove the previous entry if (-1 === $currentIndex) { throw new \Exception(sprintf('Problem making path relative - is the path "%s" absolute?', $absolutePath)); } unset($finalParts[$currentIndex]); --$currentIndex; continue; } $finalParts[] = $pathPart; ++$currentIndex; } $finalPath = implode('/', $finalParts); // Normalize: // => / // Normalize: /./ => / $finalPath = str_replace(['//', '/./'], '/', $finalPath); return $finalPath; } private function normalizeSlashes(string $path) { return str_replace('\\', '/', $path); } } src/Generator.php 0000644 00000022556 15120141002 0007766 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException; use Symfony\Bundle\MakerBundle\Util\ClassNameDetails; use Symfony\Bundle\MakerBundle\Util\PhpCompatUtil; use Symfony\Bundle\MakerBundle\Util\TemplateComponentGenerator; /** * @author Javier Eguiluz <javier.eguiluz@gmail.com> * @author Ryan Weaver <weaverryan@gmail.com> */ class Generator { private $fileManager; private $twigHelper; private $pendingOperations = []; private $namespacePrefix; private $phpCompatUtil; private $templateComponentGenerator; public function __construct(FileManager $fileManager, string $namespacePrefix, PhpCompatUtil $phpCompatUtil = null, TemplateComponentGenerator $templateComponentGenerator = null) { $this->fileManager = $fileManager; $this->twigHelper = new GeneratorTwigHelper($fileManager); $this->namespacePrefix = trim($namespacePrefix, '\\'); if (null === $phpCompatUtil) { $phpCompatUtil = new PhpCompatUtil($fileManager); trigger_deprecation('symfony/maker-bundle', '1.25', 'Initializing Generator without providing an instance of PhpCompatUtil is deprecated.'); } $this->phpCompatUtil = $phpCompatUtil; $this->templateComponentGenerator = $templateComponentGenerator; } /** * Generate a new file for a class from a template. * * @param string $className The fully-qualified class name * @param string $templateName Template name in Resources/skeleton to use * @param array $variables Array of variables to pass to the template * * @return string The path where the file will be created * * @throws \Exception */ public function generateClass(string $className, string $templateName, array $variables = []): string { $targetPath = $this->fileManager->getRelativePathForFutureClass($className); if (null === $targetPath) { throw new \LogicException(sprintf('Could not determine where to locate the new class "%s", maybe try with a full namespace like "\\My\\Full\\Namespace\\%s"', $className, Str::getShortClassName($className))); } $variables = array_merge($variables, [ 'class_name' => Str::getShortClassName($className), 'namespace' => Str::getNamespace($className), ]); $this->addOperation($targetPath, $templateName, $variables); return $targetPath; } /** * Generate a normal file from a template. */ public function generateFile(string $targetPath, string $templateName, array $variables = []) { $variables = array_merge($variables, [ 'helper' => $this->twigHelper, ]); $this->addOperation($targetPath, $templateName, $variables); } public function dumpFile(string $targetPath, string $contents) { $this->pendingOperations[$targetPath] = [ 'contents' => $contents, ]; } public function getFileContentsForPendingOperation(string $targetPath): string { if (!isset($this->pendingOperations[$targetPath])) { throw new RuntimeCommandException(sprintf('File "%s" is not in the Generator\'s pending operations', $targetPath)); } $templatePath = $this->pendingOperations[$targetPath]['template']; $parameters = $this->pendingOperations[$targetPath]['variables']; $templateParameters = array_merge($parameters, [ 'relative_path' => $this->fileManager->relativizePath($targetPath), ]); return $this->fileManager->parseTemplate($templatePath, $templateParameters); } /** * Creates a helper object to get data about a class name. * * Examples: * * // App\Entity\FeaturedProduct * $gen->createClassNameDetails('FeaturedProduct', 'Entity'); * $gen->createClassNameDetails('featured product', 'Entity'); * * // App\Controller\FooController * $gen->createClassNameDetails('foo', 'Controller', 'Controller'); * * // App\Controller\Admin\FooController * $gen->createClassNameDetails('Foo\\Admin', 'Controller', 'Controller'); * * // App\Controller\Security\Voter\CoolController * $gen->createClassNameDetails('Cool', 'Security\Voter', 'Voter'); * * // Full class names can also be passed. Imagine the user has an autoload * // rule where Cool\Stuff lives in a "lib/" directory * // Cool\Stuff\BalloonController * $gen->createClassNameDetails('Cool\\Stuff\\Balloon', 'Controller', 'Controller'); * * @param string $name The short "name" that will be turned into the class name * @param string $namespacePrefix Recommended namespace where this class should live, but *without* the "App\\" part * @param string $suffix Optional suffix to guarantee is on the end of the class */ public function createClassNameDetails(string $name, string $namespacePrefix, string $suffix = '', string $validationErrorMessage = ''): ClassNameDetails { $fullNamespacePrefix = $this->namespacePrefix.'\\'.$namespacePrefix; if ('\\' === $name[0]) { // class is already "absolute" - leave it alone (but strip opening \) $className = substr($name, 1); } else { $className = rtrim($fullNamespacePrefix, '\\').'\\'.Str::asClassName($name, $suffix); } Validator::validateClassName($className, $validationErrorMessage); // if this is a custom class, we may be completely different than the namespace prefix // the best way can do, is find the PSR4 prefix and use that if (0 !== strpos($className, $fullNamespacePrefix)) { $fullNamespacePrefix = $this->fileManager->getNamespacePrefixForClass($className); } return new ClassNameDetails($className, $fullNamespacePrefix, $suffix); } public function getRootDirectory(): string { return $this->fileManager->getRootDirectory(); } private function addOperation(string $targetPath, string $templateName, array $variables) { if ($this->fileManager->fileExists($targetPath)) { throw new RuntimeCommandException(sprintf('The file "%s" can\'t be generated because it already exists.', $this->fileManager->relativizePath($targetPath))); } $variables['relative_path'] = $this->fileManager->relativizePath($targetPath); $variables['use_attributes'] = $this->phpCompatUtil->canUseAttributes(); $variables['use_typed_properties'] = $this->phpCompatUtil->canUseTypedProperties(); $variables['use_union_types'] = $this->phpCompatUtil->canUseUnionTypes(); $templatePath = $templateName; if (!file_exists($templatePath)) { $templatePath = __DIR__.'/Resources/skeleton/'.$templateName; if (!file_exists($templatePath)) { throw new \Exception(sprintf('Cannot find template "%s"', $templateName)); } } $this->pendingOperations[$targetPath] = [ 'template' => $templatePath, 'variables' => $variables, ]; } public function hasPendingOperations(): bool { return !empty($this->pendingOperations); } /** * Actually writes and file changes that are pending. */ public function writeChanges() { foreach ($this->pendingOperations as $targetPath => $templateData) { if (isset($templateData['contents'])) { $this->fileManager->dumpFile($targetPath, $templateData['contents']); continue; } $this->fileManager->dumpFile( $targetPath, $this->getFileContentsForPendingOperation($targetPath, $templateData) ); } $this->pendingOperations = []; } public function getRootNamespace(): string { return $this->namespacePrefix; } public function generateController(string $controllerClassName, string $controllerTemplatePath, array $parameters = []): string { return $this->generateClass( $controllerClassName, $controllerTemplatePath, $parameters + [ 'generator' => $this->templateComponentGenerator, ] ); } /** * Generate a template file. */ public function generateTemplate(string $targetPath, string $templateName, array $variables = []) { $this->generateFile( $this->fileManager->getPathForTemplate($targetPath), $templateName, $variables ); } /** * @deprecated MakerBundle only supports AbstractController::class. This method will be removed in the future. */ public static function getControllerBaseClass(): ClassNameDetails { trigger_deprecation('symfony/maker-bundle', 'v1.41.0', 'MakerBundle only supports AbstractController. This method will be removed in the future.'); return new ClassNameDetails(AbstractController::class, '\\'); } } src/GeneratorTwigHelper.php 0000644 00000005054 15120141002 0011753 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle; /** * @author Sadicov Vladimir <sadikoff@gmail.com> */ final class GeneratorTwigHelper { private $fileManager; public function __construct(FileManager $fileManager) { $this->fileManager = $fileManager; } public function getEntityFieldPrintCode($entity, $field): string { $twigField = preg_replace_callback('/(?!^)_([a-z0-9])/', function ($s) { return strtoupper($s[1]); }, $field['fieldName']); $printCode = $entity.'.'.str_replace('_', '', $twigField); switch ($field['type']) { case 'datetimetz_immutable': case 'datetimetz': $printCode .= ' ? '.$printCode.'|date(\'Y-m-d H:i:s T\') : \'\''; break; case 'datetime_immutable': case 'datetime': $printCode .= ' ? '.$printCode.'|date(\'Y-m-d H:i:s\') : \'\''; break; case 'dateinterval': $printCode .= ' ? '.$printCode.'.format(\'%y year(s), %m month(s), %d day(s)\') : \'\''; break; case 'date_immutable': case 'date': $printCode .= ' ? '.$printCode.'|date(\'Y-m-d\') : \'\''; break; case 'time_immutable': case 'time': $printCode .= ' ? '.$printCode.'|date(\'H:i:s\') : \'\''; break; case 'json': $printCode .= ' ? '.$printCode.'|json_encode : \'\''; break; case 'array': $printCode .= ' ? '.$printCode.'|join(\', \') : \'\''; break; case 'boolean': $printCode .= ' ? \'Yes\' : \'No\''; break; } return $printCode; } public function getHeadPrintCode($title): string { if ($this->fileManager->fileExists($this->fileManager->getPathForTemplate('base.html.twig'))) { return <<<TWIG {% extends 'base.html.twig' %} {% block title %}$title{% endblock %} TWIG; } return <<<HTML <!DOCTYPE html> <title>$title</title> HTML; } public function getFileLink($path, $text = null, $line = 0): string { $text = $text ?: $path; return "<a href=\"{{ '$path'|file_link($line) }}\">$text</a>"; } } src/InputAwareMakerInterface.php 0000644 00000001244 15120141002 0012707 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle; use Symfony\Component\Console\Input\InputInterface; /** * Lets the configureDependencies method access to the command's input. * * @author Kévin Dunglas <dunglas@gmail.com> */ interface InputAwareMakerInterface extends MakerInterface { /** * {@inheritdoc} */ public function configureDependencies(DependencyBuilder $dependencies, InputInterface $input = null); } src/InputConfiguration.php 0000644 00000001374 15120141002 0011662 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle; final class InputConfiguration { private $nonInteractiveArguments = []; /** * Call in MakerInterface::configureCommand() to disable the automatic interactive * prompt for an argument. */ public function setArgumentAsNonInteractive(string $argumentName): void { $this->nonInteractiveArguments[] = $argumentName; } public function getNonInteractiveArguments(): array { return $this->nonInteractiveArguments; } } src/MakerBundle.php 0000644 00000002723 15120141002 0010223 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle; use Symfony\Bundle\MakerBundle\DependencyInjection\CompilerPass\DoctrineAttributesCheckPass; use Symfony\Bundle\MakerBundle\DependencyInjection\CompilerPass\MakeCommandRegistrationPass; use Symfony\Bundle\MakerBundle\DependencyInjection\CompilerPass\RemoveMissingParametersPass; use Symfony\Bundle\MakerBundle\DependencyInjection\CompilerPass\SetDoctrineAnnotatedPrefixesPass; use Symfony\Component\DependencyInjection\Compiler\PassConfig; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; /** * @author Javier Eguiluz <javier.eguiluz@gmail.com> * @author Ryan Weaver <weaverryan@gmail.com> */ class MakerBundle extends Bundle { public function build(ContainerBuilder $container) { // add a priority so we run before the core command pass $container->addCompilerPass(new DoctrineAttributesCheckPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 11); $container->addCompilerPass(new MakeCommandRegistrationPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 10); $container->addCompilerPass(new RemoveMissingParametersPass()); $container->addCompilerPass(new SetDoctrineAnnotatedPrefixesPass()); } } src/MakerInterface.php 0000644 00000003076 15120141002 0010714 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; /** * Interface that all maker commands must implement. * * @method static string getCommandDescription() * * @author Ryan Weaver <ryan@knpuniversity.com> */ interface MakerInterface { /** * Return the command name for your maker (e.g. make:report). */ public static function getCommandName(): string; /** * Configure the command: set description, input arguments, options, etc. * * By default, all arguments will be asked interactively. If you want * to avoid that, use the $inputConfig->setArgumentAsNonInteractive() method. */ public function configureCommand(Command $command, InputConfiguration $inputConfig); /** * Configure any library dependencies that your maker requires. */ public function configureDependencies(DependencyBuilder $dependencies); /** * If necessary, you can use this method to interactively ask the user for input. */ public function interact(InputInterface $input, ConsoleStyle $io, Command $command); /** * Called after normal code generation: allows you to do anything. */ public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator); } src/Str.php 0000644 00000015666 15120141002 0006614 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle; use Doctrine\Inflector\Inflector; use Doctrine\Inflector\InflectorFactory; use Symfony\Component\DependencyInjection\Container; /** * @author Javier Eguiluz <javier.eguiluz@gmail.com> * @author Ryan Weaver <weaverryan@gmail.com> */ final class Str { /** @var Inflector|null */ private static $inflector; /** * Looks for suffixes in strings in a case-insensitive way. */ public static function hasSuffix(string $value, string $suffix): bool { return 0 === strcasecmp($suffix, substr($value, -\strlen($suffix))); } /** * Ensures that the given string ends with the given suffix. If the string * already contains the suffix, it's not added twice. It's case-insensitive * (e.g. value: 'Foocommand' suffix: 'Command' -> result: 'FooCommand'). */ public static function addSuffix(string $value, string $suffix): string { return self::removeSuffix($value, $suffix).$suffix; } /** * Ensures that the given string doesn't end with the given suffix. If the * string contains the suffix multiple times, only the last one is removed. * It's case-insensitive (e.g. value: 'Foocommand' suffix: 'Command' -> result: 'Foo'. */ public static function removeSuffix(string $value, string $suffix): string { return self::hasSuffix($value, $suffix) ? substr($value, 0, -\strlen($suffix)) : $value; } /** * Transforms the given string into the format commonly used by PHP classes, * (e.g. `app:do_this-and_that` -> `AppDoThisAndThat`) but it doesn't check * the validity of the class name. */ public static function asClassName(string $value, string $suffix = ''): string { $value = trim($value); $value = str_replace(['-', '_', '.', ':'], ' ', $value); $value = ucwords($value); $value = str_replace(' ', '', $value); $value = ucfirst($value); $value = self::addSuffix($value, $suffix); return $value; } /** * Transforms the given string into the format commonly used by Twig variables * (e.g. `BlogPostType` -> `blog_post_type`). */ public static function asTwigVariable(string $value): string { $value = trim($value); $value = preg_replace('/[^a-zA-Z0-9_]/', '_', $value); $value = preg_replace('/(?<=\\w)([A-Z])/', '_$1', $value); $value = preg_replace('/_{2,}/', '_', $value); $value = strtolower($value); return $value; } public static function asLowerCamelCase(string $str): string { return lcfirst(self::asCamelCase($str)); } public static function asCamelCase(string $str): string { return strtr(ucwords(strtr($str, ['_' => ' ', '.' => ' ', '\\' => ' '])), [' ' => '']); } public static function asRoutePath(string $value): string { return '/'.str_replace('_', '/', self::asTwigVariable($value)); } public static function asRouteName(string $value): string { $routeName = self::asTwigVariable($value); return str_starts_with($routeName, 'app_') ? $routeName : 'app_'.$routeName; } public static function asSnakeCase(string $value): string { return self::asTwigVariable($value); } public static function asCommand(string $value): string { return str_replace('_', '-', self::asTwigVariable($value)); } public static function asEventMethod(string $eventName): string { return sprintf('on%s', self::asClassName($eventName)); } public static function getShortClassName(string $fullClassName): string { if (empty(self::getNamespace($fullClassName))) { return $fullClassName; } return substr($fullClassName, strrpos($fullClassName, '\\') + 1); } public static function getNamespace(string $fullClassName): string { return substr($fullClassName, 0, strrpos($fullClassName, '\\')); } public static function asFilePath(string $value): string { $value = Container::underscore(trim($value)); $value = str_replace('\\', '/', $value); return $value; } public static function singularCamelCaseToPluralCamelCase(string $camelCase): string { $snake = self::asSnakeCase($camelCase); $words = explode('_', $snake); $words[\count($words) - 1] = self::pluralize($words[\count($words) - 1]); $reSnaked = implode('_', $words); return self::asLowerCamelCase($reSnaked); } public static function pluralCamelCaseToSingular(string $camelCase): string { $snake = self::asSnakeCase($camelCase); $words = explode('_', $snake); $words[\count($words) - 1] = self::singularize($words[\count($words) - 1]); $reSnaked = implode('_', $words); return self::asLowerCamelCase($reSnaked); } public static function getRandomTerm(): string { $adjectives = [ 'tiny', 'delicious', 'gentle', 'agreeable', 'brave', 'orange', 'grumpy', 'fierce', 'victorious', ]; $nouns = [ 'elephant', 'pizza', 'popsicle', 'chef', 'puppy', 'gnome', 'kangaroo', ]; return sprintf('%s %s', $adjectives[array_rand($adjectives)], $nouns[array_rand($nouns)]); } /** * Checks if the given name is a valid PHP variable name. * * @see http://php.net/manual/en/language.variables.basics.php * * @param $name string * * @return bool */ public static function isValidPhpVariableName($name) { return (bool) preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $name, $matches); } public static function areClassesAlphabetical(string $class1, string $class2) { $arr1 = [$class1, $class2]; $arr2 = [$class1, $class2]; sort($arr2); return $arr1[0] == $arr2[0]; } public static function asHumanWords(string $variableName): string { return str_replace(' ', ' ', ucfirst(trim(implode(' ', preg_split('/(?=[A-Z])/', $variableName))))); } private static function pluralize(string $word): string { return static::getInflector()->pluralize($word); } private static function singularize(string $word): string { return static::getInflector()->singularize($word); } private static function getInflector(): Inflector { if (null === static::$inflector) { static::$inflector = InflectorFactory::create()->build(); } return static::$inflector; } } src/Validator.php 0000644 00000021027 15120141002 0007755 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle; use Doctrine\Common\Persistence\ManagerRegistry as LegacyManagerRegistry; use Doctrine\Persistence\ManagerRegistry; use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException; use Symfony\Component\Security\Core\User\UserInterface; /** * @author Javier Eguiluz <javier.eguiluz@gmail.com> * @author Ryan Weaver <weaverryan@gmail.com> * * @internal */ final class Validator { public static function validateClassName(string $className, string $errorMessage = ''): string { // remove potential opening slash so we don't match on it $pieces = explode('\\', ltrim($className, '\\')); $shortClassName = Str::getShortClassName($className); $reservedKeywords = ['__halt_compiler', 'abstract', 'and', 'array', 'as', 'break', 'callable', 'case', 'catch', 'class', 'clone', 'const', 'continue', 'declare', 'default', 'die', 'do', 'echo', 'else', 'elseif', 'empty', 'enddeclare', 'endfor', 'endforeach', 'endif', 'endswitch', 'endwhile', 'eval', 'exit', 'extends', 'final', 'finally', 'for', 'foreach', 'function', 'global', 'goto', 'if', 'implements', 'include', 'include_once', 'instanceof', 'insteadof', 'interface', 'isset', 'list', 'namespace', 'new', 'or', 'print', 'private', 'protected', 'public', 'require', 'require_once', 'return', 'static', 'switch', 'throw', 'trait', 'try', 'unset', 'use', 'var', 'while', 'xor', 'yield', 'int', 'float', 'bool', 'string', 'true', 'false', 'null', 'void', 'iterable', 'object', '__file__', '__line__', '__dir__', '__function__', '__class__', '__method__', '__namespace__', '__trait__', 'self', 'parent', ]; foreach ($pieces as $piece) { if (!mb_check_encoding($piece, 'UTF-8')) { $errorMessage = $errorMessage ?: sprintf('"%s" is not a UTF-8-encoded string.', $piece); throw new RuntimeCommandException($errorMessage); } if (!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $piece)) { $errorMessage = $errorMessage ?: sprintf('"%s" is not valid as a PHP class name (it must start with a letter or underscore, followed by any number of letters, numbers, or underscores)', $className); throw new RuntimeCommandException($errorMessage); } if (\in_array(strtolower($shortClassName), $reservedKeywords, true)) { throw new RuntimeCommandException(sprintf('"%s" is a reserved keyword and thus cannot be used as class name in PHP.', $shortClassName)); } } // return original class name return $className; } public static function notBlank(string $value = null): string { if (null === $value || '' === $value) { throw new RuntimeCommandException('This value cannot be blank.'); } return $value; } public static function validateLength($length) { if (!$length) { return $length; } $result = filter_var($length, \FILTER_VALIDATE_INT, [ 'options' => ['min_range' => 1], ]); if (false === $result) { throw new RuntimeCommandException(sprintf('Invalid length "%s".', $length)); } return $result; } public static function validatePrecision($precision) { if (!$precision) { return $precision; } $result = filter_var($precision, \FILTER_VALIDATE_INT, [ 'options' => ['min_range' => 1, 'max_range' => 65], ]); if (false === $result) { throw new RuntimeCommandException(sprintf('Invalid precision "%s".', $precision)); } return $result; } public static function validateScale($scale) { if (!$scale) { return $scale; } $result = filter_var($scale, \FILTER_VALIDATE_INT, [ 'options' => ['min_range' => 0, 'max_range' => 30], ]); if (false === $result) { throw new RuntimeCommandException(sprintf('Invalid scale "%s".', $scale)); } return $result; } public static function validateBoolean($value) { if ('yes' == $value) { return true; } if ('no' == $value) { return false; } if (null === $valueAsBool = filter_var($value, \FILTER_VALIDATE_BOOLEAN, \FILTER_NULL_ON_FAILURE)) { throw new RuntimeCommandException(sprintf('Invalid bool value "%s".', $value)); } return $valueAsBool; } public static function validatePropertyName(string $name) { // check for valid PHP variable name if (null !== $name && !Str::isValidPhpVariableName($name)) { throw new \InvalidArgumentException(sprintf('"%s" is not a valid PHP property name.', $name)); } return $name; } /** * @param ManagerRegistry|LegacyManagerRegistry $registry */ public static function validateDoctrineFieldName(string $name, $registry) { if (!$registry instanceof ManagerRegistry && !$registry instanceof LegacyManagerRegistry) { throw new \InvalidArgumentException(sprintf('Argument 2 to %s::validateDoctrineFieldName must be an instance of %s, %s passed.', __CLASS__, ManagerRegistry::class, \is_object($registry) ? \get_class($registry) : \gettype($registry))); } // check reserved words if ($registry->getConnection()->getDatabasePlatform()->getReservedKeywordsList()->isKeyword($name)) { throw new \InvalidArgumentException(sprintf('Name "%s" is a reserved word.', $name)); } self::validatePropertyName($name); return $name; } public static function validateEmailAddress(?string $email): string { if (!filter_var($email, \FILTER_VALIDATE_EMAIL)) { throw new RuntimeCommandException(sprintf('"%s" is not a valid email address.', $email)); } return $email; } public static function existsOrNull(string $className = null, array $entities = []) { if (null !== $className) { self::validateClassName($className); if (0 === strpos($className, '\\')) { self::classExists($className); } else { self::entityExists($className, $entities); } } return $className; } public static function classExists(string $className, string $errorMessage = ''): string { self::notBlank($className); if (!class_exists($className)) { $errorMessage = $errorMessage ?: sprintf('Class "%s" doesn\'t exist; please enter an existing full class name.', $className); throw new RuntimeCommandException($errorMessage); } return $className; } public static function entityExists(string $className = null, array $entities = []): string { self::notBlank($className); if (empty($entities)) { throw new RuntimeCommandException('There are no registered entities; please create an entity before using this command.'); } if (0 === strpos($className, '\\')) { self::classExists($className, sprintf('Entity "%s" doesn\'t exist; please enter an existing one or create a new one.', $className)); } if (!\in_array($className, $entities)) { throw new RuntimeCommandException(sprintf('Entity "%s" doesn\'t exist; please enter an existing one or create a new one.', $className)); } return $className; } public static function classDoesNotExist($className): string { self::notBlank($className); if (class_exists($className)) { throw new RuntimeCommandException(sprintf('Class "%s" already exists.', $className)); } return $className; } public static function classIsUserInterface($userClassName): string { self::classExists($userClassName); if (!isset(class_implements($userClassName)[UserInterface::class])) { throw new RuntimeCommandException(sprintf('The class "%s" must implement "%s".', $userClassName, UserInterface::class)); } return $userClassName; } } tools/php-cs-fixer/composer.json 0000644 00000000107 15120141002 0012713 0 ustar 00 { "require": { "friendsofphp/php-cs-fixer": "^3.2" } } tools/twigcs/src/MakerTwigRuleSet.php 0000644 00000002546 15120141002 0013673 0 ustar 00 <?php /* * This file is part of the Symfony MakerBundle package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\MakerBundle\Tools\TwigCS; use FriendsOfTwig\Twigcs\RegEngine\RulesetBuilder; use FriendsOfTwig\Twigcs\RegEngine\RulesetConfigurator; use FriendsOfTwig\Twigcs\Rule; use FriendsOfTwig\Twigcs\Ruleset\RulesetInterface; use FriendsOfTwig\Twigcs\Validator\Violation; /** * @author Jesse Rushlow <jr@rushlow.dev> * * @internal */ final class MakerTwigRuleSet implements RulesetInterface { private $twigMajorVersion; public function __construct(int $twigMajorVersion) { $this->twigMajorVersion = $twigMajorVersion; } /** * {@inheritdoc} */ public function getRules(): array { $configurator = new RulesetConfigurator(); $configurator->setTwigMajorVersion($this->twigMajorVersion); $builder = new RulesetBuilder($configurator); return [ new Rule\RegEngineRule(Violation::SEVERITY_ERROR, $builder->build()), new Rule\TrailingSpace(Violation::SEVERITY_ERROR), new Rule\UnusedMacro(Violation::SEVERITY_WARNING), new Rule\UnusedVariable(Violation::SEVERITY_WARNING), ]; } } tools/twigcs/composer.json 0000644 00000000255 15120141002 0011712 0 ustar 00 { "autoload": { "psr-4": { "Symfony\\Bundle\\MakerBundle\\Tools\\TwigCS\\": "src/" } }, "require": { "friendsoftwig/twigcs": "^4.1|^5.1" } } LICENSE 0000644 00000002051 15120141002 0005531 0 ustar 00 Copyright (c) 2004-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. appveyor.yml 0000644 00000005147 15120141002 0007125 0 ustar 00 build: false platform: - x64 clone_folder: C:\projects\maker-bundle cache: - C:\projects\maker-bundle\vendor -> composer.json # Cache chocolatey packages - C:\ProgramData\chocolatey\bin -> .appveyor.yml - C:\ProgramData\chocolatey\lib -> .appveyor.yml # Cache php install - C:\tools\php -> .appveyor.yml - .phpunit -> phpunit services: - mysql init: - SET PATH=C:\Program Files\OpenSSL;C:\tools\php;%PATH% - SET COMPOSER_NO_INTERACTION=1 - SET PHP=0 # This var is connected to PHP install cache - SET ANSICON=121x90 (121x90) - SET MAKER_DISABLE_FILE_LINKS=1 - SET MAKER_SKIP_MERCURE_TEST=1 environment: TEST_DATABASE_DSN: mysql://root:Password12!@127.0.0.1:3306/test_maker matrix: - dependencies: highest php_ver_target: 7.2.5 install: - ps: Set-Service wuauserv -StartupType Manual - IF EXIST C:\tools\php (SET PHP=1) # Checks for the PHP install being cached - IF %PHP%==0 cinst --params '""/InstallDir:C:\tools\php""' --ignore-checksums -y php --version %php_ver_target% - cd C:\tools\php - IF %PHP%==0 copy php.ini-development php.ini /Y - IF %PHP%==0 echo memory_limit=-1 >> php.ini - IF %PHP%==0 echo serialize_precision=14 >> php.ini - IF %PHP%==0 echo realpath_cache_size=8192k >> php.ini - IF %PHP%==0 echo max_execution_time=1200 >> php.ini - IF %PHP%==0 echo date.timezone="UTC" >> php.ini - IF %PHP%==0 echo extension_dir=ext >> php.ini - IF %PHP%==0 echo opcache.enable_cli=1 >> php.ini - IF %PHP%==0 echo extension=php_openssl.dll >> php.ini - IF %PHP%==0 echo extension=php_mbstring.dll >> php.ini - IF %PHP%==0 echo extension=php_fileinfo.dll >> php.ini - IF %PHP%==0 echo extension=php_mysqli.dll >> php.ini - IF %PHP%==0 echo extension=php_curl.dll >> php.ini - IF %PHP%==0 echo extension=php_pdo_mysql.dll >> php.ini - IF %PHP%==0 echo @php %%~dp0composer.phar %%* > composer.bat - appveyor-retry appveyor DownloadFile https://getcomposer.org/composer-stable.phar - del composer.phar - rename composer-stable.phar composer.phar - cd C:\projects\maker-bundle - composer global require --no-progress --no-scripts --no-plugins symfony/flex - IF %dependencies%==highest appveyor-retry composer update --no-progress --no-suggest --ansi - composer install --no-interaction --no-progress --ansi --no-scripts --working-dir=tools/twigcs - composer install --no-interaction --no-progress --ansi --no-scripts --working-dir=tools/php-cs-fixer - vendor/bin/simple-phpunit install test_script: - cd C:\projects\maker-bundle - vendor/bin/simple-phpunit composer.json 0000644 00000003650 15120141002 0007254 0 ustar 00 { "description": "Symfony Maker helps you create empty commands, controllers, form classes, tests and more so you can forget about writing boilerplate code.", "homepage": "https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html", "name": "symfony/maker-bundle", "type": "symfony-bundle", "license": "MIT", "keywords": ["generator", "code generator", "scaffolding", "scaffold"], "authors": [ { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], "minimum-stability": "dev", "require": { "php": ">=7.2.5", "doctrine/inflector": "^2.0", "nikic/php-parser": "^4.11", "symfony/config": "^5.4.7|^6.0", "symfony/console": "^5.4.7|^6.0", "symfony/dependency-injection": "^5.4.7|^6.0", "symfony/deprecation-contracts": "^2.2|^3", "symfony/filesystem": "^5.4.7|^6.0", "symfony/finder": "^5.4.3|^6.0", "symfony/framework-bundle": "^5.4.7|^6.0", "symfony/http-kernel": "^5.4.7|^6.0" }, "require-dev": { "composer/semver": "^3.0", "doctrine/doctrine-bundle": "^2.4", "doctrine/orm": "^2.10.0", "symfony/http-client": "^5.4.7|^6.0", "symfony/phpunit-bridge": "^5.4.7|^6.0", "symfony/polyfill-php80": "^1.16.0", "symfony/process": "^5.4.7|^6.0", "symfony/security-core": "^5.4.7|^6.0", "symfony/yaml": "^5.4.3|^6.0", "twig/twig": "^2.0|^3.0" }, "config": { "preferred-install": "dist", "sort-packages": true }, "conflict": { "doctrine/orm": "<2.10" }, "autoload": { "psr-4": { "Symfony\\Bundle\\MakerBundle\\": "src/" } }, "autoload-dev": { "psr-4": { "Symfony\\Bundle\\MakerBundle\\Tests\\": "tests/" } }, "extra": { "branch-alias": { "dev-main": "1.0-dev" } } } phpunit.xml.dist 0000644 00000002231 15120141002 0007677 0 ustar 00 <?xml version="1.0" encoding="UTF-8"?> <!-- https://phpunit.de/manual/current/en/appendixes.configuration.html --> <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/6.1/phpunit.xsd" backupGlobals="false" colors="true" bootstrap="vendor/autoload.php" failOnRisky="true" failOnWarning="true" > <php> <ini name="error_reporting" value="-1" /> <env name="TEST_DATABASE_DSN" value="mysql://root:root@127.0.0.1:3306/test_maker?serverVersion=5.7" /> <env name="SYMFONY_DEPRECATIONS_HELPER" value="max[self]=0"/> </php> <testsuites> <testsuite name="Project Test Suite"> <directory>tests/</directory> <exclude>tests/Maker</exclude> <exclude>tests/fixtures</exclude> <exclude>tests/tmp</exclude> </testsuite> <testsuite name="Maker Test Suite"> <directory>tests/Maker</directory> </testsuite> </testsuites> <filter> <whitelist> <directory>./src/</directory> </whitelist> </filter> </phpunit>
Coded With 💗 by
0x6ick