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 $command, InputConfiguration $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', null, InputOption::VALUE_NONE, 'Do you want to store user data in the database (via Doctrine)?')
  68. ->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>)')
  69. ->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)')
  70. ->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeUser.txt'));
  71. $inputConfig->setArgumentAsNonInteractive('name');
  72. }
  73. public function interact(InputInterface $input, ConsoleStyle $io, Command $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 $input, ConsoleStyle $io, Generator $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. overwrite: true,
  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 $dependencies, InputInterface $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. }