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

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