Pull to refresh

Увеличиваем производительность Zend Framework'а, собирая его классы в один файл

Reading time10 min
Views1.2K
Каждый раз когда вы запускаете ссылку, и на сервере ее обрабатывает ZendFramwork, происходят неприятные издержки производительности при сборке исполняющего кода PHP интерпретатором.

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

(В APC конечно можно настроить, чтобы PHP интерпритатор не проверял дату файлов, но при любом изменении файлов необходимо рестартовать Apache, что очень не удобно при developing'е).

В сети уже встречались сборщики классов, но они не всегда корректно собирали то что нужно.

Да это тема уже поднималась неоднократно, но я так и не нашел ни одного скрипта который бы правильно подключил классы Zend_Controller_Router_Route_Abstract и Zend_Controller_Router_Route_Chain.



Просто пробежаться по файлам и собрать их в один, не получиться. В зенде кругом используется наследование от классов и интерфейсов. И для корректного компилирования необходимо, чтобы класс который идет после implements или extends был объявлен в коде ранее.

Данная фича реализована в следующем классе и при сборке, я не думаю, что у вас возникнут проблемы.

Итак, компилятор можно прогнать в самом конце исполнения скрипта после Zend_Controller_Front::getInstance()->dispatch(); (обычно эта строка в index.php в корневой папке вашего проекта) используя следующий код:
ZCompile::make('d:/www/project/lib/zendframework-1.7.1/Zend.compiled.phplib',
               array(),
               'd:/www/project/lib/zendframework-1.7.1/'
               );


* This source code was highlighted with Source Code Highlighter.


Первый параметр — указывает путь до файла, в который будут собраны все классы.
Второй параметр — содержит массив дополнительных файлов, которые необходимо подключить (о нем чуть позже).
Третий параметр — содержит путь до библиотеки фреймворка, в конце обязателен прямой слеш '/'! Эта директория называется Zend. Т.е. в моем примере файлы классов Zend'a находятся в директории d:/www/project/lib/zendframework-1.7.1/Zend/. Повторюсь, необходимо указать путь только до папки Zend.

После запуска скрипта, ZCompile автоматически возьмет список необходимых классов и запишет их в один файл, соблюдая иерархию наследования.

В коде я обычно использую следующую конструкцию, для выбора загрузки фреймворка из собранного файла или из стандартной библиотеки:
if(FRAMEWORK_LOAD_COMPILED_ZEND==1) {
  require 'Zend.compiled.phplib';
} else {
  require 'Zend/Loader.php';
}
Zend_Loader::registerAutoload('Zend_Loader');


* This source code was highlighted with Source Code Highlighter.

Очень удобно хранить «скомпилированный» файл и класс ZCompile на одном уровне вместе с папкой Zend (библиотекой фреймворка). Т.е. они у меня хранятся в директории d:/www/project/lib/zendframework-1.7.1/

Итак, помните я говорил вам про второй параметр, который представляет из себя массив. Так вот, он позволяет подключить те классы, которые в работе текущего скрипта не использовались, но в проекте они нужны.
Там просто указываем, основные классы или директории, которые необходимо подключить. Например:
array(
  'Zend/Auth/'// подключить классы из директории, слеш обязателен на конце
  'Zend/Acl/'// подключить классы из директории, слеш обязателен на конце
  'Zend/View/Helper/HeadTitle.php'// подключить конкретный класс
  'Zend/View/Helper/Url.php',    // подключить конкретный класс
)


* This source code was highlighted with Source Code Highlighter.


Когда подключается директория, то считываются все файлы только на текущем уровне, в поддиректории не заходим. Когда подключаем чисто класс, то считываем только его.
НО при подключении класса (через директорию или напрямую) ZCompile соберет все классы, необходимые для работы указанного файла. Т.е. если вы указали директорию 'Zend/Auth/', то она скорее всего соберет все что содержится в указанной папке включая поддиректории.

Можно просто в отдельном файле выполнить следующий код, который просто соберет вам Acl и Auth:
<?php

set_include_path(
'd:/www/ksystem/lib/zendframework-1.7.1/'
. PATH_SEPARATOR . get_include_path());

ZCompile::make('d:/www/project/lib/zendframework-1.7.1/Zend.compiled.phplib',
               array('Zend/Auth/', 'Zend/Acl/'),
               'd:/www/ksystem/lib/zendframework-1.7.1/'
               );
?>


* This source code was highlighted with Source Code Highlighter.


Итак, сам класс ZCompile (к ошибкам в комментах не придираемся, это не главное!, некогда их было исправлять ;) ):
<?php
/**
* Класс для сборки модулей ZendFramwork в один файл.
*
* @author Nod nodkz.at.mail.ru
*/
class ZCompile
{
  static private $path;

