Упаковка Zend Framework 2 в PHAR архив

В этом посте я хочу рассказать об устранении одной из проблем ZF1 -огромного количество инклюдов (и как следствие файловых операций), а именно компиляция приложения в PHAR архив. Не скажу, что технология компиляции php кода в один файл новая. Раньше были попытки сделать это, но получалось прямо скажем не всегда. Приходилось в полуавтоматическом режиме вырезать require_once() из кода, а в некоторых местах ZF1 он был нужен, писались исключения. В общем этот подход лично я решил не применять, а ограничился установкой APC байткод-кэшера.
Однако с выходом php 5.3.0 появилась замечательная возможность использовать обертку потока phar://. Теперь можно повторить эксперимент на новом уровне.
Сразу оговорюсь, что можно точно также сжать и Zend Framework 1, есть всего-лишь одно небольшое отличие и я расскажу о нём чуть ниже. Для знакомства с теорией рекомендую почитать вот эту статью на Хабре. Для упаковки ZF2 в Phar нам понадобится несколько компонентов:
- Собственно сам ZF2. Скачать последнюю версию можно с офсайта или из Git репозитария.
- Файл-загрузчик для Phar-архива (stub.php) – заглушка. Получает управления сразу после инклюда файла с архивом. Будет ниже.
- Упаковщик package.php. Будет ниже.
Stub.php для Zend Framework 1.x
|
1 2 3 4 5 6 |
<?php require_once dirname(__FILE__).'/Zend/Loader/Autoloader.php'; Zend_Loader_Autoloader::getInstance(); Zend_Loader_Autoloader::getInstance()->setFallbackAutoloader(true); // Finalize stumb __HALT_COMPILER(); |
Stub.php для Zend Framework 2.x
|
1 2 3 4 5 6 7 8 |
<?php require_once dirname(__FILE__) . '/Zend/Loader/AutoloaderFactory.php'; Zend\Loader\AutoloaderFactory::factory(array('Zend\Loader\StandardAutoloader' => array())); $moduleLoader = new Zend\Loader\ModuleAutoloader(); $moduleLoader->register(); // Finalize stumb __HALT_COMPILER(); |
Package.php
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 |
<?php ini_set('phar.readonly', 0); /** * package.php * Create a Zend Framework phar * * @author Cal Evans <cal@calevans.com> * @author John Douglass <john .douglass@oit.gatech.edu> */ $getOptLongArray = array("stub:"); $getOptParams = "s:p:v"; $options = getOpt($getOptParams,$getOptLongArray); if(!isset($options['s'],$options['p'])) { echo "You did not specify either a path or a phar file name.\n"; displayHelp(); die(1); } /* * Set up our environment */ $sourceLocation = $options['s']; $basePointer = strpos($options['s'],'Zend'); $pharFile = $options['p']; /* * Make sure things are sane before progressing */ if ($basePointer<1) { echo "It looks like your path is not a Zend Framework path.\nPlease check and try again.\n"; displayhelp(); die(1); } // At this point, we need to check to see if the file exists. If neither exist, throw exception. if (isset($options['stub'])) { $stubFile = $options['stub']; } else { $stubFile = 'stub.php'; } if(!file_exists($sourceLocation)) { echo "ERROR: Source file location does not exist!\nCheck your source and try again.\n"; displayhelp(); die(1); } /* * Let the user know what is going on */ echo "Creating PHAR\n"; echo "Source : {$sourceLocation}\n"; echo "Destination : {$pharFile}\n"; echo "Stub File : {$stubFile}\n\n"; /* * Clean up from previous runs */ if (file_exists($pharFile)) { Phar::unlinkArchive($pharFile); } /* * Setup the phar */ $p = new Phar($pharFile, 0, $pharFile); $p->compressFiles(Phar::GZ); $p->setSignatureAlgorithm (Phar::SHA1); /* * Now build the array of files to be in the phar. * The first file is the stub file. The rest of the files are built from the directory. */ $files = array(); $files["stub.php"] = $stubFile; echo "Building the array of files to include.\n"; $rd = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($sourceLocation)); foreach($rd as $file) { if (strpos($file->getPath(),'.svn')===false && $file->getFilename() != '..' && $file->getFilename() != '.') { $fileIndex = substr($file->getPath().DIRECTORY_SEPARATOR.$file->getFilename(),$basePointer); $fileName = $file->getPath().DIRECTORY_SEPARATOR.$file->getFilename(); $files[$fileIndex] = $fileName; // Coined "phindex" to refer to the included index pointing to the real filename on disk we are creating if (isset($options['v'])) { echo " PHIndex[{$fileIndex}] = {$fileName}\n"; } // if (isset($options['v'])) } } // foreach($rd as $file) echo "Now building the phar.\n"; /* * Now build the archive. */ $p->startBuffering(); $p->buildFromIterator(new ArrayIterator($files)); $p->stopBuffering(); /* * finish up. */ $p->setStub($p->createDefaultStub("stub.php")); $p = null; if (isset($options['v'])) { echo count($files)." files Added to ".$pharFile."\n"; } // if (isset($options['v'])) echo "Done.\n"; exit; function displayHelp() { echo "\n\npachage.php\n"; echo " Authors: Cal Evans, John Douglass\n\n"; echo " -s The directory where Zend Framework is located. Must end in /Zend. \n"; echo " -p The name to give your phar file.\n"; echo " --stub The name of your stub file. Will default to stub.php if not passed in.\n"; echo " -v verbose mode.\n"; } |
Теперь надо правильно расположить файлы. В каталоге вашего проекта создайте папку ./vendor/ZendFramework/library Поместите туда эти файлы (stub.php, package.php) и папку Zend с самим фреймворком. Такое расположение папок характерно для ZF2. Если у вас в проекте другое, то придётся подправить пути. Дальше запускайте следующую команду для компиляции ZF в один файл.
|
1 2 3 4 5 6 7 8 9 10 |
andrey@z11:~/sandbox/zf2/vendor/ZendFramework/library$ php ./package.php -s ./Zend -p zf.phar -v Creating PHAR Source : ./vendor/ZendFramework/library/Zend Destination : zf.phar Stub File : stub.php PHP Fatal error: Uncaught exception 'UnexpectedValueException' with message 'creating archive "zf.phar" disabled by the php.ini setting phar.readonly' in /home/andrey/zf2.ru/package.php:70 Stack trace: #0 /home/andrey/zf2.ru/package.php(70): Phar->__construct('zf.phar', 0, 'zf.phar') #1 {main} thrown in /home/andrey/zf2.ru/package.php on line 70 |
Чтобы убрать эту ошибку надо разрешить запись в phar-архивы в php.ini:
|
1 2 |
[Phar] phar.readonly = 0 |
Теперь весь ZF упакован в один файл:
|
1 2 3 4 5 6 7 |
... PHIndex[ZendFramework/library/Zend/Mail/Transport/Exception.php] = ./vendor/ZendFramework/library/Zend/Mail/Transport/Exception.php PHIndex[ZendFramework/library/Zend/Mail/Transport/Sendmail.php] = ./vendor/ZendFramework/library/Zend/Mail/Transport/Sendmail.php PHIndex[ZendFramework/library/Zend/Mail/Storage.php] = ./vendor/ZendFramework/library/Zend/Mail/Storage.php Now building the phar. 2836 files Added to zf.phar Done. |
Отлично, теперь в каталоге ./vendor/ZendFramework/library/ у вас должен появиться файл zf.phar. А значит можно подключать скомпиленный ZF к проекту.
test_phar.php. Подключение для ZF2:
|
1 2 3 4 5 |
<?php include("phar://".dirname(__FILE__)."/vendor/ZendFramework/library/zf.phar"); $version = new Zend\Version; print "Compiled ZF version is: \r\n"; print $version::VERSION."\r\n"; |
test_phar.php Подключение для ZF1:
|
1 2 3 4 5 |
<?php include("phar://".dirname(__FILE__)."/vendor/ZendFramework/library/zf.phar"); $version = new Zend_Version; print "Compiled ZF version is: \r\n"; print $version::VERSION."\r\n"; |
Однако при запуске test_phar.php отображается пустой экран. При этом в логи PHP ничего не пишет. Прогуглив как следует этот вопрос, я выяснил, что это происходит из-за Suhosin patch. Для того, чтобы Phar архивы читались нормально, если в системе стоит suhosin patch, вам надо прописать в /etc/php5/conf.d/suhosin.ini (или в php.ini) следующее:
|
1 |
suhosin.executor.include.whitelist="phar" |
После этого скрипт должен вывести версию ZF, подключённого через phar архив.
|
1 2 3 |
andrey@z11:~/sandbox/zf2.ru# php ./test_phar.php Compiled ZF version is: 2.0.0beta1 |
Да, теперь весь ZF можно подключить с помощью одного include! Вы кстати можете модифицировать package.php для своего проекта и вообще залить всё приложение (со всеми библиотеками в один файл). А для хостингов вообще можно красиво сделать. Компилируем php-фреймворки или CMS’ки в phar, включаем APC, и один экземпляр фреймворка в памяти шарится для всех клиентов.
Кэширование APC и PHAR
Самое замечательное в этой ситуации, что когда фреймворк инклюдится по одному файлу (см. рис ниже), то часть из них всё равно вываливается из кэша, а тут один файл, который априори будет всегда лежать в памяти (т.к. будет инклюдится при каждом запросе). Вот что происходит при обычном выполнении запросов.

