В этой статье я хочу поближе познакомить вас с замечательной фичей — Phar-архивами. В предыдущем посте я упоминал о ней, а много раньше даже писал обзорную статью по Phar. Целью этого поста будет полная упаковка Zend Framework 2 в один архив, чтобы раз и навсегда исключить проблему инклюдов в ZF веб-приложениях.В последнее время я слышу слишком много флуда, что «PHP уже не торт, вот в Ruby, там да…». Или «на PHP кодят только школьники», «Enterprise приложение на PHP не сделаешь», «ZF — тормозной фреймворк» ну и т.д.  А ведь проблемы то растут не из технологий. Хотя PHP имеет груз обратной совместимости, и ZF1 объективно из коробки показывает не высокую производительность. Однако, при использовании драйвера «прямые руки» можно спокойно и без нервов переписать тормозящий участок кода, работать через ORM, оптимизировать код и т.д. Но многим людям проще обвинить PHP, чем заняться делом. Это печально 

В этом посте я хочу рассказать об устранении одной из проблем 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.require_once dirname(__FILE__).'/Zend/Loader/Autoloader.php';3.Zend_Loader_Autoloader::getInstance();4.Zend_Loader_Autoloader::getInstance()->setFallbackAutoloader(true);5.// Finalize stumb6.__HALT_COMPILER();
Stub.php для Zend Framework 2.x
1.2.require_once dirname(__FILE__) . '/Zend/Loader/AutoloaderFactory.php';3.Zend\Loader\AutoloaderFactory::factory(array('Zend\Loader\StandardAutoloader' =>array()));4.$moduleLoader = new Zend\Loader\ModuleAutoloader();5.$moduleLoader->register();6. 7.// Finalize stumb8.__HALT_COMPILER();
Package.php
001.002.ini_set('phar.readonly', 0);003./**004.* package.php005.* Create a Zend Framework phar006.*007.* @author Cal Evans 008.* @author John Douglass 009.*/010. 011.$getOptLongArray = array("stub:");012.$getOptParams    = "s:p:v";013.$options         = getOpt($getOptParams,$getOptLongArray);014. 015.if(!isset($options['s'],$options['p']))016.{017.echo "You did not specify either a path or a phar file name.\n";018.displayHelp();019.die(1);020.}021. 022./*023.* Set up our environment024.*/025.$sourceLocation = $options['s'];026.$basePointer    = strpos($options['s'],'Zend');027.$pharFile       = $options['p'];028. 029./*030.* Make sure things are sane before progressing031.*/032.if ($basePointer<1 code="">1>033.echo "It looks like your path is not a Zend Framework path.\nPlease check and try again.\n";034.displayhelp();035.die(1);036.}037. 038.// At this point, we need to check to see if the file exists. If neither exist, throw exception.039.if (isset($options['stub'])) {040.$stubFile = $options['stub'];041.} else {042.$stubFile = 'stub.php';043.}044. 045.if(!file_exists($sourceLocation))046.{047.echo "ERROR: Source file location does not exist!\nCheck your source and try again.\n";048.displayhelp();049.die(1);050.}051. 052./*053.* Let the user know what is going on054.*/055.echo "Creating PHAR\n";056.echo "Source      : {$sourceLocation}\n";057.echo "Destination : {$pharFile}\n";058.echo "Stub File   : {$stubFile}\n\n";059. 060./*061.* Clean up from previous runs062.*/063.if (file_exists($pharFile)) {064.Phar::unlinkArchive($pharFile);065.}066. 067./*068.* Setup the phar069.*/070.$p = new Phar($pharFile, 0, $pharFile);071.$p->compressFiles(Phar::GZ);072.$p->setSignatureAlgorithm (Phar::SHA1);073. 074./*075.* Now build the array of files to be in the phar.076.* The first file is the stub file. The rest of the files are built from the directory.077.*/078.$files = array();079.$files["stub.php"] = $stubFile;080. 081.echo "Building the array of files to include.\n";082. 083.$rd = new RecursiveIteratorIterator(newRecursiveDirectoryIterator($sourceLocation));084.foreach($rd as $file) {085.if (strpos($file->getPath(),'.svn')===false &&086.$file->getFilename() != '..' &&087.$file->getFilename() != '.')088.{089.$fileIndex = substr($file->getPath().DIRECTORY_SEPARATOR.$file->getFilename(),$basePointer);090.$fileName = $file->getPath().DIRECTORY_SEPARATOR.$file->getFilename();091.$files[$fileIndex] = $fileName;092.// Coined "phindex" to refer to the included index pointing to the real filename on disk we are creating093.if (isset($options['v'])) {094.echo "   PHIndex[{$fileIndex}] = {$fileName}\n";           095.} // if (isset($options['v']))096.}097.} // foreach($rd as $file)098. 099.echo "Now building the phar.\n";100. 101./*102.* Now build the archive.103.*/104.$p->startBuffering();105.$p->buildFromIterator(new ArrayIterator($files));106.$p->stopBuffering();107. 108./*109.* finish up.110.*/111.$p->setStub($p->createDefaultStub("stub.php"));112.$p = null;113. 114.if (isset($options['v'])) {115.echo count($files)." files Added to ".$pharFile."\n";116.} // if (isset($options['v']))117. 118.echo "Done.\n";119.exit;120. 121.function displayHelp()122.{123.echo "\n\npachage.php\n";124.echo "  Authors: Cal Evans, John Douglass\n\n";125.echo "  -s The directory where Zend Framework is located. Must end in /Zend. \n";126.echo "  -p The name to give your phar file.\n";127.echo "  --stub The name of your stub file. Will default to stub.php if not passed in.\n";128.echo "  -v verbose mode.\n";129.}
Теперь надо правильно расположить файлы. В каталоге вашего проекта создайте папку ./vendor/ZendFramework/library Поместите туда эти файлы (stub.php, package.php) и папку Zend с самим фреймворком. Такое расположение папок характерно для ZF2. Если у вас в проекте другое, то придётся подправить пути. Дальше запускайте следующую команду для компиляции ZF в один файл.
01.andrey@z11:~/sandbox/zf2/vendor/ZendFramework/library$ php ./package.php -s ./Zend -p zf.phar -v02.Creating PHAR03.Source      : ./vendor/ZendFramework/library/Zend04.Destination : zf.phar05.Stub File   : stub.php06.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:7007.Stack trace:08.#0 /home/andrey/zf2.ru/package.php(70): Phar->__construct('zf.phar', 0, 'zf.phar')09.#1 {main}10.thrown in /home/andrey/zf2.ru/package.php on line 70
Чтобы убрать эту ошибку надо разрешить запись в phar-архивы в php.ini:
1.[Phar]2.phar.readonly = 0
Теперь весь ZF упакован в один файл:
1....2.PHIndex[ZendFramework/library/Zend/Mail/Transport/Exception.php] = ./vendor/ZendFramework/library/Zend/Mail/Transport/Exception.php3.PHIndex[ZendFramework/library/Zend/Mail/Transport/Sendmail.php] = ./vendor/ZendFramework/library/Zend/Mail/Transport/Sendmail.php4.PHIndex[ZendFramework/library/Zend/Mail/Storage.php] = ./vendor/ZendFramework/library/Zend/Mail/Storage.php5.Now building the phar.6.2836 files Added to zf.phar7.Done.
Отлично, теперь в каталоге ./vendor/ZendFramework/library/ у вас должен появиться файл zf.phar. А значит можно подключать скомпиленный ZF к проекту.
test_phar.php. Подключение для ZF2:
1.2.include("phar://".dirname(__FILE__)."/vendor/ZendFramework/library/zf.phar");3.$version = new Zend\Version;4.print "Compiled ZF version is: \r\n";5.print $version::VERSION."\r\n";
test_phar.php Подключение для ZF1:
1.2.include("phar://".dirname(__FILE__)."/vendor/ZendFramework/library/zf.phar");3.$version = new Zend_Version;4.print "Compiled ZF version is: \r\n";5.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.andrey@z11:~/sandbox/zf2.ru# php ./test_phar.php2.Compiled ZF version is:3.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.
В этом посте будет пара бонусов, а именно скомпиленные в 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.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.




