понедельник, 19 декабря 2011 г.

The "default" context does not exist и symfony doctrine:data-dump

Если вы не видели подобных сообщений в CLI symfony - вы счастливчик. Если да - рассмотрим workaround для борьбы с подобными капризами.

Если вы читаете это, скорее всего у вас вылетела ошибка при попытке выполнить эту команду (либо подобную ей):


Непродолжительное гугление показало, что, во-первых, панацеи нет, во-вторых - ноги растут из использования sfContext::getInstance() в классах модели. Для решения проблемы можно применить следующий workaround:
  1. Для того, чтобы понять где именно обваливается task, запустим команду с волшебным ключиком --trace:


    После кучи дампов запросов мы увидим уже намозолившую глаза ошибку, а далее trace:


  2. Теперь дело за малым - пойти в метод PaymentSystem->getUrl(), и провести небольшой рефакторинг. Цель - убрать оттуда вызов sfContext::getInstance(), или, если это невозможно, поместить его внутрь условия:


  3. Так повторяем до тех пор, пока не увидим сообщение:


Читать дальше......

понедельник, 23 мая 2011 г.

Учим генерилку модулей diem своему кодестайлу

Кто знаком с symfony, тот наверняка слышал о такой штуке как diem. Это замечательная надстройка над первой версией symfony, которая прикручивает к нему возможности CMS.

Diem, как и symfony, облегчает жизнь программисту, предоставляя возможность автоматической генерации кода. Но генерит он его, естественно, в symfony-style. А что, если он нам не подходит?

В проекте на diem концепция работы со view несколько отличается от стандартной. Если в стандартном symfony всё идёт от темплитов, которые представляют собой целые страницы (слой отображения), и экшенов - слой контроллера для темплита, то в diem сделали упор на удобство работы контенщика, а именно на возможность построения страницы "по кирпичикам". Это значит, что теперь менеджер создаёт страницу и накидывает на неё "виджеты" (widgets). Прямая аналогия с компонентами битрикса. А widget по своей сути есть не что иное, как partial с соответствующей логикой, помещённой в компонент.

Таким образом, в diem слой Controller/View несколько сместился от action/template к component/partial (хотя, конечно action/template никто не отменял). Косвенный намёк на это можно найти в описании релиза 5й версии:
The keyword "actions" has been replaced with "components".
"actions" keyword is now deprecated but still supported for backward compatibility.
Теперь, собственно, про генерацию кода. Для примера - конкретная (и, я думаю, распространённая) задача. Научить diem следующим правилам:
  1. В качестве отступов использовать "\t" вместо четырёх пробелов.
  2. Фигурная скобка должна открываться и закрываться на той же строке как у класса, так и у методов.
Здесь рассматривается генерация кода и правка кодестайла для модулей в diem. Для "голого" symfony всё выглядит иначе (об этом, быть может напишу позже).

Итак, чтобы сгенерить новый модуль в diem, описываем его в modules.yml:
testmodule:
      name: Демо-стенд
      components:
        createOrder: {name: 'Создание заказа'}
        changeService: {name: 'Изменени услуги'}
По такому описанию, diem в состоянии сгенерить файлы модуля, если набрать в CLI:
./symfony dmFront:generate
После вызова этой команды, управление по цепочке передаётся до метода dmFrontGenerateTask::execute(). Этот метод делает 3 вещи:
  1. Генерит скелет класса action'ов:
    $actionGenerator = new dmFrontActionGenerator($module, $this->dispatcher, $this->get('filesystem'), $moduleDir);
    
  2. Компонентов
    $componentGenerator = new dmFrontComponentGenerator($module, $this->dispatcher, $this->get('filesystem'), $moduleDir);
    
  3. И шаблоны
    $actionTemplateGenerator = new dmFrontActionTemplateGenerator($module, $this->dispatcher, $this->get('filesystem'), $moduleDir);
    
Diem в качестве инструмента для генерации кода использует Zend CodeGenerator (lib/vendor/diem/dmCorePlugin/lib/vendor/Zend/CodeGenerator). Кроме того, они добавили своих французских плющек и назвали их dmZend CodeGenerator(lib/vendor/diem/dmCorePlugin/lib/vendor/dmZend). Всё это вызывается из классов генерации кода lib/vendor/diem/dmFrontPlugin/lib/generator.

Вот три места где заключены все правила кодестайла для генератора кода diem.

