├── 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 |