понедельник, 3 июня 2013 г.

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

http://tokarchuk.ru/2011/12/zend-framework-2-in-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 нам понадобится несколько компонентов:
  1. Собственно сам ZF2. Скачать последнюю версию можно с офсайта или из Git репозитария.
  2. Файл-загрузчик для Phar-архива (stub.php) — заглушка. Получает управления сразу после инклюда файла с архивом. Будет ниже.
  3. Упаковщик 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 stumb
6.__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 stumb
8.__HALT_COMPILER();
Package.php
001.
002.ini_set('phar.readonly', 0);
003./**
004.* package.php
005.* Create a Zend Framework phar
006.*
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 environment
024.*/
025.$sourceLocation $options['s'];
026.$basePointer    strpos($options['s'],'Zend');
027.$pharFile       $options['p'];
028. 
029./*
030.* Make sure things are sane before progressing
031.*/
032.if ($basePointer<1 code="">
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 on
054.*/
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 runs
062.*/
063.if (file_exists($pharFile)) {
064.Phar::unlinkArchive($pharFile);
065.}
066. 
067./*
068.* Setup the phar
069.*/
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 creating
093.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 -v
02.Creating PHAR
03.Source      : ./vendor/ZendFramework/library/Zend
04.Destination : zf.phar
05.Stub File   : stub.php
06.PHP Fatal error:  Uncaught exception 'UnexpectedValueException' with message 'creating archive "zf.phar" disabled by the php.ini setting phar.readonlyin/home/andrey/zf2.ru/package.php:70
07.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.php
3.PHIndex[ZendFramework/library/Zend/Mail/Transport/Sendmail.php] = ./vendor/ZendFramework/library/Zend/Mail/Transport/Sendmail.php
4.PHIndex[ZendFramework/library/Zend/Mail/Storage.php] = ./vendor/ZendFramework/library/Zend/Mail/Storage.php
5.Now building the phar.
6.2836 files Added to zf.phar
7.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.php
2.Compiled ZF version is:
3.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
Подключаются они обычным инклюдом:
1.
2.include("phar://".dirname(__FILE__)."/vendor/ZendFramework/library/zf.phar");
Не забудьте, что при перекомпилировании фреймворка при работающем APC в памяти остаётся висеть старый архив, и вам надо перезапустить php5-fpm или Apache, чтобы сбросить кэш.

Ссылки

UPD

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