📜 ⬆️ ⬇️

Automatic build application on Kohana using Phar

Any webmaster who develops and distributes a slightly serious (by the size) web application has come across at least once with the inconveniences that arise when the number of files in a project exceeds a few hundred.

Starting with PHP 5.2, it became possible to distribute applications or its individual components in the form of phar archives. I can be wrong, but so far this approach is not very common. I myself have never used this way of software distribution, but recently I decided to pay attention to it. As an experiment, I chose one project that “rotates” on the Kohana framework. And that's what came of it.

Disclaimer


I did not manage to find however reliable data on how “harms” the performance described in this article. I used this approach on a server with an installed APC, which most likely eliminates potential performance problems (if there are any). In any case, in practice, no performance degradation was observed.
')
I do not pretend to the optimality of the way in which I achieve the set result (about which below). Only the result is important - any comments on improving the process are welcome, and will be added immediately to the article.

It is assumed that phing is used to build a php project.

Prologue


So, what I want to achieve. I want to reduce the number of php files in the production version — scatter the Kohana core and each of its modules into separate phar files. Just in case, I'll tell you how the structure of the main directories and files in the application is organized.

/application
/modules
/auth
/cache
/database
/orm
/system
index.php
.htaccess


Accordingly, the system is a directory with the Kohana core, in the modules directory there are, respectively, its modules. You can watch them: auth , cache , database , orm , etc. We will remove these directories in phar-files.

Preparation: obvious obstacles and their solutions


The first obstacle. Just "drive" the folder into the archives, we will not succeed. That is, it will work out, of course, but the application will not work. Autoload-functions do not know anything about our desire to make the world better, and it will look for directories in the old place.

