PreCompileCommand.php 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. <?php
  2. namespace ClassPreloader\Command;
  3. use ClassPreloader\Config;
  4. use ClassPreloader\Parser\DirVisitor;
  5. use ClassPreloader\Parser\NodeTraverser;
  6. use ClassPreloader\Parser\FileVisitor;
  7. use Symfony\Component\Filesystem\Filesystem;
  8. use Symfony\Component\Console\Input\InputInterface;
  9. use Symfony\Component\Console\Input\InputOption;
  10. use Symfony\Component\Console\Output\OutputInterface;
  11. use Symfony\Component\Console\Command\Command;
  12. class PreCompileCommand extends Command
  13. {
  14. protected $input;
  15. protected $output;
  16. protected $printer;
  17. protected $traverser;
  18. protected $parser;
  19. public function __construct()
  20. {
  21. parent::__construct();
  22. $this->printer = new \PHPParser_PrettyPrinter_Zend();
  23. $this->parser = new \PHPParser_Parser(new \PHPParser_Lexer());
  24. }
  25. /**
  26. * {@inheritdoc}
  27. */
  28. protected function configure()
  29. {
  30. parent::configure();
  31. $this->setName('compile')
  32. ->setDescription('Compiles classes into a single file')
  33. ->addOption('config', null, InputOption::VALUE_REQUIRED, 'CSV of filenames to load, or the path to a PHP script that returns an array of file names')
  34. ->addOption('output', null, InputOption::VALUE_REQUIRED)
  35. ->addOption('fix_dir', null, InputOption::VALUE_REQUIRED, 'Convert __DIR__ constants to the original directory of a file', 1)
  36. ->addOption('fix_file', null, InputOption::VALUE_REQUIRED, 'Convert __FILE__ constants to the original path of a file', 1)
  37. ->addOption('strip_comments', null, InputOption::VALUE_REQUIRED, 'Set to 1 to strip comments from each source file', 0)
  38. ->setHelp(<<<EOF
  39. The <info>%command.name%</info> command iterates over each script, normalizes
  40. the file to be wrapped in namespaces, and combines each file into a single PHP
  41. file.
  42. EOF
  43. );
  44. }
  45. /**
  46. * Get the node traverser used by the command
  47. *
  48. * @return NodeTraverser
  49. */
  50. protected function getTraverser()
  51. {
  52. if (!$this->traverser) {
  53. $this->traverser = new NodeTraverser();
  54. if ($this->input->getOption('fix_dir')) {
  55. $this->traverser->addVisitor(new DirVisitor($file));
  56. }
  57. if ($this->input->getOption('fix_file')) {
  58. $this->traverser->addVisitor(new FileVisitor($file));
  59. }
  60. }
  61. return $this->traverser;
  62. }
  63. /**
  64. * Get a pretty printed string of code from a file while applying visitors
  65. *
  66. * @param string $file Name of the file to get code from
  67. * @param NodeTraverser $traverser Node traverser
  68. *
  69. * @return string
  70. */
  71. protected function getCode($file)
  72. {
  73. if (!is_readable($file)) {
  74. throw new \RuntimeException("Cannot open {$file} for reading");
  75. }
  76. if ($this->input->getOption('strip_comments')) {
  77. $content = php_strip_whitespace($file);
  78. } else {
  79. $content = file_get_contents($file);
  80. }
  81. $stmts = $this->getTraverser()
  82. ->traverseFile($this->parser->parse($content), $file);
  83. $pretty = $this->printer->prettyPrint($stmts);
  84. // Remove the open PHP tag
  85. if (substr($pretty, 6) == "<?php\n") {
  86. $pretty = substr($pretty, 7);
  87. }
  88. // Add a wrapping namespace if needed
  89. if (false === strpos($pretty, 'namespace ')) {
  90. $pretty = "namespace {\n" . $pretty . "\n}\n";
  91. }
  92. return $pretty;
  93. }
  94. /**
  95. * Validate the command options
  96. */
  97. protected function validateCommand()
  98. {
  99. if (!$this->input->getOption('output')) {
  100. throw new \InvalidArgumentException('An output option is required');
  101. }
  102. if (!$this->input->getOption('config')) {
  103. throw new \InvalidArgumentException('A config option is required');
  104. }
  105. }
  106. /**
  107. * Get a list of files in order
  108. *
  109. * @param mixed $config Configuration option
  110. *
  111. * @return array
  112. */
  113. protected function getFileList($config)
  114. {
  115. $this->output->writeln('> Loading configuration file');
  116. $filesystem = new Filesystem();
  117. if (strpos($config, ',')) {
  118. return array_filter(explode(',', $config));
  119. }
  120. // Ensure absolute paths are resolved
  121. if (!$filesystem->isAbsolutePath($config)) {
  122. $config = getcwd() . '/' . $config;
  123. }
  124. // Ensure that the config file exists
  125. if (!file_exists($config)) {
  126. throw new \InvalidArgumentException(sprintf('Configuration file "%s" does not exist.', $config));
  127. }
  128. $result = require $config;
  129. if ($result instanceof Config) {
  130. foreach ($result->getVisitors() as $visitor) {
  131. $this->getTraverser()->addVisitor($visitor);
  132. }
  133. return $result;
  134. } elseif (is_array($result)) {
  135. return $result;
  136. }
  137. throw new \InvalidArgumentException(
  138. 'Config must return an array of filenames or a Config object'
  139. );
  140. }
  141. /**
  142. * Prepare the output file and directory
  143. *
  144. * @param string $outputFile The full path to the output file
  145. */
  146. protected function prepareOutput($outputFile)
  147. {
  148. $dir = dirname($outputFile);
  149. if (!is_dir($dir) && !mkdir($dir, 0777, true)) {
  150. throw new \RuntimeException('Unable to create directory ' . $dir);
  151. }
  152. }
  153. /**
  154. * {@inheritdoc}
  155. */
  156. protected function execute(InputInterface $input, OutputInterface $output)
  157. {
  158. $this->input = $input;
  159. $this->output = $output;
  160. $this->validateCommand();
  161. $outputFile = $this->input->getOption('output');
  162. $config = $this->input->getOption('config');
  163. $files = $this->getFileList($config);
  164. $output->writeLn('- Found ' . count($files) . ' files');
  165. // Make sure that the output dir can be used or create it
  166. $this->prepareOutput($outputFile);
  167. if (!$handle = fopen($input->getOption('output'), 'w')) {
  168. throw new \RuntimeException(
  169. "Unable to open {$outputFile} for writing"
  170. );
  171. }
  172. // Write the first line of the output
  173. fwrite($handle, "<?php\n");
  174. $output->writeln('> Compiling classes');
  175. foreach ($files as $file) {
  176. $this->output->writeln('- Writing ' . $file);
  177. fwrite($handle, $this->getCode($file) . "\n");
  178. }
  179. fclose($handle);
  180. $output->writeln("> Compiled loader written to {$outputFile}");
  181. $output->writeln('- ' . (round(filesize($outputFile) / 1024)) . ' kb');
  182. }
  183. }