GenerateBundleCommand.php 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  1. <?php
  2. /*
  3. * This file is part of the Symfony 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 Sensio\Bundle\GeneratorBundle\Command;
  11. use Symfony\Component\Console\Input\InputOption;
  12. use Symfony\Component\Console\Input\InputInterface;
  13. use Symfony\Component\Console\Output\OutputInterface;
  14. use Symfony\Component\Console\Output\Output;
  15. use Symfony\Component\HttpKernel\KernelInterface;
  16. use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
  17. use Sensio\Bundle\GeneratorBundle\Generator\BundleGenerator;
  18. use Sensio\Bundle\GeneratorBundle\Manipulator\KernelManipulator;
  19. use Sensio\Bundle\GeneratorBundle\Manipulator\RoutingManipulator;
  20. use Sensio\Bundle\GeneratorBundle\Command\Helper\DialogHelper;
  21. /**
  22. * Generates bundles.
  23. *
  24. * @author Fabien Potencier <fabien@symfony.com>
  25. */
  26. class GenerateBundleCommand extends ContainerAwareCommand
  27. {
  28. private $generator;
  29. /**
  30. * @see Command
  31. */
  32. protected function configure()
  33. {
  34. $this
  35. ->setDefinition(array(
  36. new InputOption('namespace', '', InputOption::VALUE_REQUIRED, 'The namespace of the bundle to create'),
  37. new InputOption('dir', '', InputOption::VALUE_REQUIRED, 'The directory where to create the bundle'),
  38. new InputOption('bundle-name', '', InputOption::VALUE_REQUIRED, 'The optional bundle name'),
  39. new InputOption('format', '', InputOption::VALUE_REQUIRED, 'Use the format for configuration files (php, xml, yml, or annotation)', 'annotation'),
  40. new InputOption('structure', '', InputOption::VALUE_NONE, 'Whether to generate the whole directory structure'),
  41. ))
  42. ->setDescription('Generates a bundle')
  43. ->setHelp(<<<EOT
  44. The <info>generate:bundle</info> command helps you generates new bundles.
  45. By default, the command interacts with the developer to tweak the generation.
  46. Any passed option will be used as a default value for the interaction
  47. (<comment>--namespace</comment> is the only one needed if you follow the
  48. conventions):
  49. <info>php app/console generate:bundle --namespace=Acme/BlogBundle</info>
  50. Note that you can use <comment>/</comment> instead of <comment>\\</comment> for the namespace delimiter to avoid any
  51. problem.
  52. If you want to disable any user interaction, use <comment>--no-interaction</comment> but don't forget to pass all needed options:
  53. <info>php app/console generate:bundle --namespace=Acme/BlogBundle --dir=src [--bundle-name=...] --no-interaction</info>
  54. Note that the bundle namespace must end with "Bundle".
  55. EOT
  56. )
  57. ->setName('generate:bundle')
  58. ;
  59. }
  60. /**
  61. * @see Command
  62. *
  63. * @throws \InvalidArgumentException When namespace doesn't end with Bundle
  64. * @throws \RuntimeException When bundle can't be executed
  65. */
  66. protected function execute(InputInterface $input, OutputInterface $output)
  67. {
  68. $dialog = $this->getDialogHelper();
  69. if ($input->isInteractive()) {
  70. if (!$dialog->askConfirmation($output, $dialog->getQuestion('Do you confirm generation', 'yes', '?'), true)) {
  71. $output->writeln('<error>Command aborted</error>');
  72. return 1;
  73. }
  74. }
  75. foreach (array('namespace', 'dir') as $option) {
  76. if (null === $input->getOption($option)) {
  77. throw new \RuntimeException(sprintf('The "%s" option must be provided.', $option));
  78. }
  79. }
  80. $namespace = Validators::validateBundleNamespace($input->getOption('namespace'));
  81. if (!$bundle = $input->getOption('bundle-name')) {
  82. $bundle = strtr($namespace, array('\\' => ''));
  83. }
  84. $bundle = Validators::validateBundleName($bundle);
  85. $dir = Validators::validateTargetDir($input->getOption('dir'), $bundle, $namespace);
  86. $format = Validators::validateFormat($input->getOption('format'));
  87. $structure = $input->getOption('structure');
  88. $dialog->writeSection($output, 'Bundle generation');
  89. if (!$this->getContainer()->get('filesystem')->isAbsolutePath($dir)) {
  90. $dir = getcwd().'/'.$dir;
  91. }
  92. $generator = $this->getGenerator();
  93. $generator->generate($namespace, $bundle, $dir, $format, $structure);
  94. $output->writeln('Generating the bundle code: <info>OK</info>');
  95. $errors = array();
  96. $runner = $dialog->getRunner($output, $errors);
  97. // check that the namespace is already autoloaded
  98. $runner($this->checkAutoloader($output, $namespace, $bundle, $dir));
  99. // register the bundle in the Kernel class
  100. $runner($this->updateKernel($dialog, $input, $output, $this->getContainer()->get('kernel'), $namespace, $bundle));
  101. // routing
  102. $runner($this->updateRouting($dialog, $input, $output, $bundle, $format));
  103. $dialog->writeGeneratorSummary($output, $errors);
  104. }
  105. protected function interact(InputInterface $input, OutputInterface $output)
  106. {
  107. $dialog = $this->getDialogHelper();
  108. $dialog->writeSection($output, 'Welcome to the Symfony2 bundle generator');
  109. // namespace
  110. $output->writeln(array(
  111. '',
  112. 'Your application code must be written in <comment>bundles</comment>. This command helps',
  113. 'you generate them easily.',
  114. '',
  115. 'Each bundle is hosted under a namespace (like <comment>Acme/Bundle/BlogBundle</comment>).',
  116. 'The namespace should begin with a "vendor" name like your company name, your',
  117. 'project name, or your client name, followed by one or more optional category',
  118. 'sub-namespaces, and it should end with the bundle name itself',
  119. '(which must have <comment>Bundle</comment> as a suffix).',
  120. '',
  121. 'See http://symfony.com/doc/current/cookbook/bundles/best_practices.html#index-1 for more',
  122. 'details on bundle naming conventions.',
  123. '',
  124. 'Use <comment>/</comment> instead of <comment>\\</comment> for the namespace delimiter to avoid any problem.',
  125. '',
  126. ));
  127. $namespace = $dialog->askAndValidate($output, $dialog->getQuestion('Bundle namespace', $input->getOption('namespace')), array('Sensio\Bundle\GeneratorBundle\Command\Validators', 'validateBundleNamespace'), false, $input->getOption('namespace'));
  128. $input->setOption('namespace', $namespace);
  129. // bundle name
  130. $bundle = $input->getOption('bundle-name') ?: strtr($namespace, array('\\Bundle\\' => '', '\\' => ''));
  131. $output->writeln(array(
  132. '',
  133. 'In your code, a bundle is often referenced by its name. It can be the',
  134. 'concatenation of all namespace parts but it\'s really up to you to come',
  135. 'up with a unique name (a good practice is to start with the vendor name).',
  136. 'Based on the namespace, we suggest <comment>'.$bundle.'</comment>.',
  137. '',
  138. ));
  139. $bundle = $dialog->askAndValidate($output, $dialog->getQuestion('Bundle name', $bundle), array('Sensio\Bundle\GeneratorBundle\Command\Validators', 'validateBundleName'), false, $bundle);
  140. $input->setOption('bundle-name', $bundle);
  141. // target dir
  142. $dir = $input->getOption('dir') ?: dirname($this->getContainer()->getParameter('kernel.root_dir')).'/src';
  143. $output->writeln(array(
  144. '',
  145. 'The bundle can be generated anywhere. The suggested default directory uses',
  146. 'the standard conventions.',
  147. '',
  148. ));
  149. $dir = $dialog->askAndValidate($output, $dialog->getQuestion('Target directory', $dir), function ($dir) use ($bundle, $namespace) { return Validators::validateTargetDir($dir, $bundle, $namespace); }, false, $dir);
  150. $input->setOption('dir', $dir);
  151. // format
  152. $output->writeln(array(
  153. '',
  154. 'Determine the format to use for the generated configuration.',
  155. '',
  156. ));
  157. $format = $dialog->askAndValidate($output, $dialog->getQuestion('Configuration format (yml, xml, php, or annotation)', $input->getOption('format')), array('Sensio\Bundle\GeneratorBundle\Command\Validators', 'validateFormat'), false, $input->getOption('format'));
  158. $input->setOption('format', $format);
  159. // optional files to generate
  160. $output->writeln(array(
  161. '',
  162. 'To help you getting started faster, the command can generate some',
  163. 'code snippets for you.',
  164. '',
  165. ));
  166. $structure = $input->getOption('structure');
  167. if (!$structure && $dialog->askConfirmation($output, $dialog->getQuestion('Do you want to generate the whole directory structure', 'no', '?'), false)) {
  168. $structure = true;
  169. }
  170. $input->setOption('structure', $structure);
  171. // summary
  172. $output->writeln(array(
  173. '',
  174. $this->getHelper('formatter')->formatBlock('Summary before generation', 'bg=blue;fg=white', true),
  175. '',
  176. sprintf("You are going to generate a \"<info>%s\\%s</info>\" bundle\nin \"<info>%s</info>\" using the \"<info>%s</info>\" format.", $namespace, $bundle, $dir, $format),
  177. '',
  178. ));
  179. }
  180. protected function checkAutoloader(OutputInterface $output, $namespace, $bundle, $dir)
  181. {
  182. $output->write('Checking that the bundle is autoloaded: ');
  183. if (!class_exists($namespace.'\\'.$bundle)) {
  184. return array(
  185. '- Edit the <comment>app/autoload.php</comment> file and register the bundle',
  186. ' namespace at the top of the <comment>registerNamespaces()</comment> call:',
  187. '',
  188. sprintf('<comment> \'%s\' => \'%s\',</comment>', $namespace, realpath($dir)),
  189. '',
  190. );
  191. }
  192. }
  193. protected function updateKernel($dialog, InputInterface $input, OutputInterface $output, KernelInterface $kernel, $namespace, $bundle)
  194. {
  195. $auto = true;
  196. if ($input->isInteractive()) {
  197. $auto = $dialog->askConfirmation($output, $dialog->getQuestion('Confirm automatic update of your Kernel', 'yes', '?'), true);
  198. }
  199. $output->write('Enabling the bundle inside the Kernel: ');
  200. $manip = new KernelManipulator($kernel);
  201. try {
  202. $ret = $auto ? $manip->addBundle($namespace.'\\'.$bundle) : false;
  203. if (!$ret) {
  204. $reflected = new \ReflectionObject($kernel);
  205. return array(
  206. sprintf('- Edit <comment>%s</comment>', $reflected->getFilename()),
  207. ' and add the following bundle in the <comment>AppKernel::registerBundles()</comment> method:',
  208. '',
  209. sprintf(' <comment>new %s(),</comment>', $namespace.'\\'.$bundle),
  210. '',
  211. );
  212. }
  213. } catch (\RuntimeException $e) {
  214. return array(
  215. sprintf('Bundle <comment>%s</comment> is already defined in <comment>AppKernel::registerBundles()</comment>.', $namespace.'\\'.$bundle),
  216. '',
  217. );
  218. }
  219. }
  220. protected function updateRouting($dialog, InputInterface $input, OutputInterface $output, $bundle, $format)
  221. {
  222. $auto = true;
  223. if ($input->isInteractive()) {
  224. $auto = $dialog->askConfirmation($output, $dialog->getQuestion('Confirm automatic update of the Routing', 'yes', '?'), true);
  225. }
  226. $output->write('Importing the bundle routing resource: ');
  227. $routing = new RoutingManipulator($this->getContainer()->getParameter('kernel.root_dir').'/config/routing.yml');
  228. try {
  229. $ret = $auto ? $routing->addResource($bundle, $format) : false;
  230. if (!$ret) {
  231. if ('annotation' === $format) {
  232. $help = sprintf(" <comment>resource: \"@%s/Controller/\"</comment>\n <comment>type: annotation</comment>\n", $bundle);
  233. } else {
  234. $help = sprintf(" <comment>resource: \"@%s/Resources/config/routing.%s\"</comment>\n", $bundle, $format);
  235. }
  236. $help .= " <comment>prefix: /</comment>\n";
  237. return array(
  238. '- Import the bundle\'s routing resource in the app main routing file:',
  239. '',
  240. sprintf(' <comment>%s:</comment>', $bundle),
  241. $help,
  242. '',
  243. );
  244. }
  245. } catch (\RuntimeException $e) {
  246. return array(
  247. sprintf('Bundle <comment>%s</comment> is already imported.', $bundle),
  248. '',
  249. );
  250. }
  251. }
  252. protected function getGenerator()
  253. {
  254. if (null === $this->generator) {
  255. $this->generator = new BundleGenerator($this->getContainer()->get('filesystem'), __DIR__.'/../Resources/skeleton/bundle');
  256. }
  257. return $this->generator;
  258. }
  259. public function setGenerator(BundleGenerator $generator)
  260. {
  261. $this->generator = $generator;
  262. }
  263. protected function getDialogHelper()
  264. {
  265. $dialog = $this->getHelperSet()->get('dialog');
  266. if (!$dialog || get_class($dialog) !== 'Sensio\Bundle\GeneratorBundle\Command\Helper\DialogHelper') {
  267. $this->getHelperSet()->set($dialog = new DialogHelper());
  268. }
  269. return $dialog;
  270. }
  271. }