vendor/symfony/maker-bundle/src/Maker/MakeUser.php line 234

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the Symfony MakerBundle package.
  4.  *
  5.  * (c) Fabien Potencier <fabien@symfony.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Symfony\Bundle\MakerBundle\Maker;
  11. use Doctrine\Bundle\DoctrineBundle\DoctrineBundle;
  12. use Symfony\Bundle\MakerBundle\ConsoleStyle;
  13. use Symfony\Bundle\MakerBundle\DependencyBuilder;
  14. use Symfony\Bundle\MakerBundle\Doctrine\DoctrineHelper;
  15. use Symfony\Bundle\MakerBundle\Doctrine\EntityClassGenerator;
  16. use Symfony\Bundle\MakerBundle\Doctrine\ORMDependencyBuilder;
  17. use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException;
  18. use Symfony\Bundle\MakerBundle\FileManager;
  19. use Symfony\Bundle\MakerBundle\Generator;
  20. use Symfony\Bundle\MakerBundle\InputConfiguration;
  21. use Symfony\Bundle\MakerBundle\Security\SecurityConfigUpdater;
  22. use Symfony\Bundle\MakerBundle\Security\UserClassBuilder;
  23. use Symfony\Bundle\MakerBundle\Security\UserClassConfiguration;
  24. use Symfony\Bundle\MakerBundle\Util\ClassSourceManipulator;
  25. use Symfony\Bundle\MakerBundle\Util\UseStatementGenerator;
  26. use Symfony\Bundle\MakerBundle\Util\YamlManipulationFailedException;
  27. use Symfony\Bundle\MakerBundle\Validator;
  28. use Symfony\Bundle\SecurityBundle\SecurityBundle;
  29. use Symfony\Component\Console\Command\Command;
  30. use Symfony\Component\Console\Input\InputArgument;
  31. use Symfony\Component\Console\Input\InputInterface;
  32. use Symfony\Component\Console\Input\InputOption;
  33. use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
  34. use Symfony\Component\Security\Core\Exception\UserNotFoundException;
  35. use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
  36. use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
  37. use Symfony\Component\Security\Core\User\UserInterface;
  38. use Symfony\Component\Security\Core\User\UserProviderInterface;
  39. use Symfony\Component\Yaml\Yaml;
  40. /**
  41.  * @author Ryan Weaver <weaverryan@gmail.com>
  42.  *
  43.  * @internal
  44.  */
  45. final class MakeUser extends AbstractMaker
  46. {
  47.     public function __construct(
  48.         private FileManager $fileManager,
  49.         private UserClassBuilder $userClassBuilder,
  50.         private SecurityConfigUpdater $configUpdater,
  51.         private EntityClassGenerator $entityClassGenerator,
  52.         private DoctrineHelper $doctrineHelper,
  53.     ) {
  54.     }
  55.     public static function getCommandName(): string
  56.     {
  57.         return 'make:user';
  58.     }
  59.     public static function getCommandDescription(): string
  60.     {
  61.         return 'Creates a new security user class';
  62.     }
  63.     public function configureCommand(Command $commandInputConfiguration $inputConfig): void
  64.     {
  65.         $command
  66.             ->addArgument('name'InputArgument::OPTIONAL'The name of the security user class (e.g. <fg=yellow>User</>)')
  67.             ->addOption('is-entity'nullInputOption::VALUE_NONE'Do you want to store user data in the database (via Doctrine)?')
  68.             ->addOption('identity-property-name'nullInputOption::VALUE_REQUIRED'Enter a property name that will be the unique "display" name for the user (e.g. <comment>email, username, uuid</comment>)')
  69.             ->addOption('with-password'nullInputOption::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)')
  70.             ->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeUser.txt'));
  71.         $inputConfig->setArgumentAsNonInteractive('name');
  72.     }
  73.     public function interact(InputInterface $inputConsoleStyle $ioCommand $command): void
  74.     {
  75.         if (null === $input->getArgument('name')) {
  76.             $name $io->ask(
  77.                 $command->getDefinition()->getArgument('name')->getDescription(),
  78.                 'User'
  79.             );
  80.             $input->setArgument('name'$name);
  81.         }
  82.         $userIsEntity $io->confirm(
  83.             'Do you want to store user data in the database (via Doctrine)?',
  84.             class_exists(DoctrineBundle::class)
  85.         );
  86.         if ($userIsEntity) {
  87.             $dependencies = new DependencyBuilder();
  88.             ORMDependencyBuilder::buildDependencies($dependencies);
  89.             $missingPackagesMessage $dependencies->getMissingPackagesMessage(self::getCommandName(), 'Doctrine must be installed to store user data in the database');
  90.             if ($missingPackagesMessage) {
  91.                 throw new RuntimeCommandException($missingPackagesMessage);
  92.             }
  93.         }
  94.         $input->setOption('is-entity'$userIsEntity);
  95.         $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']);
  96.         $input->setOption('identity-property-name'$identityFieldName);
  97.         $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).');
  98.         $userWillHavePassword $io->confirm('Does this app need to hash/check user passwords?');
  99.         $input->setOption('with-password'$userWillHavePassword);
  100.     }
  101.     public function generate(InputInterface $inputConsoleStyle $ioGenerator $generator): void
  102.     {
  103.         $userClassConfiguration = new UserClassConfiguration(
  104.             $input->getOption('is-entity'),
  105.             $input->getOption('identity-property-name'),
  106.             $input->getOption('with-password')
  107.         );
  108.         $userClassNameDetails $generator->createClassNameDetails(
  109.             $input->getArgument('name'),
  110.             $userClassConfiguration->isEntity() ? 'Entity\\' 'Security\\'
  111.         );
  112.         // A) Generate the User class
  113.         if ($userClassConfiguration->isEntity()) {
  114.             $classPath $this->entityClassGenerator->generateEntityClass(
  115.                 $userClassNameDetails,
  116.                 false// api resource
  117.                 $userClassConfiguration->hasPassword() // security user
  118.             );
  119.         } else {
  120.             $classPath $generator->generateClass($userClassNameDetails->getFullName(), 'Class.tpl.php');
  121.         }
  122.         // need to write changes early so we can modify the contents below
  123.         $generator->writeChanges();
  124.         $entityUsesAttributes = ($isEntity $userClassConfiguration->isEntity()) && $this->doctrineHelper->doesClassUsesAttributes($userClassNameDetails->getFullName());
  125.         if ($isEntity && !$entityUsesAttributes) {
  126.             throw new \RuntimeException('MakeUser only supports attribute mapping with doctrine entities.');
  127.         }
  128.         // B) Implement UserInterface
  129.         $manipulator = new ClassSourceManipulator(
  130.             sourceCode$this->fileManager->getFileContents($classPath),
  131.             overwritetrue,
  132.             useAttributesForDoctrineMapping$entityUsesAttributes
  133.         );
  134.         $manipulator->setIo($io);
  135.         $this->userClassBuilder->addUserInterfaceImplementation($manipulator$userClassConfiguration);
  136.         $generator->dumpFile($classPath$manipulator->getSourceCode());
  137.         // C) Generate a custom user provider, if necessary
  138.         if (!$userClassConfiguration->isEntity()) {
  139.             $userClassConfiguration->setUserProviderClass($generator->getRootNamespace().'\\Security\\UserProvider');
  140.             $useStatements = new UseStatementGenerator([
  141.                 UnsupportedUserException::class,
  142.                 UserNotFoundException::class,
  143.                 PasswordAuthenticatedUserInterface::class,
  144.                 PasswordUpgraderInterface::class,
  145.                 UserInterface::class,
  146.                 UserProviderInterface::class,
  147.             ]);
  148.             $customProviderPath $generator->generateClass(
  149.                 $userClassConfiguration->getUserProviderClass(),
  150.                 'security/UserProvider.tpl.php',
  151.                 [
  152.                     'use_statements' => $useStatements,
  153.                     'user_short_name' => $userClassNameDetails->getShortName(),
  154.                 ]
  155.             );
  156.         }
  157.         // D) Update security.yaml
  158.         $securityYamlUpdated false;
  159.         $path 'config/packages/security.yaml';
  160.         if ($this->fileManager->fileExists($path)) {
  161.             try {
  162.                 $newYaml $this->configUpdater->updateForUserClass(
  163.                     $this->fileManager->getFileContents($path),
  164.                     $userClassConfiguration,
  165.                     $userClassNameDetails->getFullName()
  166.                 );
  167.                 $generator->dumpFile($path$newYaml);
  168.                 $securityYamlUpdated true;
  169.             } catch (YamlManipulationFailedException) {
  170.             }
  171.         }
  172.         $generator->writeChanges();
  173.         $this->writeSuccessMessage($io);
  174.         $io->text('Next Steps:');
  175.         $nextSteps = [
  176.             sprintf('Review your new <info>%s</info> class.'$userClassNameDetails->getFullName()),
  177.         ];
  178.         if ($userClassConfiguration->isEntity()) {
  179.             $nextSteps[] = sprintf(
  180.                 'Use <comment>make:entity</comment> to add more fields to your <info>%s</info> entity and then run <comment>make:migration</comment>.',
  181.                 $userClassNameDetails->getShortName()
  182.             );
  183.         } else {
  184.             $nextSteps[] = sprintf(
  185.                 'Open <info>%s</info> to finish implementing your user provider.',
  186.                 $this->fileManager->relativizePath($customProviderPath)
  187.             );
  188.         }
  189.         if (!$securityYamlUpdated) {
  190.             $yamlExample $this->configUpdater->updateForUserClass(
  191.                 'security: {}',
  192.                 $userClassConfiguration,
  193.                 $userClassNameDetails->getFullName()
  194.             );
  195.             $nextSteps[] = "Your <info>security.yaml</info> could not be updated automatically. You'll need to add the following config manually:\n\n".$yamlExample;
  196.         }
  197.         $nextSteps[] = 'Create a way to authenticate! See https://symfony.com/doc/current/security.html';
  198.         $nextSteps array_map(static fn ($step) => sprintf('  - %s'$step), $nextSteps);
  199.         $io->text($nextSteps);
  200.     }
  201.     public function configureDependencies(DependencyBuilder $dependenciesInputInterface $input null): void
  202.     {
  203.         // checking for SecurityBundle guarantees security.yaml is present
  204.         $dependencies->addClassDependency(
  205.             SecurityBundle::class,
  206.             'security'
  207.         );
  208.         // needed to update the YAML files
  209.         $dependencies->addClassDependency(
  210.             Yaml::class,
  211.             'yaml'
  212.         );
  213.         if (null !== $input && $input->getOption('is-entity')) {
  214.             ORMDependencyBuilder::buildDependencies($dependencies);
  215.         }
  216.     }
  217. }