├── Readme.md ├── composer.json └── src ├── ClassmapReader ├── ChainReader.php ├── DirectoryReader.php ├── OptimizedReader.php └── ReaderInterface.php ├── Compiler ├── CliCompiler.php ├── CompilerInterface.php ├── FallbackCompiler.php └── PhpServerCompiler.php ├── Console └── WarmupCommand.php ├── Plugin.php └── Resource └── server.php /Readme.md: -------------------------------------------------------------------------------- 1 | # OpCode Warmer (composer plugin) 2 | 3 | Optimize your application by warming up OpCode. 4 | 5 | ## Requirements 6 | 7 | - PHP `>=7.0` 8 | - Zend extension [Opcache](http://php.net/manual/en/book.opcache.php) 9 | - extension [Sockets](http://php.net/manual/en/book.sockets.php) 10 | - composer `>=1.0.0` 11 | 12 | ## Install 13 | 14 | ```bash 15 | $ composer global require "jderusse/composer-warmup" 16 | ``` 17 | 18 | ## Configure 19 | 20 | ```ini 21 | ; /etc/php/7.0/cli/conf.d/10-opcache.ini 22 | zend_extension=opcache.so 23 | opcache.enable_cli=1 24 | opcache.file_cache='/tmp/opcache' 25 | 26 | ; recommended 27 | opcache.file_update_protection=0 28 | ``` 29 | 30 | ## Usage 31 | 32 | ```bash 33 | $ cd my-project 34 | $ composer warmup-opcode 35 | ``` 36 | 37 | ## How does it work? 38 | 39 | Since PHP 7.0, the OpCache extension is able to store the compiled OpCode into 40 | files. 41 | 42 | This plugin adds the `warmup-opcode` command to 43 | [composer](https://getcomposer.org/) which triggers the compilation for every 44 | PHP file discovered in the project. 45 | 46 | When you start the application for the first time, PHP doesn't need to compile 47 | the files, which improve performance. 48 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jderusse/composer-warmup", 3 | "type": "composer-plugin", 4 | "require": { 5 | "php": "^7.0 || ^8.0", 6 | "composer-plugin-api": "^1.0 || ^2.0", 7 | "ext-Zend-OPcache": "*", 8 | "ext-sockets": "*" 9 | }, 10 | "extra": { 11 | "class": "Jderusse\\Warmup\\Plugin" 12 | }, 13 | "autoload": { 14 | "psr-4": { 15 | "Jderusse\\Warmup\\": "src" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/ClassmapReader/ChainReader.php: -------------------------------------------------------------------------------- 1 | readers = $readers; 15 | } 16 | 17 | public function getClassmap() : \Traversable 18 | { 19 | /** @var ReaderInterface $reader */ 20 | foreach ($this->readers as $reader) { 21 | yield from $reader->getClassmap(); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/ClassmapReader/DirectoryReader.php: -------------------------------------------------------------------------------- 1 | directory = $directory; 22 | 23 | $this->filesystem = new Filesystem(); 24 | $this->basePath = $this->filesystem->normalizePath(realpath(getcwd())); 25 | } 26 | 27 | public function getClassmap() : \Traversable 28 | { 29 | foreach (ClassMapGenerator::createMap($this->directory) as $class => $path) { 30 | yield $class => $this->normalize($path); 31 | } 32 | } 33 | 34 | private function normalize(string $path) : string 35 | { 36 | if (!$this->filesystem->isAbsolutePath($path)) { 37 | $path = $this->basePath.'/'.$path; 38 | } 39 | 40 | return $this->filesystem->normalizePath($path); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/ClassmapReader/OptimizedReader.php: -------------------------------------------------------------------------------- 1 | config = $config; 18 | } 19 | 20 | public function getClassmap() : \Traversable 21 | { 22 | $filesystem = new Filesystem(); 23 | $vendorPath = $filesystem->normalizePath(realpath($this->config->get('vendor-dir'))); 24 | $classmapPath = $vendorPath.'/composer/autoload_classmap.php'; 25 | 26 | if (!is_file($classmapPath)) { 27 | throw new \RuntimeException( 28 | 'Th dumped classmap does not exists. Try to run `composer dump-autoload --optimize` first.' 29 | ); 30 | } 31 | 32 | yield from include $vendorPath.'/composer/autoload_classmap.php'; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/ClassmapReader/ReaderInterface.php: -------------------------------------------------------------------------------- 1 | compilers = $compilers; 15 | } 16 | 17 | public function compile(string $file) 18 | { 19 | foreach ($this->compilers as $compiler) { 20 | try { 21 | return $compiler->compile($file); 22 | } catch (\Throwable $e) { 23 | continue; 24 | } 25 | } 26 | 27 | throw new \RuntimeException("Failed to compile {$file}."); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Compiler/PhpServerCompiler.php: -------------------------------------------------------------------------------- 1 | findPort($portRange); 21 | $this->address = sprintf('127.0.0.1:%d', $port); 22 | 23 | $this->startServer(); 24 | } 25 | 26 | public function startServer() 27 | { 28 | $finder = new PhpExecutableFinder(); 29 | if (false === $binary = $finder->find()) { 30 | throw new \RuntimeException('Unable to find PHP binary to run server.'); 31 | } 32 | 33 | $builder = new ProcessBuilder( 34 | ['exec', $binary, '-S', $this->address, realpath(__DIR__.'/../Resource/server.php')] 35 | ); 36 | $builder->setWorkingDirectory(realpath(__DIR__.'/../Resource')); 37 | $builder->setTimeout(null); 38 | $this->process = $builder->getProcess(); 39 | $this->process->start(); 40 | 41 | $this->waitServer(); 42 | } 43 | 44 | public function waitServer(int $timeout = 10) 45 | { 46 | $start = time(); 47 | while (time() - $start <= $timeout) { 48 | usleep(10000); 49 | if (!$this->process->isRunning()) { 50 | continue; 51 | } 52 | try { 53 | file_get_contents(sprintf('http://%s/', $this->address)); 54 | 55 | return true; 56 | } catch (\Throwable $e) { 57 | } 58 | } 59 | 60 | throw new \RuntimeException('Server is not responding'); 61 | } 62 | 63 | public function __destruct() 64 | { 65 | $this->stopServer(); 66 | } 67 | 68 | public function stopServer() 69 | { 70 | if ($this->process and $this->process->isRunning()) { 71 | $this->process->stop(0); 72 | } 73 | } 74 | 75 | public function compile(string $file) 76 | { 77 | file_get_contents(sprintf('http://%s/?file=%s', $this->address, urlencode($file))); 78 | } 79 | 80 | private function findPort(array $portRange) 81 | { 82 | foreach (range(...$portRange) as $port) { 83 | if ($this->isPortAvailable($port)) { 84 | return $port; 85 | } 86 | } 87 | 88 | throw new \RuntimeException(sprintf('Unable to find a suitable port in the range %d..%d', ...$portRange)); 89 | } 90 | 91 | private function isPortAvailable(int $port) : bool 92 | { 93 | $h = null; 94 | try { 95 | $h = socket_create_listen($port); 96 | if ($h !== false) { 97 | return true; 98 | } 99 | } catch (\ErrorException $e) { 100 | // just ignore exception port already in use 101 | } finally { 102 | if (is_resource($h)) { 103 | socket_close($h); 104 | } 105 | } 106 | 107 | return false; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Console/WarmupCommand.php: -------------------------------------------------------------------------------- 1 | setName('warmup-opcode') 22 | ->setDescription('Warmup the application\'s OpCode') 23 | ->addArgument('extra', InputArgument::IS_ARRAY, 'add extra path to compile'); 24 | } 25 | 26 | protected function execute(InputInterface $input, OutputInterface $output) 27 | { 28 | if (!extension_loaded('Zend OPcache')) { 29 | throw new \RuntimeException('You have to enable opcache to use this commande'); 30 | } 31 | 32 | if (!(bool) ini_get('opcache.enable_cli')) { 33 | throw new \RuntimeException('You have to enable the opcache extension for usage in the CLI using: opcache.enable_cli'); 34 | } 35 | 36 | $opcacheDir = ini_get('opcache.file_cache'); 37 | if (empty($opcacheDir)) { 38 | throw new \RuntimeException('You have to define a file_cache to use in using: opcache.file_cache'); 39 | } 40 | 41 | if (!is_dir($opcacheDir)) { 42 | throw new \RuntimeException(sprintf('You have to create the "%s" directory', $opcacheDir)); 43 | } 44 | 45 | if ((int) ini_get('opcache.file_update_protection') > 0) { 46 | $output->writeln( 47 | 'You should set the `opcache.file_update_protection` to 0 in order to cache recently updated files' 48 | ); 49 | } 50 | 51 | $composer = $this->getComposer(); 52 | 53 | $reader = new ChainReader( 54 | array_merge( 55 | [new OptimizedReader($composer->getConfig())], 56 | array_map( 57 | function ($extra) { 58 | return new DirectoryReader($extra); 59 | }, 60 | $input->getArgument('extra') 61 | ) 62 | ) 63 | ); 64 | 65 | $compiler = new FallbackCompiler([ 66 | new CliCompiler(), 67 | new PhpServerCompiler(), 68 | ]); 69 | 70 | foreach ($reader->getClassmap() as $file) { 71 | try { 72 | $compiler->compile($file); 73 | if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) { 74 | $output->writeln(sprintf('Compiled file %s', $file)); 75 | } 76 | } catch (\Throwable $e) { 77 | $output->writeln(sprintf('Failed to compile file %s', $file)); 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Plugin.php: -------------------------------------------------------------------------------- 1 | static::class, 30 | ]; 31 | } 32 | 33 | public function getCommands() 34 | { 35 | return [new WarmupCommand()]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Resource/server.php: -------------------------------------------------------------------------------- 1 |