  /**
   * Сборка заинклюденных файлов с удалением комментов, require/include, и проч
   *
   * @param string $dest Абсолютное имя скомпиленного файла
   * @param string $includes Массив файлов ZF для сборки
   * @return array
   */
  public static function make($dest, array $add_includes = array(), $path = '')
  {
    self::$path=$path;
    $includes=array_merge($add_includes, self::_scanFolderFiles($add_includes), self::_getZendIncludes());

    //убираем слеш в начале
    foreach($includes as $key=>&$value) {
      if(substr($value,0,1)=='/') {
        $includes[$key] = substr($value,1);
      }
    }

    // получаем последовательную очередность загрузки файлов
    $ordered_include=Array();
    foreach($includes as $class_file) {
      self::_getClassOrderIncludes($class_file, $ordered_include);
    }

    // Удалить теги '<?php' '? >', комменты ,пустые перводы строк и require/include[_once]
    // и записать в файл
    $pattern[] ='%(^\<\?php|\?\>$)%m';
    $replacement[] = '';
    $pattern[] ='%/\*.*?\*/%sm';
    $replacement[] = '';
    //$pattern[] ='%//.*$%m';
    //$replacement[] = '';
    $pattern[] ='%(require_once|include_once|require|include) [("\'](.*?)[)"\'];%sm';
    $replacement[] = '';
    $pattern[] ='%(\n){2,}%';
    $replacement[] = "\n";

    $body = "<?php\n";
    $worked_classes = Array();
    foreach ($ordered_include as &$fname) {
      if(!in_array($fname, $worked_classes)) {
        $worked_classes[] = $fname;

        $fname = self::$path.$fname;
        if(@file_exists($fname)&&is_file($fname)) {
          $body.="/*** FILE: ".$fname." ***/ \r\n";
          $body .= preg_replace($pattern, $replacement, file_get_contents($fname, true));
        }
      }
    }

    $size = file_put_contents($dest, $body);

    return array('includes' => $includes, 'compiledBody' => $body, 'compiledSize' => $size);
  }

  /**
   * Просконировать названия на соответствие названию директории (т.е. оканчивается на /).
   * И затем взять только те php файлы, которые лежат в этой папке (без рекурсивного прохода).
   *
   * @param array $add_includes
   * @return array
   */
  private static function _scanFolderFiles(&$add_includes) {
    $add_includes_dirs=array();
    foreach($add_includes as $key=>$elem) {
      if(substr($elem,-1)=='/') {
        if (is_dir(self::$path.$elem)) {
          if ($dh = opendir(self::$path.$elem)) {
            while (($file = readdir($dh)) !== false) {
              if(strpos(strtolower($file), '.php')!==false) {
                $add_includes_dirs[]=$elem.$file;
              }
            }
            closedir($dh);
          }
        }
        $add_includes[$key]='';
      }
    }
    return $add_includes_dirs;
  }

  /**
   * Получаем массив файлов которые необходимо подключить для работы, переданного файла.
   *
   * @param string $fname
   * @param array &$already_included
   */
  private static function _getClassOrderIncludes($class_filename, array &$already_included=Array(), array &$stack=Array()) {
    // проверяем на зацикливание в рекурсии
    if(!in_array($class_filename, $stack)) {
      array_push($stack, $class_filename);
    } else {
      if(!in_array($class_filename, $already_included)) {
        $already_included[]=$class_filename;
      }
      return;
    }

    // если файл существует и мы его еще не парсили
    if(is_file(self::$path.$class_filename) && !in_array($class_filename, $already_included)) {
      $class_file_content = file_get_contents(self::$path.$class_filename, true);

      // выделяем наследование классов для первоначального подключения
      // т.е. если класс расширяется через extends или implements,
      // то расширяемый клас приводим к имени файла и подключаем
      if(preg_match_all('/class\s+[_\w]+\s+(extends|implements)\s+([_\w]+)/i', $class_file_content, $arr)) {
        foreach($arr[2] as $new_class_name) {
          $new_class_path = str_replace('_','/',$new_class_name).'.php';
          if(!in_array($new_class_path, $already_included)) {
            self::_getClassOrderIncludes($new_class_path, $already_included, $stack);
          }
        }
      }

      // выдергиваем из него все строчки связанные с подключением других файлов
      if(preg_match_all('%(require_once|include_once|require|include) [("\'](.*?)[)"\'];%sm', $class_file_content, $arr)) {
        // для каждого инклудинного файла
        foreach($arr[2] as $new_class_path) {
          // проверяем на то, что мы его еще не сканировали, это дешевле чем парсить файл
          if(!in_array($new_class_path, $already_included) && !in_array($new_class_path, $stack)) {
            // проверяем чтобы имя было не переменной
            if(strpos($new_class_path, '$')===false) {
              // начинаем ранее подключать, то что инклудиться в текущем инклуде
              self::_getClassOrderIncludes($new_class_path, $already_included, $stack);
            }
          }
        }
      }
      if(!in_array($class_filename, $already_included)) {
        $already_included[]=$class_filename;
      }
    }

    array_pop($stack);
  }

  /**
   * Выбрать уникальные файлы ZF заинклюденные в проект
   *
   * @return array
   */
  private static function _getZendIncludes()
  {
    $required = array();
    $included_files = get_included_files();
    $included_files;
    foreach ($included_files as $fname) {
      $fname = str_replace('/','\\',$fname);

      if (!(strpos($fname, '
\\Zend\\') > 0) || (strstr($fname, __CLASS__ . '.php'))) {
        continue;
      }

      $required[] = str_replace('
\\', '/', substr($fname, strpos($fname, '\\Zend\\'), strlen($fname)));
    }

    return array_unique($required);
  }
}
?>


* This source code was highlighted with Source Code Highlighter.


Скачать класс

Юзайте, на здоровье.
Tags:
Hubs:
+1
Comments8

Articles

Change theme settings