Чтобы решить задачу, поставленную выше, делаем следующие изменения:
  • lib/vendor/diem/dmCorePlugin/lib/vendor/Zend/CodeGenerator/Php/Class.php метод generate()
    $implemented = $this->getImplementedInterfaces();
            if (!empty($implemented)) {
                $output .= ' implements ' . implode(', ', $implemented);
            }
    
    //        $output .= self::LINE_FEED . '{' . self::LINE_FEED . self::LINE_FEED;
            $output .= ' {' . self::LINE_FEED . self::LINE_FEED;  // Our codestyle
    
  • lib/vendor/diem/dmCorePlugin/lib/vendor/dmZend/CodeGenerator/Php/Method.php метод generate()
    if (!empty($parameters)) {
          foreach ($parameters as $parameter) {
            $parameterOuput[] = $parameter->generate();
          }
    
          $output .= implode(', ', $parameterOuput);
        }
    
    //    $output .= ')' . self::LINE_FEED . $indent . '{' . self::LINE_FEED;
        $output .= ') {' . self::LINE_FEED;  //Our codestyle
    
  • lib/vendor/diem/dmFrontPlugin/lib/generator/dmFrontActionGenerator.php
    class dmFrontActionGenerator extends dmFrontModuleGenerator
    {
      protected
      $class,
    //  $indentation = '  ';
      $indentation = "\t";  // Our codestyle
    
  • lib/vendor/diem/dmFrontPlugin/lib/generator/dmFrontComponentGenerator.php
    class dmFrontComponentGenerator extends dmFrontModuleGenerator
    {
      protected
      $class,
    //  $indentation = '  ';
      $indentation = "\t";  // Our codestyle
    
    

В итоге, получаем код, соответствующий нашему кодестайлу:
/**
 * Демо-стенд components
 * 
 * No redirection nor database manipulation ( insert, update, delete ) here
 */
class testmoduleComponents extends myFrontModuleComponents {

 public function executeCreateOrder() {
  // Your code here
 }

 public function executeChangeService() {
  // Your code here
 }


}
У обычной правки файлов либы есть минус - они слетят при обновлении. Тут есть 2 выхода - поместить либу под систему контроля версий, либо добавить свой task для CLI. Второй, конечно, правильнее, но, как оно обычно бывает, сложнее :)
Читать дальше......

четверг, 24 марта 2011 г.

Автолоад своих классов, bitrix и ZF

Однажды на одном из проектов у меня возникла задача сделать централизованный автолоад классов, так как проект из "маленького" стал перерастать в "не совсем маленький". Проблема, казалось бы, банальна, но не всё оказалось так просто. Проект на битриксе, слой контроллера и модели, в большинстве своём, самописный - порядка 25 классов, которые активно юзают Zend Framework.

Кратенько о структуре папок. В папке /external проекта у нас хранится ZF, а в папке /php.inc/classes - наша библиотечка. Причём наши классы - не отельный namespace формата ZF, а просто куча файлов с префиксом, лежащие на одном уровне вложенности.

Прежде всего, регистрируем include_path'ы:

set_include_path(
    $_SERVER['DOCUMENT_ROOT'] . '/../external' . PATH_SEPARATOR
    . $_SERVER['DOCUMENT_ROOT'] . '/../php.inc/classes' . PATH_SEPARATOR 
    . get_include_path()
);

Теперь собственно решение. Самое простое решение - через функцию __autoload.
  1. Свои классы определяем просто - смотрим на первую часть имени класса - она содержит уникальный (в рамках проекта) префикс. Если есть совпадение - просто делаем require_once.
  2. Классы ZF тоже определяем по префиксу, но для подключения класса используем Zend_Loader::loadClass(). Он может кинуть исключение, если попытаться подключить класс, допустим, ZendTruLaLa. Поэтому оборачиваем в try/catch.
  3. С классами битрикса хитрее. Основное время при написании автолоадера ушло на поиск того, как битрикс автолоадит свои классы. В результате нашёл. Без подробностей - делает он это через свой автолоад и метод CModule::RequireAutoloadClass(). Он возвращает true/false - как раз то что нам нужно.
Теперь можно автолоадить:
function __autoload($sClassName) {

    if (strpos($sClassName, 'My') === 0) {
        return require_once $sClassName . '.php';
    }

    if (strpos($sClassName, 'Zend') === 0) {
        try {
            Zend_Loader::loadClass($sClassName);
            return true;
        } catch (Exception $e) {
            return false;
        }
    }

    if (CModule::RequireAutoloadClass($sClassName)) {
        return true;
    }

    return false;
}
Решение далеко не идеальное. Оно подходит в качестве первоначального "рабочего" варианта и даёт пространство для рефакторинга. Здесь отмечу, что если делать "по уму", то, конечно, это должен быть отдельный класс, что-то вроде этого:
class MyAutoloader {
    
    public function __construct() {
        spl_autoload_register(array($this, 'loader'));
    }

    private function loader($sClassName) {

        if (strpos($sClassName, 'My') === 0) {
            return require_once $sClassName . '.php';
        }

        if (strpos($sClassName, 'Zend') === 0) {
            try {
                Zend_Loader::loadClass($sClassName);
                return true;
            } catch (Exception $e) {
                return false;
            }
        }

        if (CModule::RequireAutoloadClass($sClassName)) {
            return true;
        }

        return false;
    }
}
Поэтому ставим себе
/**
 * @todo Разобраться, почему не подключаются классы
 * битрикса при использовании spl_autoload_register.
 * После этого можно будет вынести автолоад
 * в отдельный класс.
 */
Читать дальше......