Упаковка Zend Framework 2 в PHAR архив
В этой статье я хочу поближе познакомить вас с замечательной фичей – 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 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.
как же я раньше не додумался сделать этого 🙂
отличная идея и реализация!
Шпасибо 😉
Очень интересно.
А ты не делал какие-нибудь тесты чтобы сравнить производительность?
Хотелось бы увидеть графики “до и после”.
До бенчмарка руки не дошли пока. Будет позже.
Ну а где же сравнение производительности? Может оно и не стоит того совсем.
Не, не делал. Но в планах есть. Думаю на ZF1.
А почему именно apc, а не xcache?
Да привычнее как-то. У APC есть удобная утилитка просмотра кэша, а ещё его можно юзать прямо из PHP.
Можешь посоветовать какие-нибудь утилиты для просмотра нагрузки на сервер. У нас сайт падает…перестают инклудится файлы, а нагрузку не могу определить
Во-первых посмотри логи mysql slow query log, найдёшь там тормзные запросы. Во вторых сделай банальные top и ps -aux и посмотри, кто у тебя там самый прожорливый. Обычно это либо mysql, либо php. В случае с mysql надо будет оптимизировать запросы, расставить индексы, смотреть на планы запросов и т.д. В случае с php – надо смотреть какие именно странички долго грузятся / потребляют память или cpu. Тут можно оптимизировать алгоритм обработки, заняться кэшированием, и кэшировать результаты обработки запросов / блоки на сайте / страницы целиком в зависимости от обстоятельств. Можешь мне на почту написать – пообщаемся на эту тему.
Смотрю, тема phar становиться все более актуальна )))
У phing кстати есть готовый таск для создания архивов:
http://www.phing.info/docs/guide/stable/chapters/appendixes/AppendixC-OptionalTasks.html#PharPackageTask
Я так понимаю это все же продакшин версия сайта? Как IDE относятся к phar?
Конечно только для продакшена. IDE ничего подсказывать не будет, если юзается только phar. Так это же не проблема по-моему, встроить в деплоилку компиляцию phar архива.
Eclipse и Zend Studio позволяют подключать phar архив к проекту как библиотеку, так что подсказки будут
Вот это реально круто!
Мне непонятен один момент:
в stub.php вы просто устанавливаете autoload для классов Zend, но, по умолчанию, Zend содержит в себе директивы include_once, т. е. в любом случае зависимости будут пытаться грузится через include_path.
Мне кажется, что стоит в stub.php добавить что-то вроде
Phar::mapPhar(‘zf.phar’);
set_include_path(‘phar://zf.phar’ . PATH_SEPARATOR . get_include_path());
Zend_Loader_Autoloader::setZfPath() здесь не сработает, так как в нём есть проверка на то, что путь – это директория 🙂
В общем попробовал использовать ZF через phar. Результат разочаровал: APC – 50 ms, APC + phar – 175 ms. Мерял xdebug, приложение сферическое – статья + комменты.
Кстати, до кучи проверил и Yaf – 5 ms!
Ты про этот (http://habrahabr.ru/blogs/php/128271/)?
Так не удивительно, он же на сях написан.
Кстати, спасибо за тесты!
Оффтоп, но втему:
Еще есть сервис для очистки папки Zend от файлов, не используемых в проекте:
http://ikuznetsov.blogspot.com/2012/02/zend-framework.html
Неплохой сервис. Я раньше юзал похожий, но потом отказался, т.к. при использовании нового класса надо было перезакачивать архив.
К сожалению, как показала практика, APC не кэширует файлы полученные по протоколу phar://… Соответствующий тикет открыт на их сайте: https://bugs.php.net/bug.php?id=59398
А тот файл, что виден в табличке APC – это кэширование stub части phar архива. На это намекает его размер в памяти.
Так что ждём исправление работы APC с обёртками потоков. Другие opcode кэшеры имеют ту же проблему.
Опа, вот это очень интересно. Спасибо за инфу!
Всем привет.
Не знаю что делать с ZFTool для ZF2
Помогите кто может. Я его установил как модуль в папку \vendor\zendframework\zftool и в config/application.config.php дописал этот модуль. Потом поставил phpunit и он мне сказал что нельзя проинициализировать zftool. В path windows 7 я вписал …zftool/bin. Открываю cmd windows и пишу zftool.phar и вижу алерт с текстом не могу открыть файл. если пишу php zftool.phar на экране вижу все доступные команды(те все ок). Потом решил увидеть тесты на екране проекта и пошел в модуль zftool и расскоментировал все в файле zftool/config/global.php и по идее если набрать site/diagnostics должен быть отчет но у меня 404 ошибка. И с командной строки пытаюсь вызвать все команды( php public/index.php diag -v …) и ничего кроме версии php.
Буду очень благодарен за любую инфу. спасибо