GenerateBundleCommand.php 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  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. 'Use <comment>/</comment> instead of <comment>\\</comment> for the namespace delimiter to avoid any problem.',
  122. '',
  123. ));
  124. $namespace = $dialog->askAndValidate($output, $dialog->getQuestion('Bundle namespace', $input->getOption('namespace')), array('Sensio\Bundle\GeneratorBundle\Command\Validators', 'validateBundleNamespace'), false, $input->getOption('namespace'));
  125. $input->setOption('namespace', $namespace);
  126. // bundle name
  127. $bundle = $input->getOption('bundle-name') ?: strtr($namespace, array('\\Bundle\\' => '', '\\' => ''));
  128. $output->writeln(array(
  129. '',
  130. 'In your code, a bundle is often referenced by its name. It can be the',
  131. 'concatenation of all namespace parts but it\'s really up to you to come',
  132. 'up with a unique name (a good practice is to start with the vendor name).',
  133. 'Based on the namespace, we suggest <comment>'.$bundle.'</comment>.',
  134. '',
  135. ));
  136. $bundle = $dialog->askAndValidate($output, $dialog->getQuestion('Bundle name', $bundle), array('Sensio\Bundle\GeneratorBundle\Command\Validators', 'validateBundleName'), false, $bundle);
  137. $input->setOption('bundle-name', $bundle);
  138. // target dir
  139. $dir = $input->getOption('dir') ?: dirname($this->getContainer()->getParameter('kernel.root_dir')).'/src';
  140. $output->writeln(array(
  141. '',
  142. 'The bundle can be generated anywhere. The suggested default directory uses',
  143. 'the standard conventions.',
  144. '',
  145. ));
  146. $dir = $dialog->askAndValidate($output, $dialog->getQuestion('Target directory', $dir), function ($dir) use ($bundle, $namespace) { return Validators::validateTargetDir($dir, $bundle, $namespace); }, false, $dir);
  147. $input->setOption('dir', $dir);
  148. // format
  149. $output->writeln(array(
  150. '',
  151. 'Determine the format to use for the generated configuration.',
  152. '',
  153. ));
  154. $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'));
  155. $input->setOption('format', $format);
  156. // optional files to generate
  157. $output->writeln(array(
  158. '',
  159. 'To help you getting started faster, the command can generate some',
  160. 'code snippets for you.',
  161. '',
  162. ));
  163. $structure = $input->getOption('structure');
  164. if (!$structure && $dialog->askConfirmation($output, $dialog->getQuestion('Do you want to generate the whole directory structure', 'no', '?'), false)) {
  165. $structure = true;
  166. }
  167. $input->setOption('structure', $structure);
  168. // summary
  169. $output->writeln(array(
  170. '',
  171. $this->getHelper('formatter')->formatBlock('Summary before generation', 'bg=blue;fg=white', true),
  172. '',
  173. 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),
  174. '',
  175. ));
  176. }
  177. protected function checkAutoloader(OutputInterface $output, $namespace, $bundle, $dir)
  178. {
  179. $output->write('Checking that the bundle is autoloaded: ');
  180. if (!class_exists($namespace.'\\'.$bundle)) {
  181. return array(
  182. '- Edit the <comment>app/autoloader.php</comment> file and register the bundle',
  183. ' namespace at the top of the <comment>registerNamespaces()</comment> call:',
  184. '',
  185. sprintf('<comment> \'%s\' => \'%s\',</comment>', $namespace, realpath($dir)),
  186. '',
  187. );
  188. }
  189. }
  190. protected function updateKernel($dialog, InputInterface $input, OutputInterface $output, KernelInterface $kernel, $namespace, $bundle)
  191. {
  192. $auto = true;
  193. if ($input->isInteractive()) {
  194. $auto = $dialog->askConfirmation($output, $dialog->getQuestion('Confirm automatic update of your Kernel', 'yes', '?'), true);
  195. }
  196. $output->write('Enabling the bundle inside the Kernel: ');
  197. $manip = new KernelManipulator($kernel);
  198. try {
  199. $ret = $auto ? $manip->addBundle($namespace.'\\'.$bundle) : false;
  200. if (!$ret) {
  201. $reflected = new \ReflectionObject($kernel);
  202. return array(
  203. sprintf('- Edit <comment>%s</comment>', $reflected->getFilename()),
  204. ' and add the following bundle in the <comment>AppKernel::registerBundles()</comment> method:',
  205. '',
  206. sprintf(' <comment>new %s(),</comment>', $namespace.'\\'.$bundle),
  207. '',
  208. );
  209. }
  210. } catch (\RuntimeException $e) {
  211. return array(
  212. sprintf('Bundle <comment>%s</comment> is already defined in <comment>AppKernel::registerBundles()</comment>.', $namespace.'\\'.$bundle),
  213. '',
  214. );
  215. }
  216. }
  217. protected function updateRouting($dialog, InputInterface $input, OutputInterface $output, $bundle, $format)
  218. {
  219. $auto = true;
  220. if ($input->isInteractive()) {
  221. $auto = $dialog->askConfirmation($output, $dialog->getQuestion('Confirm automatic update of the Routing', 'yes', '?'), true);
  222. }
  223. $output->write('Importing the bundle routing resource: ');
  224. $routing = new RoutingManipulator($this->getContainer()->getParameter('kernel.root_dir').'/config/routing.yml');
  225. try {
  226. $ret = $auto ? $routing->addResource($bundle, $format) : false;
  227. if (!$ret) {
  228. if ('annotation' === $format) {
  229. $help = sprintf(" <comment>resource: \"@%s/Resources/Controller/\"</comment>\n <comment>type: annotation</comment>", $bundle);
  230. } else {
  231. $help = sprintf(" <comment>resource: \"@%s/Resources/config/routing.%s\"</comment>\n", $bundle, $format);
  232. }
  233. $help .= " <comment>prefix: /</comment>\n";
  234. return array(
  235. '- Import the bundle\'s routing resource in the app main routing file:',
  236. '',
  237. sprintf(' <comment>%s:</comment>', $bundle),
  238. $help,
  239. '',
  240. );
  241. }
  242. } catch (\RuntimeException $e) {
  243. return array(
  244. sprintf('Bundle <comment>%s</comment> is already imported.', $bundle),
  245. '',
  246. );
  247. }
  248. }
  249. protected function getGenerator()
  250. {
  251. if (null === $this->generator) {
  252. $this->generator = new BundleGenerator($this->getContainer()->get('filesystem'), __DIR__.'/../Resources/skeleton/bundle');
  253. }
  254. return $this->generator;
  255. }
  256. public function setGenerator(BundleGenerator $generator)
  257. {
  258. $this->generator = $generator;
  259. }
  260. protected function getDialogHelper()
  261. {
  262. $dialog = $this->getHelperSet()->get('dialog');
  263. if (!$dialog || get_class($dialog) !== 'Sensio\Bundle\GeneratorBundle\Command\Helper\DialogHelper') {
  264. $this->getHelperSet()->set($dialog = new DialogHelper());
  265. }
  266. return $dialog;
  267. }
  268. }