Теперь для чистоты экспериментов очистим кэш.
Сделаем include PHAR архива и посмотрим кэш.
А потом ещё раз include.
О чём это говорит? При include конкретного файла класса он сначала берется из кэша, и если его там нет, то из Phar архива, который тоже висит в памяти, т.е. также кэшируется. В итоге дисковых операций не происходит.
Бонусы
В этом посте будет пара бонусов, а именно скомпиленные в PHAR фреймворки Zend Framework 1 и Zend Framework 2.
zf_1_11_0_dev.phar.tar.gz (зеркало)
zf_2_0_0beta1.phar.tar.gz (зеркало)
Скачать всё с GitHub
Подключаются они обычным инклюдом:
|
1 2 |
<?php include("phar://".dirname(__FILE__)."/vendor/ZendFramework/library/zf.phar"); |
Не забудьте, что при перекомпилировании фреймворка при работающем APC в памяти остаётся висеть старый архив, и вам надо перезапустить php5-fpm или Apache, чтобы сбросить кэш.
Ссылки
- http://www.php.net/manual/en/phar.using.intro.php
- http://blog.calevans.com/2009/07/19/lessons-in-phar/
- http://habrahabr.ru/blogs/php/118269/
UPD
Обязательно нужно убрать саму папку library/Zend из проекта, иначе идет передекларирование классов, т.е. ты все классы подключил через phar, а Loader делает include_once например, и он инклудит физический файл, но класс уже был объявлен в phar и выходит ошибка. Или же проверять загружен ли класс или нет перед include_once.