Decision. Kohana uses the HMVC paradigm . Therefore, slightly "podshamanit" it in the right places will not cause any difficulty. Namely - we need to do this in her very heart. So let's get started. Create a file /application/classes/kohana.php with the following content:

 <?php defined('SYSPATH') or die('No direct script access.'); class Kohana extends Kohana_Core { /** * Changes the currently enabled modules. Module paths may be relative * or absolute, but must point to a directory: * * Kohana::modules(array('modules/foo', MODPATH.'bar')); * * @param array list of module paths * @return array enabled modules */ public static function modules(array $modules = NULL) { if ($modules === NULL) { // Not changing modules, just return the current set return Kohana::$_modules; } // Start a new list of include paths, APPPATH first $paths = array(APPPATH); foreach ($modules as $name => $path) { if (is_file($path.'.phar')) { // Add phar-version of the the module to include paths $paths[] = $modules[$name] = 'phar://'.realpath($path.'.phar').DIRECTORY_SEPARATOR; } elseif(is_dir($path)) { // Add the module to include paths $paths[] = $modules[$name] = realpath($path).DIRECTORY_SEPARATOR; } else { // This module is invalid, remove it unset($modules[$name]); } } // Finish the include paths by adding SYSPATH $paths[] = SYSPATH; // Set the new include paths Kohana::$_paths = $paths; // Set the current module list Kohana::$_modules = $modules; foreach (Kohana::$_modules as $path) { $init = $path.'init'.EXT; if (is_file($init)) { // Include the module initialization file once require_once $init; } } return Kohana::$_modules; } } // End Kohana 


If you are an experienced developer, then you could easily notice here the possibility of a little tuning - at least temporary caching of the paths. Let's do this “homework”.

Obstacle second. The SYSPATH constant, which is declared in index.php, still leads to the system folder, which we will not have if we stuff its contents into the phar archive. There is nothing to do - you have to go to index.php to correct the situation.

You can, for example, add to it a check for existence in the root of the system.phar file - and you can dance depending on this, but, you see, this is not very good. So I resorted to this solution. I created an analogue of the index.php file in the root directory of the application (in the repository) and called it index.php.rename.

The idea is simple - during the project build, it will be renamed and the usual index.php will be replaced with a new one. In it, we will change the line where the constant SYSPATH (approximately 18 line) is defined for this:

 /** * The directory in which the Kohana resources are located. The system * directory must contain the classes/kohana.php file. * * @see http://kohanaframework.org/guide/about.install#system */ $system = 'system.phar'; 


And, accordingly, approximately in line 78 we change the contents of the SYSPATH constant itself:

 define('SYSPATH', 'phar://'.realpath($system).DIRECTORY_SEPARATOR); 


But I would like to linger on this moment for a while. For example, on a production server, I do not need modules that allow unit tests, documentation modules, etc. Therefore, I do not just not include them in the assembly, but I also make a separate /application/bootstrap.php.rename , in which I initialize the kernel without them. You can find a bunch of places in your application where you can use a similar approach. But that is not the problem.

I repent - I spent 5 minutes searching for information on how to recursively change the files of the form * .rename for the entire project to just the files of the form * with the replacement of the original. And then I decided not to spend it anymore, and write my own task for phing. It looks like this:

 <?php require_once 'phing/Task.php'; class Rename extends Task { /** * Directory with the files to rename * * @var string */ protected $targetDir; /** * Extension of files to rename * * @var string */ protected $ext = 'rename'; /** * Files array * * @var array */ protected $filesets = array(); /** * Task initialization * * @return boolean */ public function init() { return true; } /** * Sets target directory with the files to rename * * @param string $targetDir Target directory * @return void */ public function setTargetDir($targetDir) { $this->targetDir = $targetDir; } /** * Sets extension of files to rename * * @param string $ext Extension of files to rename * @return void */ public function setExt($ext) { $this->ext = $ext; } /** * Creates fileSet parameter * * @return array Fileset array */ public function createFileSet() { $num = array_push($this->filesets, new FileSet()); return $this->filesets[$num - 1]; } /** * Entry point - file renaming * * @throws BuildException * @return void */ public function main() { // We may have several filesets - // will process them all foreach ($this->filesets as $fs) { try { // Get an files array for current fileset $files = $fs->getDirectoryScanner($this->project) ->getIncludedFiles(); $fullPath = realpath($fs->getDir($this->project)); foreach ($files as $file) { //if (is_file($fullPath.$file)) //{ // Get file extension $ext = pathinfo($fullPath.'/'.$file, PATHINFO_EXTENSION); $this->log('Ext '.$ext); if ($ext == $this->ext) { $new = $fullPath.'/'.str_replace('.'.$this->ext, '', $file); $this->log('Renaming file '.$fullPath.'/'.$file.' to '.$new); // If file already exists, remove it if (is_file($new)) unlink($new); // Then rename our file rename($fullPath.'/'.$file, $new); } //} } } catch (BuildException $be) { if ($this->failonerror) { throw $be; } else { $this->log($be->getMessage(), Project::MSG_WARN); } } } } } 


I will not consider here the process of writing and using my own tasks for phing - you can read about it, for example, here . If someone offers a solution — you can solve this problem using standard phing tools, and I will immediately include it in an article that will only benefit from it.

A couple of words about the features of Phar


So everything seems ready. Let me just say a few words about the features of phar and its use. In essence, the phar file is an archive. It is tempting to indicate a higher compression level in order to get smaller files (the same profit). But, although I myself have never received any problems with this, nevertheless, this is not recommended. It is not recommended to compress them at all - this will potentially protect you from problems with the portability of the application (there was a server on Linux, started on Windows, and the PHP version is smaller, or without the necessary set of libraries - and everything stopped working).

We write build.xml


Again, no one forces you to use the algorithm proposed by me - you can modify it to your own taste. But usually the application build process looks like this:

  1. The project from the working directory (working copy of the repository with loaded submodules) is copied to the build directory, excluding service files (all kinds of .git folders, .gitignore files, etc.) and garbage (unit tests, documentation, etc.)
  2. Kohana's core and its modules are shoved over phar archives (just what we were going to)
  3. Removed Kohana core folders and its modules


I do not consider automatic unit testing. This is the topic of a separate article. In this process, you can also include minimization of static files - javascript and css, their "gluing".

For example, I also remove extra line breaks and spaces from view files (view files). As a result, the HTML code of the application on the production server is issued in one line. But, again ... the article is not about that. As a result, we get something like the following build.xml:

 <?xml version="1.0"?> <project name="make_project" basedir=".." default="build"> <!--      --> <property name="source_dir" value="/path/to/source/directory/" override="false" /> <property name="deploy_dir" value="/path/to/deploy/directory/" override="false" /> <!--  ,    --> <target name="copy"> <copy todir="${deploy_dir}"> <fileset dir="${source_dir}" defaultexcludes="true"> <include name="**" /> <exclude name=".git/" /> <exclude name="**/.git/" /> <exclude name="*.gitignore" /> <exclude name="**/*.gitignore" /> <exclude name="*.gitmodules" /> <exclude name="**/*.gitmodules" /> <exclude name="*.sql" /> <exclude name="**/*.sql" /> <exclude name="build.xml" /> <exclude name="*.md" /> <exclude name="*.txt" /> <exclude name="**/*.txt" /> <exclude name="application/cache/**" /> <exclude name="application/logs/**" /> <exclude name="modules/userguide/" /> <exclude name="modules/**/config/" /> <exclude name="system/guide/" /> <exclude name="system/tests/" /> <exclude name="modules/**/guide/" /> <exclude name="modules/**/tests/" /> </fileset> </copy> </target> <!--  *.rename  --> <target name="rename_prepared_files" depends="copy"> <taskdef name="rename" classname="phing.tasks.my.Rename" /> <echo>Renaming prepared files</echo> <rename ext="rename"> <fileset dir="${deploy_dir}"> <include name="**/*.rename" /> </fileset> </rename> </target> <!--    --> <target name="make_empty_dirs" depends="rename_prepared_files"> <mkdir dir="${deploy_dir}/application/logs" /> <mkdir dir="${deploy_dir}/application/cache" /> </target> <!--  --> <target name="pack_to_phar" depends="make_empty_dirs"> <pharpackage destfile="${deploy_dir}/system.phar" basedir="${deploy_dir}/system/" signature="md5"> <fileset dir="${deploy_dir}/system/"> <include name="**" /> </fileset> <metadata> <element name="version" value="3.1.3.1" /> <element name="authors"> <element name="Kohana Team" /> </element> </metadata> </pharpackage> <!-- ... --> <pharpackage destfile="${deploy_dir}/modules/orm.phar" basedir="${deploy_dir}/modules/orm/" signature="md5"> <fileset dir="${deploy_dir}/modules/orm/"> <include name="**" /> </fileset> <metadata> <element name="version" value="3.1.3.1" /> <element name="authors"> <element name="Kohana Team" /> </element> </metadata> </pharpackage> </target> <!--     (system, modules) --> <target name="remove_unused" depends="pack_to_phar"> <delete dir="${deploy_dir}/system" /> <delete dir="${deploy_dir}/modules/auth" /> <delete dir="${deploy_dir}/modules/cache" /> <delete dir="${deploy_dir}/modules/database" /> <delete dir="${deploy_dir}/modules/orm" /> </target> <!-- ! --> <target name="build" depends="remove_unused" /> </project> 


It remains only to test everything on the test server and make sure that everything works.

Conclusion


As a result, we got a neatly designed (technically) web application that can be uploaded to the server much faster than when the Kohana kernel and modules were in the usual, usual form (not phar-archives).

I will remind you. What I didn't consider was performance issues. I have no idea about the difference in performance (if any) and the order of this difference. Everything that I managed to find on the Internet on this issue either suggests that the difference is not too noticeable, or does not contain any serious research at all.

I suspect that if there is a decrease in performance, then it is easily stopped by APC, xCache or another intermediate code cache (opcode cache). What is the difference, what intermediate code to cache - from a physical file or from a file that is taken from a phar-archive. But truth is more important. It may be worthwhile to tackle this issue more closely and publish the results.

Source: https://habr.com/ru/post/124536/


All Articles