AssetsInstallCommand.php 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  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 Symfony\Bundle\FrameworkBundle\Command;
  11. use Symfony\Component\Console\Input\InputArgument;
  12. use Symfony\Component\Console\Input\InputInterface;
  13. use Symfony\Component\Console\Input\InputOption;
  14. use Symfony\Component\Console\Output\OutputInterface;
  15. use Symfony\Component\Console\Style\SymfonyStyle;
  16. use Symfony\Component\Filesystem\Exception\IOException;
  17. use Symfony\Component\Filesystem\Filesystem;
  18. use Symfony\Component\Finder\Finder;
  19. use Symfony\Component\HttpKernel\Bundle\BundleInterface;
  20. /**
  21. * Command that places bundle web assets into a given directory.
  22. *
  23. * @author Fabien Potencier <fabien@symfony.com>
  24. * @author Gábor Egyed <gabor.egyed@gmail.com>
  25. */
  26. class AssetsInstallCommand extends ContainerAwareCommand
  27. {
  28. const METHOD_COPY = 'copy';
  29. const METHOD_ABSOLUTE_SYMLINK = 'absolute symlink';
  30. const METHOD_RELATIVE_SYMLINK = 'relative symlink';
  31. /**
  32. * @var Filesystem
  33. */
  34. private $filesystem;
  35. /**
  36. * {@inheritdoc}
  37. */
  38. protected function configure()
  39. {
  40. $this
  41. ->setName('assets:install')
  42. ->setDefinition(array(
  43. new InputArgument('target', InputArgument::OPTIONAL, 'The target directory', 'web'),
  44. ))
  45. ->addOption('symlink', null, InputOption::VALUE_NONE, 'Symlinks the assets instead of copying it')
  46. ->addOption('relative', null, InputOption::VALUE_NONE, 'Make relative symlinks')
  47. ->setDescription('Installs bundles web assets under a public web directory')
  48. ->setHelp(<<<'EOT'
  49. The <info>%command.name%</info> command installs bundle assets into a given
  50. directory (e.g. the <comment>web</comment> directory).
  51. <info>php %command.full_name% web</info>
  52. A "bundles" directory will be created inside the target directory and the
  53. "Resources/public" directory of each bundle will be copied into it.
  54. To create a symlink to each bundle instead of copying its assets, use the
  55. <info>--symlink</info> option (will fall back to hard copies when symbolic links aren't possible:
  56. <info>php %command.full_name% web --symlink</info>
  57. To make symlink relative, add the <info>--relative</info> option:
  58. <info>php %command.full_name% web --symlink --relative</info>
  59. EOT
  60. )
  61. ;
  62. }
  63. /**
  64. * {@inheritdoc}
  65. */
  66. protected function execute(InputInterface $input, OutputInterface $output)
  67. {
  68. $targetArg = rtrim($input->getArgument('target'), '/');
  69. if (!is_dir($targetArg)) {
  70. throw new \InvalidArgumentException(sprintf('The target directory "%s" does not exist.', $input->getArgument('target')));
  71. }
  72. $this->filesystem = $this->getContainer()->get('filesystem');
  73. // Create the bundles directory otherwise symlink will fail.
  74. $bundlesDir = $targetArg.'/bundles/';
  75. $this->filesystem->mkdir($bundlesDir, 0777);
  76. $io = new SymfonyStyle($input, $output);
  77. $io->newLine();
  78. if ($input->getOption('relative')) {
  79. $expectedMethod = self::METHOD_RELATIVE_SYMLINK;
  80. $io->text('Trying to install assets as <info>relative symbolic links</info>.');
  81. } elseif ($input->getOption('symlink')) {
  82. $expectedMethod = self::METHOD_ABSOLUTE_SYMLINK;
  83. $io->text('Trying to install assets as <info>absolute symbolic links</info>.');
  84. } else {
  85. $expectedMethod = self::METHOD_COPY;
  86. $io->text('Installing assets as <info>hard copies</info>.');
  87. }
  88. $io->newLine();
  89. $rows = array();
  90. $copyUsed = false;
  91. $exitCode = 0;
  92. $validAssetDirs = array();
  93. /** @var BundleInterface $bundle */
  94. foreach ($this->getContainer()->get('kernel')->getBundles() as $bundle) {
  95. if (!is_dir($originDir = $bundle->getPath().'/Resources/public')) {
  96. continue;
  97. }
  98. $assetDir = preg_replace('/bundle$/', '', strtolower($bundle->getName()));
  99. $targetDir = $bundlesDir.$assetDir;
  100. $validAssetDirs[] = $assetDir;
  101. if (OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) {
  102. $message = sprintf("%s\n-> %s", $bundle->getName(), $targetDir);
  103. } else {
  104. $message = $bundle->getName();
  105. }
  106. try {
  107. $this->filesystem->remove($targetDir);
  108. if (self::METHOD_RELATIVE_SYMLINK === $expectedMethod) {
  109. $method = $this->relativeSymlinkWithFallback($originDir, $targetDir);
  110. } elseif (self::METHOD_ABSOLUTE_SYMLINK === $expectedMethod) {
  111. $method = $this->absoluteSymlinkWithFallback($originDir, $targetDir);
  112. } else {
  113. $method = $this->hardCopy($originDir, $targetDir);
  114. }
  115. if (self::METHOD_COPY === $method) {
  116. $copyUsed = true;
  117. }
  118. if ($method === $expectedMethod) {
  119. $rows[] = array(sprintf('<fg=green;options=bold>%s</>', '\\' === \DIRECTORY_SEPARATOR ? 'OK' : "\xE2\x9C\x94" /* HEAVY CHECK MARK (U+2714) */), $message, $method);
  120. } else {
  121. $rows[] = array(sprintf('<fg=yellow;options=bold>%s</>', '\\' === \DIRECTORY_SEPARATOR ? 'WARNING' : '!'), $message, $method);
  122. }
  123. } catch (\Exception $e) {
  124. $exitCode = 1;
  125. $rows[] = array(sprintf('<fg=red;options=bold>%s</>', '\\' === \DIRECTORY_SEPARATOR ? 'ERROR' : "\xE2\x9C\x98" /* HEAVY BALLOT X (U+2718) */), $message, $e->getMessage());
  126. }
  127. }
  128. // remove the assets of the bundles that no longer exist
  129. $dirsToRemove = Finder::create()->depth(0)->directories()->exclude($validAssetDirs)->in($bundlesDir);
  130. $this->filesystem->remove($dirsToRemove);
  131. $io->table(array('', 'Bundle', 'Method / Error'), $rows);
  132. if (0 !== $exitCode) {
  133. $io->error('Some errors occurred while installing assets.');
  134. } else {
  135. if ($copyUsed) {
  136. $io->note('Some assets were installed via copy. If you make changes to these assets you have to run this command again.');
  137. }
  138. $io->success('All assets were successfully installed.');
  139. }
  140. return $exitCode;
  141. }
  142. /**
  143. * Try to create relative symlink.
  144. *
  145. * Falling back to absolute symlink and finally hard copy.
  146. *
  147. * @param string $originDir
  148. * @param string $targetDir
  149. *
  150. * @return string
  151. */
  152. private function relativeSymlinkWithFallback($originDir, $targetDir)
  153. {
  154. try {
  155. $this->symlink($originDir, $targetDir, true);
  156. $method = self::METHOD_RELATIVE_SYMLINK;
  157. } catch (IOException $e) {
  158. $method = $this->absoluteSymlinkWithFallback($originDir, $targetDir);
  159. }
  160. return $method;
  161. }
  162. /**
  163. * Try to create absolute symlink.
  164. *
  165. * Falling back to hard copy.
  166. *
  167. * @param string $originDir
  168. * @param string $targetDir
  169. *
  170. * @return string
  171. */
  172. private function absoluteSymlinkWithFallback($originDir, $targetDir)
  173. {
  174. try {
  175. $this->symlink($originDir, $targetDir);
  176. $method = self::METHOD_ABSOLUTE_SYMLINK;
  177. } catch (IOException $e) {
  178. // fall back to copy
  179. $method = $this->hardCopy($originDir, $targetDir);
  180. }
  181. return $method;
  182. }
  183. /**
  184. * Creates symbolic link.
  185. *
  186. * @param string $originDir
  187. * @param string $targetDir
  188. * @param bool $relative
  189. *
  190. * @throws IOException if link can not be created
  191. */
  192. private function symlink($originDir, $targetDir, $relative = false)
  193. {
  194. if ($relative) {
  195. $originDir = $this->filesystem->makePathRelative($originDir, realpath(\dirname($targetDir)));
  196. }
  197. $this->filesystem->symlink($originDir, $targetDir);
  198. if (!file_exists($targetDir)) {
  199. throw new IOException(sprintf('Symbolic link "%s" was created but appears to be broken.', $targetDir), 0, null, $targetDir);
  200. }
  201. }
  202. /**
  203. * Copies origin to target.
  204. *
  205. * @param string $originDir
  206. * @param string $targetDir
  207. *
  208. * @return string
  209. */
  210. private function hardCopy($originDir, $targetDir)
  211. {
  212. $this->filesystem->mkdir($targetDir, 0777);
  213. // We use a custom iterator to ignore VCS files
  214. $this->filesystem->mirror($originDir, $targetDir, Finder::create()->ignoreDotFiles(false)->in($originDir));
  215. return self::METHOD_COPY;
  216. }
  217. }