Упаковка php приложения с помощью Phar

Упаковка php приложения с помощью Phar

Развертывание веб-приложений может быть сложным и громоздким, если у вас нет правильных инструментов. Если вам когда-либо прежде приходилось разворачивать Java приложения, то вы вероятно имеете представление о JAR-файлах (означает "Java Archive"). Все исполняемые и дополнительные файлы приложения могут быть объединены в один файл JAR, что бывает очень удобно, когда приходит время развертывания приложений.

Файлы Phar ("Php Archive") аналогичны концепции JAR файлов, но для PHP. Если у вас есть PHP 5.3 или выше, расширение Phar встроено и включено; вы можете начать использовать его без каких-либо дополнительных требований.

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

PHP Phar: Упаковка php приложения с помощью Phar

Прототипом данной статьи является эта англоязычная статья: Packaging Your Apps with Phar

Phar файлы по умолчанию рассматриваются как объекты только для чтения, и вы можете использовать любой файл Phar без какой-либо специальной настройки. Это отлично подходит для развертывания. Но, вот, если вы будете создавать свои собственные Phar-файлы вам необходимо разрешить доступ на запись - это можно сделать двумя способами:

  1. Через php.ini - файл

    Откройте php.ini, найдите директиву phar.readonly, и изменить её значение на phar.readonly = 0

  2. При запуске php интерпретатора

    Когда будете запускать скрипт создания Phar файла то интерпретатору PHP можно передать опцию:

            $php5 -d phar.readonly=0 <Имя файла создателя.php>
            

Итак, давайте уже чего-нибудь упакуем.

PHP Phar: Ваш первый Phar архив

Начнем с создания структуры каталогов приложения; создайте где-нибудь в вашей системе, следующую структуру:

Структура каталогов приложения для примера использования Phar
Каталог build
Будет содержать вновь созданный архив PHAR, чтобы избежать засорения каталога исходников сгенерированными артефактами.
Каталог src
Будет содержать исходные файлы приложения.
Файл index.php
Будет служить в качестве точки входа приложения.
Файл common.php
Будет служить в качестве псевдо-библиотеки общих классов, используемых приложением.
Файл config.php
Будет служить в качестве конфигурационного файла приложения.

Содержимое index.php выглядит следующим образом:

<?php
require_once "phar://myapp.phar/common.php";
$config = parse_ini_file("config.ini");
AppManager::run($config);

Содержимое common.php выглядит следующим образом :

<?php
class AppManager
{
    public static function run($config) {
         echo "Application is now running with the following configuration... ";
         var_dump($config);
     }
}

А содержимое config.ini выглядит следующим образом:

[database]
host = localhost
db   = dbname
user = myuser
pass = dbpass

PHP Phar: Создание Phar архива

Помимо, структуры приложения, необходимо также написать сценарий для создания архива Phar. Создайте еще один PHP - файл create-phar.php прямо в директории myapp с таким содержимым:

<?php
// Важно правильно определить путь **
$srcRoot    = getcwd() . "/src";
$buildRoot  = getcwd() . "/build";

$phar = new Phar(
    $buildRoot . "/myapp.phar",
    FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::KEY_AS_FILENAME,
    "myapp.phar"
);

$phar["index.php"]  = file_get_contents($srcRoot . "/index.php");
$phar["common.php"] = file_get_contents($srcRoot . "/common.php");
$phar->setStub($phar->createDefaultStub("index.php"));

copy($srcRoot . "/config.ini", $buildRoot . "/config.ini");

** если неправильно определить путь, в статье оригинале, кстати, указано так:
$srcRoot = "~/myapp/src";
$buildRoot = "~/myapp/build";
и у меня это не заработало, то вполне реально при запуске получить следующую ошибку:
Fatal error: Uncaught exception 'UnexpectedValueException' with message 'Cannot create phar '/myapp/build/myapp.phar', file extension (or combination) not recognised or the directory does not exist' in /.../myapp/create-phar.php:11

Phar так же поддерживает компрессию содержимого (необходимо, что бы было установлено соответсвующее расширение)

<?php
// ...
$phar['big.txt'] = 'This is a big text file';
$phar['big.txt']->setCompressedBZIP2();

Затем откройте окно терминала, перейдите в каталог myapp и запустите его:

user@host:~/path/to/myapp$php5 create-phar.php

Или, если вы не стали прописывать phar.readonly=0 в php.ini то можно как было сказано выше вызвать так:

user@host:~/path/to/myapp$php5 -d phar.readonly=0 create-phar.php

После запуска сценария, вы должны найти архив myapp.phar в каталоге build вместе с копией config.ini файла. Скопируйте эти два файла в корневую директории вашего веб - сервера (например htdocs).

Чтобы получить доступ к упакованному приложению Phar вы могли бы вызвать архив непосредственно, но данный подход не рекомендуется и может потребовать дополнительной настройки вашего веб-сервера для пересылки файлов Phar на правильный обработчик PHP. Другой подход заключается в создании сценария запуска, который включает в себя архив.

Создайте скрипт запуска назовите его например run.php в корневой директории вашего веб - сервера со следующим содержимым:

