DumpCommand.php 8.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. <?php
  2. /*
  3. * This file is part of the Symfony framework.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * This source file is subject to the MIT license that is bundled
  8. * with this source code in the file LICENSE.
  9. */
  10. namespace Symfony\Bundle\AsseticBundle\Command;
  11. use Assetic\Util\PathUtils;
  12. use Assetic\AssetWriter;
  13. use Assetic\Asset\AssetInterface;
  14. use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
  15. use Symfony\Component\Console\Input\InputArgument;
  16. use Symfony\Component\Console\Input\InputInterface;
  17. use Symfony\Component\Console\Input\InputOption;
  18. use Symfony\Component\Console\Output\OutputInterface;
  19. /**
  20. * Dumps assets to the filesystem.
  21. *
  22. * @author Kris Wallsmith <kris@symfony.com>
  23. */
  24. class DumpCommand extends ContainerAwareCommand
  25. {
  26. private $basePath;
  27. private $verbose;
  28. private $am;
  29. protected function configure()
  30. {
  31. $this
  32. ->setName('assetic:dump')
  33. ->setDescription('Dumps all assets to the filesystem')
  34. ->addArgument('write_to', InputArgument::OPTIONAL, 'Override the configured asset root')
  35. ->addOption('watch', null, InputOption::VALUE_NONE, 'Check for changes every second, debug mode only')
  36. ->addOption('force', null, InputOption::VALUE_NONE, 'Force an initial generation of all assets (used with --watch)')
  37. ->addOption('period', null, InputOption::VALUE_REQUIRED, 'Set the polling period in seconds (used with --watch)', 1)
  38. ;
  39. }
  40. protected function initialize(InputInterface $input, OutputInterface $output)
  41. {
  42. parent::initialize($input, $output);
  43. $this->basePath = $input->getArgument('write_to') ?: $this->getContainer()->getParameter('assetic.write_to');
  44. $this->verbose = $input->getOption('verbose');
  45. $this->am = $this->getContainer()->get('assetic.asset_manager');
  46. }
  47. protected function execute(InputInterface $input, OutputInterface $output)
  48. {
  49. $output->writeln(sprintf('Dumping all <comment>%s</comment> assets.', $input->getOption('env')));
  50. $output->writeln(sprintf('Debug mode is <comment>%s</comment>.', $this->am->isDebug() ? 'on' : 'off'));
  51. $output->writeln('');
  52. if (!$input->getOption('watch')) {
  53. foreach ($this->am->getNames() as $name) {
  54. $this->dumpAsset($name, $output);
  55. }
  56. return;
  57. }
  58. if (!$this->am->isDebug()) {
  59. throw new \RuntimeException('The --watch option is only available in debug mode.');
  60. }
  61. $this->watch($input, $output);
  62. }
  63. /**
  64. * Watches a asset manager for changes.
  65. *
  66. * This method includes an infinite loop the continuously polls the asset
  67. * manager for changes.
  68. *
  69. * @param InputInterface $input The command input
  70. * @param OutputInterface $output The command output
  71. */
  72. private function watch(InputInterface $input, OutputInterface $output)
  73. {
  74. $refl = new \ReflectionClass('Assetic\\AssetManager');
  75. $prop = $refl->getProperty('assets');
  76. $prop->setAccessible(true);
  77. $cache = sys_get_temp_dir().'/assetic_watch_'.substr(sha1($this->basePath), 0, 7);
  78. if ($input->getOption('force') || !file_exists($cache)) {
  79. $previously = array();
  80. } else {
  81. $previously = unserialize(file_get_contents($cache));
  82. if (!is_array($previously)) {
  83. $previously = array();
  84. }
  85. }
  86. $error = '';
  87. while (true) {
  88. try {
  89. foreach ($this->am->getNames() as $name) {
  90. if ($this->checkAsset($name, $previously)) {
  91. $this->dumpAsset($name, $output);
  92. }
  93. }
  94. // reset the asset manager
  95. $prop->setValue($this->am, array());
  96. $this->am->load();
  97. file_put_contents($cache, serialize($previously));
  98. $error = '';
  99. } catch (\Exception $e) {
  100. if ($error != $msg = $e->getMessage()) {
  101. $output->writeln('<error>[error]</error> '.$msg);
  102. $error = $msg;
  103. }
  104. }
  105. sleep($input->getOption('period'));
  106. }
  107. }
  108. /**
  109. * Checks if an asset should be dumped.
  110. *
  111. * @param string $name The asset name
  112. * @param array &$previously An array of previous visits
  113. *
  114. * @return Boolean Whether the asset should be dumped
  115. */
  116. private function checkAsset($name, array &$previously)
  117. {
  118. $formula = $this->am->hasFormula($name) ? serialize($this->am->getFormula($name)) : null;
  119. $asset = $this->am->get($name);
  120. $values = $this->getContainer()->getParameter('assetic.variables');
  121. $values = array_intersect_key($values, array_flip($asset->getVars()));
  122. if (empty($values)) {
  123. $mtime = $asset->getLastModified();
  124. } else {
  125. $writer = new AssetWriter(sys_get_temp_dir(), $this->getContainer()->getParameter('assetic.variables'));
  126. $ref = new \ReflectionMethod($writer, 'getCombinations');
  127. $ref->setAccessible(true);
  128. $combinations = $ref->invoke($writer, $asset->getVars());
  129. $mtime = 0;
  130. foreach ($combinations as $combination) {
  131. $asset->setValues($combination);
  132. $assetMtime = $asset->getLastModified();
  133. if ($assetMtime > $mtime) {
  134. $mtime = $assetMtime;
  135. }
  136. }
  137. }
  138. if (isset($previously[$name])) {
  139. $changed = $previously[$name]['mtime'] != $mtime || $previously[$name]['formula'] != $formula;
  140. } else {
  141. $changed = true;
  142. }
  143. $previously[$name] = array('mtime' => $mtime, 'formula' => $formula);
  144. return $changed;
  145. }
  146. /**
  147. * Writes an asset.
  148. *
  149. * If the application or asset is in debug mode, each leaf asset will be
  150. * dumped as well.
  151. *
  152. * @param string $name An asset name
  153. * @param OutputInterface $output The command output
  154. */
  155. private function dumpAsset($name, OutputInterface $output)
  156. {
  157. $asset = $this->am->get($name);
  158. $formula = $this->am->getFormula($name);
  159. // start by dumping the main asset
  160. $this->doDump($asset, $output);
  161. // dump each leaf if debug
  162. if (isset($formula[2]['debug']) ? $formula[2]['debug'] : $this->am->isDebug()) {
  163. foreach ($asset as $leaf) {
  164. $this->doDump($leaf, $output);
  165. }
  166. }
  167. }
  168. /**
  169. * Performs the asset dump.
  170. *
  171. * @param AssetInterface $asset An asset
  172. * @param OutputInterface $output The command output
  173. *
  174. * @throws RuntimeException If there is a problem writing the asset
  175. */
  176. private function doDump(AssetInterface $asset, OutputInterface $output)
  177. {
  178. $writer = new AssetWriter(sys_get_temp_dir(), $this->getContainer()->getParameter('assetic.variables'));
  179. $ref = new \ReflectionMethod($writer, 'getCombinations');
  180. $ref->setAccessible(true);
  181. $combinations = $ref->invoke($writer, $asset->getVars());
  182. foreach ($combinations as $combination) {
  183. $asset->setValues($combination);
  184. $target = rtrim($this->basePath, '/').'/'.str_replace('_controller/', '',
  185. PathUtils::resolvePath($asset->getTargetPath(), $asset->getVars(),
  186. $asset->getValues()));
  187. if (!is_dir($dir = dirname($target))) {
  188. $output->writeln(sprintf(
  189. '<comment>%s</comment> <info>[dir+]</info> %s',
  190. date('H:i:s'),
  191. $dir
  192. ));
  193. if (false === @mkdir($dir, 0777, true)) {
  194. throw new \RuntimeException('Unable to create directory '.$dir);
  195. }
  196. }
  197. $output->writeln(sprintf(
  198. '<comment>%s</comment> <info>[file+]</info> %s',
  199. date('H:i:s'),
  200. $target
  201. ));
  202. if ($this->verbose) {
  203. if ($asset instanceof \Traversable) {
  204. foreach ($asset as $leaf) {
  205. $root = $leaf->getSourceRoot();
  206. $path = $leaf->getSourcePath();
  207. $output->writeln(sprintf(' <comment>%s/%s</comment>', $root ?: '[unknown root]', $path ?: '[unknown path]'));
  208. }
  209. } else {
  210. $root = $asset->getSourceRoot();
  211. $path = $asset->getSourcePath();
  212. $output->writeln(sprintf(' <comment>%s/%s</comment>', $root ?: '[unknown root]', $path ?: '[unknown path]'));
  213. }
  214. }
  215. if (false === @file_put_contents($target, $asset->dump())) {
  216. throw new \RuntimeException('Unable to write file '.$target);
  217. }
  218. }
  219. }
  220. }