CacheClearCommand.php 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  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\Command\Command;
  12. use Symfony\Component\Console\Exception\RuntimeException;
  13. use Symfony\Component\Console\Input\InputInterface;
  14. use Symfony\Component\Console\Input\InputOption;
  15. use Symfony\Component\Console\Output\OutputInterface;
  16. use Symfony\Component\Console\Style\SymfonyStyle;
  17. use Symfony\Component\DependencyInjection\Dumper\Preloader;
  18. use Symfony\Component\EventDispatcher\EventDispatcher;
  19. use Symfony\Component\Filesystem\Exception\IOException;
  20. use Symfony\Component\Filesystem\Filesystem;
  21. use Symfony\Component\Finder\Finder;
  22. use Symfony\Component\HttpKernel\CacheClearer\CacheClearerInterface;
  23. use Symfony\Component\HttpKernel\RebootableInterface;
  24. /**
  25. * Clear and Warmup the cache.
  26. *
  27. * @author Francis Besset <francis.besset@gmail.com>
  28. * @author Fabien Potencier <fabien@symfony.com>
  29. *
  30. * @final
  31. */
  32. class CacheClearCommand extends Command
  33. {
  34. protected static $defaultName = 'cache:clear';
  35. private $cacheClearer;
  36. private $filesystem;
  37. public function __construct(CacheClearerInterface $cacheClearer, Filesystem $filesystem = null)
  38. {
  39. parent::__construct();
  40. $this->cacheClearer = $cacheClearer;
  41. $this->filesystem = $filesystem ?: new Filesystem();
  42. }
  43. /**
  44. * {@inheritdoc}
  45. */
  46. protected function configure()
  47. {
  48. $this
  49. ->setDefinition([
  50. new InputOption('no-warmup', '', InputOption::VALUE_NONE, 'Do not warm up the cache'),
  51. new InputOption('no-optional-warmers', '', InputOption::VALUE_NONE, 'Skip optional cache warmers (faster)'),
  52. ])
  53. ->setDescription('Clear the cache')
  54. ->setHelp(<<<'EOF'
  55. The <info>%command.name%</info> command clears the application cache for a given environment
  56. and debug mode:
  57. <info>php %command.full_name% --env=dev</info>
  58. <info>php %command.full_name% --env=prod --no-debug</info>
  59. EOF
  60. )
  61. ;
  62. }
  63. /**
  64. * {@inheritdoc}
  65. */
  66. protected function execute(InputInterface $input, OutputInterface $output): int
  67. {
  68. $fs = $this->filesystem;
  69. $io = new SymfonyStyle($input, $output);
  70. $kernel = $this->getApplication()->getKernel();
  71. $realCacheDir = $kernel->getContainer()->getParameter('kernel.cache_dir');
  72. $realBuildDir = $kernel->getContainer()->hasParameter('kernel.build_dir') ? $kernel->getContainer()->getParameter('kernel.build_dir') : $realCacheDir;
  73. // the old cache dir name must not be longer than the real one to avoid exceeding
  74. // the maximum length of a directory or file path within it (esp. Windows MAX_PATH)
  75. $oldCacheDir = substr($realCacheDir, 0, -1).('~' === substr($realCacheDir, -1) ? '+' : '~');
  76. $fs->remove($oldCacheDir);
  77. if (!is_writable($realCacheDir)) {
  78. throw new RuntimeException(sprintf('Unable to write in the "%s" directory.', $realCacheDir));
  79. }
  80. $useBuildDir = $realBuildDir !== $realCacheDir;
  81. $oldBuildDir = substr($realBuildDir, 0, -1).('~' === substr($realBuildDir, -1) ? '+' : '~');
  82. if ($useBuildDir) {
  83. $fs->remove($oldBuildDir);
  84. if (!is_writable($realBuildDir)) {
  85. throw new RuntimeException(sprintf('Unable to write in the "%s" directory.', $realBuildDir));
  86. }
  87. if ($this->isNfs($realCacheDir)) {
  88. $fs->remove($realCacheDir);
  89. } else {
  90. $fs->rename($realCacheDir, $oldCacheDir);
  91. }
  92. $fs->mkdir($realCacheDir);
  93. }
  94. $io->comment(sprintf('Clearing the cache for the <info>%s</info> environment with debug <info>%s</info>', $kernel->getEnvironment(), var_export($kernel->isDebug(), true)));
  95. if ($useBuildDir) {
  96. $this->cacheClearer->clear($realBuildDir);
  97. }
  98. $this->cacheClearer->clear($realCacheDir);
  99. // The current event dispatcher is stale, let's not use it anymore
  100. $this->getApplication()->setDispatcher(new EventDispatcher());
  101. $containerFile = (new \ReflectionObject($kernel->getContainer()))->getFileName();
  102. $containerDir = basename(\dirname($containerFile));
  103. // the warmup cache dir name must have the same length as the real one
  104. // to avoid the many problems in serialized resources files
  105. $warmupDir = substr($realBuildDir, 0, -1).('_' === substr($realBuildDir, -1) ? '-' : '_');
  106. if ($output->isVerbose() && $fs->exists($warmupDir)) {
  107. $io->comment('Clearing outdated warmup directory...');
  108. }
  109. $fs->remove($warmupDir);
  110. if ($_SERVER['REQUEST_TIME'] <= filemtime($containerFile) && filemtime($containerFile) <= time()) {
  111. if ($output->isVerbose()) {
  112. $io->comment('Cache is fresh.');
  113. }
  114. if (!$input->getOption('no-warmup') && !$input->getOption('no-optional-warmers')) {
  115. if ($output->isVerbose()) {
  116. $io->comment('Warming up optional cache...');
  117. }
  118. $warmer = $kernel->getContainer()->get('cache_warmer');
  119. // non optional warmers already ran during container compilation
  120. $warmer->enableOnlyOptionalWarmers();
  121. $preload = (array) $warmer->warmUp($realCacheDir);
  122. if ($preload && file_exists($preloadFile = $realCacheDir.'/'.$kernel->getContainer()->getParameter('kernel.container_class').'.preload.php')) {
  123. Preloader::append($preloadFile, $preload);
  124. }
  125. }
  126. } else {
  127. $fs->mkdir($warmupDir);
  128. if (!$input->getOption('no-warmup')) {
  129. if ($output->isVerbose()) {
  130. $io->comment('Warming up cache...');
  131. }
  132. $this->warmup($warmupDir, $realCacheDir, !$input->getOption('no-optional-warmers'));
  133. }
  134. if (!$fs->exists($warmupDir.'/'.$containerDir)) {
  135. $fs->rename($realBuildDir.'/'.$containerDir, $warmupDir.'/'.$containerDir);
  136. touch($warmupDir.'/'.$containerDir.'.legacy');
  137. }
  138. if ($this->isNfs($realBuildDir)) {
  139. $io->note('For better performances, you should move the cache and log directories to a non-shared folder of the VM.');
  140. $fs->remove($realBuildDir);
  141. } else {
  142. $fs->rename($realBuildDir, $oldBuildDir);
  143. }
  144. $fs->rename($warmupDir, $realBuildDir);
  145. if ($output->isVerbose()) {
  146. $io->comment('Removing old build and cache directory...');
  147. }
  148. if ($useBuildDir) {
  149. try {
  150. $fs->remove($oldBuildDir);
  151. } catch (IOException $e) {
  152. if ($output->isVerbose()) {
  153. $io->warning($e->getMessage());
  154. }
  155. }
  156. }
  157. try {
  158. $fs->remove($oldCacheDir);
  159. } catch (IOException $e) {
  160. if ($output->isVerbose()) {
  161. $io->warning($e->getMessage());
  162. }
  163. }
  164. }
  165. if ($output->isVerbose()) {
  166. $io->comment('Finished');
  167. }
  168. $io->success(sprintf('Cache for the "%s" environment (debug=%s) was successfully cleared.', $kernel->getEnvironment(), var_export($kernel->isDebug(), true)));
  169. return 0;
  170. }
  171. private function isNfs(string $dir): bool
  172. {
  173. static $mounts = null;
  174. if (null === $mounts) {
  175. $mounts = [];
  176. if ('/' === \DIRECTORY_SEPARATOR && $files = @file('/proc/mounts')) {
  177. foreach ($files as $mount) {
  178. $mount = \array_slice(explode(' ', $mount), 1, -3);
  179. if (!\in_array(array_pop($mount), ['vboxsf', 'nfs'])) {
  180. continue;
  181. }
  182. $mounts[] = implode(' ', $mount).'/';
  183. }
  184. }
  185. }
  186. foreach ($mounts as $mount) {
  187. if (0 === strpos($dir, $mount)) {
  188. return true;
  189. }
  190. }
  191. return false;
  192. }
  193. private function warmup(string $warmupDir, string $realBuildDir, bool $enableOptionalWarmers = true)
  194. {
  195. // create a temporary kernel
  196. $kernel = $this->getApplication()->getKernel();
  197. if (!$kernel instanceof RebootableInterface) {
  198. throw new \LogicException('Calling "cache:clear" with a kernel that does not implement "Symfony\Component\HttpKernel\RebootableInterface" is not supported.');
  199. }
  200. $kernel->reboot($warmupDir);
  201. // warmup temporary dir
  202. if ($enableOptionalWarmers) {
  203. $warmer = $kernel->getContainer()->get('cache_warmer');
  204. // non optional warmers already ran during container compilation
  205. $warmer->enableOnlyOptionalWarmers();
  206. $preload = (array) $warmer->warmUp($warmupDir);
  207. if ($preload && file_exists($preloadFile = $warmupDir.'/'.$kernel->getContainer()->getParameter('kernel.container_class').'.preload.php')) {
  208. Preloader::append($preloadFile, $preload);
  209. }
  210. }
  211. // fix references to cached files with the real cache directory name
  212. $search = [$warmupDir, str_replace('\\', '\\\\', $warmupDir)];
  213. $replace = str_replace('\\', '/', $realBuildDir);
  214. foreach (Finder::create()->files()->in($warmupDir) as $file) {
  215. $content = str_replace($search, $replace, file_get_contents($file), $count);
  216. if ($count) {
  217. file_put_contents($file, $content);
  218. }
  219. }
  220. }
  221. }