<?php
require "myapp.phar";

Код по сути ничего не делает, но избавляет вас от перенастройки вашего сервера для обработки PHAR файлов напрямую. Если вы размещаете свое приложение на shared - хостинге и не имеете доступа к конфигурации вашего сервера, то это идеальное решение ;)

После создания скрипта запуска, корень вашего веб-приложения может выглядеть следующим образом:

Пример файловой структуры корня веб-приложения использующего Phar

Укажите в браузере путь к run.php сценарию, и вы должны увидеть следующий вывод:

Пример вывода веб-приложения использующего Phar

PHP Phar: Под капотом

Давайте внимательнее посмотрим на код create-phar.php, чтобы разобраться что все это значит. Взглянем на следующую строку из него:

<?php
$phar = new Phar(
    $buildRoot . "/myapp.phar",
    FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::KEY_AS_FILENAME,
    "myapp.phar"
);

Здесь создается новый Phar объект, конструктор которого обычно принимает три аргумента. Первый аргумент это путь, куда сохранить созданный файл. Вы можете не только создавать новые архивы, но и манипулировать существующими.

Вторым аргументом является флаг, который указывает, как Phar объект будет обрабатывать файлы. Phar объект является подклассом PHP класса RecursiveDirectoryIterator , и этот аргумент просто передается в родительский класс. Аргументы которые я предоставил вообще-то и так передаются ему по умолчанию, но для примера я предоставил их явно.

Третий аргумент - это псевдоним архива, который необходимо будет использовать в скриптах внутри архива, при подключении других файлов внутри этого же архива, с помощью обертки потока phar. Другими словами, все файлы внутри архива, которые требуют другие файлы из архива, должны ссылаться на него в явном виде с помощью обертки потока и псевдонима. Например, вот код из index.php приведенный ранее, который подключает внутри себя файла common.php:

<?php
require_once "phar://myapp.phar/common.php";

Напоминаю, мы рассматриваем код файла create-phar.php ;) После того, как объект создан, далее по коду в архив добавляются файлы index.php и common.php

<?php
// ...
$phar["index.php"]  = file_get_contents($srcRoot . "/index.php");
$phar["common.php"] = file_get_contents($srcRoot . "/common.php");
// ...

Т.к. класс Phar реализует ко всему прочему PHP интерфейс ArrayAccess мы можем работать с Phar объектом так же как и с ассоциативным массивом, указав в качестве ключей имена файлов, а в качестве значений необходимо предоставить содержимое файлов. Это можно сделать с помощью file_get_contents(). Вы можете добавить столько файлов, сколько вам необходимо, но если вам нужно добавить много файлов, или нужно добавить все файлы некоего каталога, то вы можете рассмотреть более удобный метод buildFromDirectory() Который позволяет построить архив Phar из файлов в указанной директории:

<?php
$phar->buildFromDirectory("/path/to/dir",'/.php$/');

Первый аргумент buildFromDirectory() это путь к нужному каталогу, а второй (необязательный) - регулярное выражение, определяющее, какие файлы каталога включить. Для того, чтобы включить все файлы в каталоге, просто пропустите второй аргумент.

Далее (в коде create-phar.php) вызывается метод setStub(), чтобы создать файл заглушку. Но я бы его назвал файл-загрузчик (прим. переводчика) Файл-загрузчик говорит архиву, что делать, когда его загрузили компилятором.

И, в последней строке create-phar.php: config.ini просто копируется из src-каталога в build-каталог рядом с Phar архивом.

Прим. переводчика: Мы не помещаем config.ini в архив, потому что файлы конфигурации обычно создаются для того что бы пользователи могли их править, а поместив его внутрь архива - мы лишаем пользователя такой возможности

PHP Phar: Файл-загрузчик

При запуске архива Phar, Файл-загрузчик внутри рассматривается как мета - файл, чтобы инициализировать архив и сообщить ему, что делать, когда его вызывают, не обращаясь к конкретному файлу внутри архива. В примере выше, я предоставил создание Файл-загрузчик Phar объекту при помощи метода createDefaultStub(), который создает Файл-загрузчик по умолчанию, содержащий следующий код:

<?php
Phar::mapPhar();
include "phar://myapp.phar/index.php";
__HALT_COMPILER();

Прим. переводчика: на самом деле у меня под PHP 5.6.2 файл-загрузчик по-умолчанию выглядит совсем иначе, и имеет гораздо более объемное содержимое, но это не важно. Важно понимать для чего создается этот файл и в каких случаях он срабатывает.

При обращении к архиву таким образом файл-загрузчик сработает:

    <?php
    include "myphar.phar";
    

При обращении к конкретному файлу архива файл-загрузчик НЕ сработает:

    <?php
    include "phar://myphar.phar/somefile.php";
    

