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

// Декабрь 12th, 2011 // Highload, Zend Framework

В этой статье я хочу поближе познакомить вас с замечательной фичей — 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 нам понадобится несколько компонентов:

  1. Собственно сам ZF2. Скачать последнюю версию можно с офсайта или из Git репозитария.
  2. Файл-загрузчик для Phar-архива (stub.php) — заглушка. Получает управления сразу после инклюда файла с архивом. Будет ниже.
  3. Упаковщик package.php. Будет ниже.

Stub.php для Zend Framework 1.x

<?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

<?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

<?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 в один файл.

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:

[Phar]
phar.readonly = 0

Теперь весь ZF упакован в один файл:

...
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:

<?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:

<?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) следующее:

suhosin.executor.include.whitelist="phar"

После этого скрипт должен вывести версию ZF, подключённого через phar архив.

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

Самое замечательное в этой ситуации, что когда фреймворк инклюдится по одному файлу (см. рис ниже), то часть из них всё равно вываливается из кэша, а тут один файл, который априори будет всегда лежать в памяти (т.к. будет инклюдится при каждом запросе). Вот что происходит при обычном выполнении запросов.

Без использования 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

Подключаются они обычным инклюдом:

<?php
include("phar://".dirname(__FILE__)."/vendor/ZendFramework/library/zf.phar");

Не забудьте, что при перекомпилировании фреймворка при работающем APC в памяти остаётся висеть старый архив, и вам надо перезапустить php5-fpm или Apache, чтобы сбросить кэш.

Ссылки

UPD

Обязательно нужно убрать саму папку library/Zend из проекта, иначе идет передекларирование классов, т.е. ты все классы подключил через phar, а Loader делает include_once например, и он инклудит физический файл, но класс уже был объявлен в phar и выходит ошибка. Или же проверять загружен ли класс или нет перед include_once.

Share

Спасибо!


Если вам помогла статья, или вы хотите поддержать мои исследования и блог - вот лучший способ сделать это:


23 Responses to “Упаковка Zend Framework 2 в PHAR архив”

  1. Sergey:

    как же я раньше не додумался сделать этого :)
    отличная идея и реализация!

  2. Очень интересно.
    А ты не делал какие-нибудь тесты чтобы сравнить производительность?
    Хотелось бы увидеть графики «до и после».

  3. Yaroslav:

    Ну а где же сравнение производительности? Может оно и не стоит того совсем.

  4. myopenid.com ErgallM:

    А почему именно apc, а не xcache?

    • google.com Андрей Токарчук:

      Да привычнее как-то. У APC есть удобная утилитка просмотра кэша, а ещё его можно юзать прямо из PHP.

      • myopenid.com ErgallM:

        Можешь посоветовать какие-нибудь утилиты для просмотра нагрузки на сервер. У нас сайт падает…перестают инклудится файлы, а нагрузку не могу определить

        • google.com Андрей Токарчук:

          Во-первых посмотри логи mysql slow query log, найдёшь там тормзные запросы. Во вторых сделай банальные top и ps -aux и посмотри, кто у тебя там самый прожорливый. Обычно это либо mysql, либо php. В случае с mysql надо будет оптимизировать запросы, расставить индексы, смотреть на планы запросов и т.д. В случае с php — надо смотреть какие именно странички долго грузятся / потребляют память или cpu. Тут можно оптимизировать алгоритм обработки, заняться кэшированием, и кэшировать результаты обработки запросов / блоки на сайте / страницы целиком в зависимости от обстоятельств. Можешь мне на почту написать — пообщаемся на эту тему.

  5. Смотрю, тема phar становиться все более актуальна )))

    У phing кстати есть готовый таск для создания архивов:

    http://www.phing.info/docs/guide/stable/chapters/appendixes/AppendixC-OptionalTasks.html#PharPackageTask

  6. craz:

    Я так понимаю это все же продакшин версия сайта? Как IDE относятся к phar?

    • google.com Андрей Токарчук:

      Конечно только для продакшена. IDE ничего подсказывать не будет, если юзается только phar. Так это же не проблема по-моему, встроить в деплоилку компиляцию phar архива.

  7. Andrew:

    Мне непонятен один момент:
    в 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() здесь не сработает, так как в нём есть проверка на то, что путь — это директория :)

  8. Andrew:

    В общем попробовал использовать ZF через phar. Результат разочаровал: APC — 50 ms, APC + phar — 175 ms. Мерял xdebug, приложение сферическое — статья + комменты.
    Кстати, до кучи проверил и Yaf — 5 ms!

  9. Abram:

    Оффтоп, но втему:
    Еще есть сервис для очистки папки Zend от файлов, не используемых в проекте:
    http://ikuznetsov.blogspot.com/2012/02/zend-framework.html

    • google.com Андрей Токарчук:

      Неплохой сервис. Я раньше юзал похожий, но потом отказался, т.к. при использовании нового класса надо было перезакачивать архив.

  10. M.A.D.:

    К сожалению, как показала практика, APC не кэширует файлы полученные по протоколу phar://… Соответствующий тикет открыт на их сайте: https://bugs.php.net/bug.php?id=59398

    А тот файл, что виден в табличке APC — это кэширование stub части phar архива. На это намекает его размер в памяти.

    Так что ждём исправление работы APC с обёртками потоков. Другие opcode кэшеры имеют ту же проблему.

  11. alex:

    Всем привет.
    Не знаю что делать с 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.
    Буду очень благодарен за любую инфу. спасибо

Комментировать