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

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. private $fileManager;
  48. private $userClassBuilder;
  49. private $configUpdater;
  50. private $entityClassGenerator;
  51. private $doctrineHelper;
  52. public function __construct(FileManager $fileManager, UserClassBuilder $userClassBuilder, SecurityConfigUpdater $configUpdater, EntityClassGenerator $entityClassGenerator, DoctrineHelper $doctrineHelper)
  53. {
  54. $this->fileManager = $fileManager;
  55. $this->userClassBuilder = $userClassBuilder;
  56. $this->configUpdater = $configUpdater;
  57. $this->entityClassGenerator = $entityClassGenerator;
  58. $this->doctrineHelper = $doctrineHelper;
  59. }
  60. public static function getCommandName(): string
  61. {
  62. return 'make:user';
  63. }
  64. public static function getCommandDescription(): string
  65. {
  66. return 'Creates a new security user class';
  67. }
  68. public function configureCommand(Command $command, InputConfiguration $inputConfig): void
  69. {
  70. $command
  71. ->addArgument('name', InputArgument::OPTIONAL, 'The name of the security user class (e.g. <fg=yellow>User</>)')
  72. ->addOption('is-entity', null, InputOption::VALUE_NONE, 'Do you want to store user data in the database (via Doctrine)?')
  73. ->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>)')
  74. ->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)')
  75. ->addOption('use-argon2', null, InputOption::VALUE_NONE, 'Use the Argon2i password encoder? (deprecated)')
  76. ->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeUser.txt'));
  77. $inputConfig->setArgumentAsNonInteractive('name');
  78. }
  79. public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void
  80. {
  81. if (null === $input->getArgument('name')) {
  82. $name = $io->ask(
  83. $command->getDefinition()->getArgument('name')->getDescription(),
  84. 'User'
  85. );
  86. $input->setArgument('name', $name);
  87. }
  88. $userIsEntity = $io->confirm(
  89. 'Do you want to store user data in the database (via Doctrine)?',
  90. class_exists(DoctrineBundle::class)
  91. );
  92. if ($userIsEntity) {
  93. $dependencies = new DependencyBuilder();
  94. ORMDependencyBuilder::buildDependencies($dependencies);
  95. $missingPackagesMessage = $dependencies->getMissingPackagesMessage(self::getCommandName(), 'Doctrine must be installed to store user data in the database');
  96. if ($missingPackagesMessage) {
  97. throw new RuntimeCommandException($missingPackagesMessage);
  98. }
  99. }
  100. $input->setOption('is-entity', $userIsEntity);
  101. $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']);
  102. $input->setOption('identity-property-name', $identityFieldName);
  103. $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).');
  104. $userWillHavePassword = $io->confirm('Does this app need to hash/check user passwords?');
  105. $input->setOption('with-password', $userWillHavePassword);
  106. }
  107. public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void
  108. {
  109. $userClassConfiguration = new UserClassConfiguration(
  110. $input->getOption('is-entity'),
  111. $input->getOption('identity-property-name'),
  112. $input->getOption('with-password')
  113. );
  114. if ($input->getOption('use-argon2')) {
  115. @trigger_error('The "--use-argon2" option is deprecated since MakerBundle 1.12.', \E_USER_DEPRECATED);
  116. $userClassConfiguration->useArgon2(true);
  117. }
  118. $userClassNameDetails = $generator->createClassNameDetails(
  119. $input->getArgument('name'),
  120. $userClassConfiguration->isEntity() ? 'Entity\\' : 'Security\\'
  121. );
  122. // A) Generate the User class
  123. if ($userClassConfiguration->isEntity()) {
  124. $classPath = $this->entityClassGenerator->generateEntityClass(
  125. $userClassNameDetails,
  126. false, // api resource
  127. $userClassConfiguration->hasPassword() // security user
  128. );
  129. } else {
  130. $classPath = $generator->generateClass($userClassNameDetails->getFullName(), 'Class.tpl.php');
  131. }
  132. // need to write changes early so we can modify the contents below
  133. $generator->writeChanges();
  134. $useAttributesForDoctrineMapping = $userClassConfiguration->isEntity() && ($this->doctrineHelper->isDoctrineSupportingAttributes()) && $this->doctrineHelper->doesClassUsesAttributes($userClassNameDetails->getFullName());
  135. // B) Implement UserInterface
  136. $manipulator = new ClassSourceManipulator(
  137. $this->fileManager->getFileContents($classPath),
  138. true,
  139. !$useAttributesForDoctrineMapping,
  140. true,
  141. $useAttributesForDoctrineMapping
  142. );
  143. $manipulator->setIo($io);
  144. $this->userClassBuilder->addUserInterfaceImplementation($manipulator, $userClassConfiguration);
  145. $generator->dumpFile($classPath, $manipulator->getSourceCode());
  146. // C) Generate a custom user provider, if necessary
  147. if (!$userClassConfiguration->isEntity()) {
  148. $userClassConfiguration->setUserProviderClass($generator->getRootNamespace().'\\Security\\UserProvider');
  149. $useStatements = new UseStatementGenerator([
  150. UnsupportedUserException::class,
  151. UserNotFoundException::class,
  152. PasswordAuthenticatedUserInterface::class,
  153. PasswordUpgraderInterface::class,
  154. UserInterface::class,
  155. UserProviderInterface::class,
  156. ]);
  157. $customProviderPath = $generator->generateClass(
  158. $userClassConfiguration->getUserProviderClass(),
  159. 'security/UserProvider.tpl.php',
  160. [
  161. 'use_statements' => $useStatements,
  162. 'user_short_name' => $userClassNameDetails->getShortName(),
  163. ]
  164. );
  165. }
  166. // D) Update security.yaml
  167. $securityYamlUpdated = false;
  168. $path = 'config/packages/security.yaml';
  169. if ($this->fileManager->fileExists($path)) {
  170. try {
  171. $newYaml = $this->configUpdater->updateForUserClass(
  172. $this->fileManager->getFileContents($path),
  173. $userClassConfiguration,
  174. $userClassNameDetails->getFullName()
  175. );
  176. $generator->dumpFile($path, $newYaml);
  177. $securityYamlUpdated = true;
  178. } catch (YamlManipulationFailedException $e) {
  179. }
  180. }
  181. $generator->writeChanges();
  182. $this->writeSuccessMessage($io);
  183. $io->text('Next Steps:');
  184. $nextSteps = [
  185. sprintf('Review your new <info>%s</info> class.', $userClassNameDetails->getFullName()),
  186. ];
  187. if ($userClassConfiguration->isEntity()) {
  188. $nextSteps[] = sprintf(
  189. 'Use <comment>make:entity</comment> to add more fields to your <info>%s</info> entity and then run <comment>make:migration</comment>.',
  190. $userClassNameDetails->getShortName()
  191. );
  192. } else {
  193. $nextSteps[] = sprintf(
  194. 'Open <info>%s</info> to finish implementing your user provider.',
  195. $this->fileManager->relativizePath($customProviderPath)
  196. );
  197. }
  198. if (!$securityYamlUpdated) {
  199. $yamlExample = $this->configUpdater->updateForUserClass(
  200. 'security: {}',
  201. $userClassConfiguration,
  202. $userClassNameDetails->getFullName()
  203. );
  204. $nextSteps[] = "Your <info>security.yaml</info> could not be updated automatically. You'll need to add the following config manually:\n\n".$yamlExample;
  205. }
  206. $nextSteps[] = 'Create a way to authenticate! See https://symfony.com/doc/current/security.html';
  207. $nextSteps = array_map(function ($step) {
  208. return sprintf(' - %s', $step);
  209. }, $nextSteps);
  210. $io->text($nextSteps);
  211. }
  212. public function configureDependencies(DependencyBuilder $dependencies, InputInterface $input = null): void
  213. {
  214. // checking for SecurityBundle guarantees security.yaml is present
  215. $dependencies->addClassDependency(
  216. SecurityBundle::class,
  217. 'security'
  218. );
  219. // needed to update the YAML files
  220. $dependencies->addClassDependency(
  221. Yaml::class,
  222. 'yaml'
  223. );
  224. if (null !== $input && $input->getOption('is-entity')) {
  225. ORMDependencyBuilder::buildDependencies($dependencies);
  226. }
  227. }
  228. }