Данный файл полезен тем, что внутри него можно размещать некоторый пользовательский код, который будет выполняться при обращении к архиву, а так же в данном файле можно разместить свой собственный авто-загрузчик, если того потребует ваше приложение. Например так как ниже (взял пример из PSR-4)

        $phar->setStub("
            <?php
            Phar::mapPhar();
            spl_autoload_register(function ($class) {
                // project-specific namespace prefix
                $prefix = 'Foo\\Bar\\';
                // base directory for the namespace prefix
                $base_dir = 'phar://myapp.phar/';
                // does the class use the namespace prefix?
                $len = strlen($prefix);
                if (strncmp($prefix, $class, $len) !== 0) {
                    // no, move to the next registered autoloader
                    return;
                }
                // get the relative class name
                $relative_class = substr($class, $len);
                // replace the namespace prefix with the base directory, replace namespace
                // separators with directory separators in the relative class name, append
                // with .php
                $file = $base_dir . str_replace('\\', '/', $relative_class) . '.php';
                // if the file exists, require it
                if (file_exists($file)) {
                    require $file;
                }
            });
            __HALT_COMPILER();"
        );
    

По умолчанию, Файл-загрузчик, созданный с использованием метода createDefaultStub() иллюстрирует некоторые ключевые моменты: Phar::mapPhar() - Загрузит в память мета-данные архива и инициализирует его. Метод Phar::mapPhar() должен вызываться только внутри файла-загрузчика, и прежде, чем вы будете пытаться подключить какие либо файлы из архива, к тому же ваш Файл-загрузчик должен заканчиваться вызовом __HALT_COMPILER(); без завершающего пробела. Эта функция прекращает дальнейшее выполнение интерпретатора PHP в этой точке, что дает возможность включения любых других данных после него без риска попытки интерпретировать их. Это требование Phar, без которого архив не будет работать вообще!

PHP Phar: Пояснения по-поводу загрузчика и Phar::mapPhar()

После недолгого выяснения я узнал что, статичный метод Phar::mapPhar() предназначен для вызова только внутри файла-загрузчика! Данный метод загружает в память интерпретатора мета-информацию об архиве, например, то какие файлы и где он содержит. Без этой информации интерпретатор PHP не сможет работать с архивом - у него не будет инфы, о том какие файлы и где лежат в архиве. Поэтому Phar::mapPhar() - должен быть вызван в загрузчике, прежде чем мы попытаемся подключить какой либо конкретный файл архива! Я бы вообще назвал его не Phar::mapPhar(), а Phar::setup(), или Phar::init()! Но с названиями там похоже везде проблемы :D ...

Да, еще! Файл загрузчика физически располагается в самом начале архива (архив .phar можно попытаться открыть любым текстовым редактором и убедиться в этом). Далее в файле архива следуют данные самого архива, и файлов в нем содержащиеся, вот тут становиться очевидным требование вызова в конце файла-загрузчика директивы __HALT_COMPILER(); т.к. сразу после файла-загрузчика идут данные архива в собственном формате, которые обрушат интерпретатор, если он попытается их интерпретировать. Поэтому, после того, как интерпретатор прочитает данные файла-загрузчика, дальше ему ходить не нужно - "снег в башка попадет..." © ! Вот поэтому мы и "гасим" его директивой __HALT_COMPILER() которая останавливает работу компилятора в точке, где она расположена.

Вы можете создать свой собственный файл загрузчика для выполнения необходимых инициализаций из архива PHAR и использовать его при создании Phar-архива в следующим образом:

<?php
$phar->setStub(file_get_contents("stub.php"));

Если вы разрабатываете библиотеку классов или другой "includable" код, то размещение его в архиве PHAR является отличным решением, облегчающим его использование в разных проектах. Расширение Phar было сильно оптимизировано, чтобы дать такую ​​же, если не лучше производительность, чем обычный доступ к файлам, так что, скорее всего, вы не ухудшите производительность ваших приложений, используя его.

Для того, чтобы эффективно использовать Phar, вы должны знать, что есть некоторые ограничения. Вот несколько советов, которые помогут вам лучше понять Phar и получить максимальную отдачу от него:

  • Вы можете упаковать все приложение в Phar, но это не является решением вопроса автоматического развертывания. Это всего лишь организация единой точки входа для приложения.
  • Вы можете выполнять функции автоматического развертывания вручную внутри архива, ради большей стабильности приложения, такие, как создание кэша и загрузка каталогов на сервер, создание общих файлов конфигурации, и т.д.
  • Вы должны избегать записи в файлы Phar в реальных приложениях. Вместо этого поместите все записываемые файлы вне архива. Запись в Phar отключена в стандартной установке PHP, потому что это небезопасно для вашего приложения.

PHP Phar: Резюме

Phar может сэкономить много времени при упаковке и развертывании приложений и я рекомендую вам рассмотреть возможность использования его. Эта статья была призвана познакомить вас с основными понятиями Phar. Вы видели, как создать архив и включать в него файлы, узнали немного о важности файл загрузчика, и как PHP может получить доступ к файлам внутри архива. К сожалению, документация для работы с Phar в PHP руководстве, достаточно скудная, да и в интернете про него не особо пишут. Надеюсь, в будущих статьях я еще напишу про Phar.

Добавить комментарий


Защитный код
Обновить



Кто на сайте
Сейчас 100 гостей онлайн