Показаны сообщения с ярлыком symfony. Показать все сообщения
Показаны сообщения с ярлыком symfony. Показать все сообщения

среда, 4 февраля 2009 г.

Работа с файловой системой

В symfony существует много полезных инструментов предзначенных для решения различных задач. И ковыряя "The API Documentation", либо исходники можно найти очень полезные инструменты. В частности в symfony существуют удобные инструменты для работы с файловой системой, о которых я кратенько и напишу.

sfFinder - ищейка

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

  • file включает в разультат только файлы
  • dir включает в разультат только директории
  • any и файлы и директории

И последний метод - это пожалуй in($path[, $path2, ..]). Метод который запускает поиск и возвращает результат. Метод in() в параметрах принимает путь, где собственно искать и принимает он переменное кол-во параметров, т.е. можно задать ->in('.'), а можно ->in('apps', 'lib'). Метод этот последний не в том смысле, что больше нет методов, а в том что он как правило завершает цепочку вызовов методов.

Между вызовом ->type() и ->in() вызываются методы задающие параметры поиска. Полный список методов можно найти здесь.
Я же хочу рассмотреть лишь некоторые из них, наиболее часто используемые.

  • ignore_version_control() - игнорировать директории и файлы систем контроля версий, такие как .svn
  • maxdepth($level) - уровень вложенности элементов, глубже которого не искать
  • follow_link() - следовать по сим. линкам
  • relative() - возвращать относительный путь, если не указать, то для всех найденных элементов возвращается полный путь от корня
  • name($patter[, $pattern[, ..]]) - шаблон имени для поиска. Метод принимает переменное кол-во аргументов, т.е. можно задать несколько шаблонов
  • not_name($patter[, $pattern2[, patternN]]) - противоположен предыдущему. Исключает из результатов все элементы у которых имена совпадают с заданными шаблонами
  • mindepth($level) - задает минимальный уровень вложенности
  • size($size[, $size2[, ... $sizen]]) задает шаблон размера файла. Так же может принимать несколько шаблонов. Шаблоны можно задавать вида "< 10K" "> 10M"
  • discard($name[, $name2[, $nameN]]) - удаляет из результатов директорию совпадающую с указанными шаблонами. Принимает переменное кол-во аргументов
  • prune($name[, $name2[, $nameN]]) - удаляет из результатов вложенные элементы директорий совпадающих с задаными шаблонами
  • exec($callable[, $callable2[, $callableN]]) - вызвать функцию для каждого элемента. Например ->exec(array($object, 'methodName'))
Так же все методы принимающие переменное кол-во аргументов, можно вызывать несколько раз, задавая разные параметры, например: $finder->name('*.js', '*.css') то же самое, что и $finder->name('*.js')->name('*.css')

Ну и несколько примеров.


    // найти все классы таблиц моделей
    sfFinder::type('file')
        ->name('*Table.class.php')
        ->ignore_version_control()
        ->in('lib/model/doctrine');

    // найти все классы моделей
    sfFinder::type('file')
        ->name('*.class.php')
        ->not_name('*Table.class.php')
        ->prune('base')
        ->ignore_version_control()
        ->in('lib/model/doctrine');

    // найти все конфигурационные файлы модулей
    sfFinder::type('file')
        ->name('module.yml')
        ->ignore_version_control()
        ->in('apps');

    // найти все загруженные файлы размером больше 10Мб
    sfFinder::type('file')
        ->size('> 10M')
        ->ignore_version_control()
        ->follow_link()
        ->in('web/uploads');

    // найти все js и css файлы
    sfFinder::type('file')
        ->ignore_version_control()
        ->follow_link()
        ->name('*.js', '*.css')
        ->in('web');
    

sfFilesystem

Второй инструмент sfFilesystem представляет базовые методы для манипуляций с файловой системой. Подробнее с методами можно ознакомиться в The API Documentation.

Здесь я лишь коротко рассмотрю некоторые методы.

  • sh($cmd) - выполнить команду оболочки, возвращает результат выполненной команды. Например:
    
            $fs = new sfFilesystem;
            // вывести листинг содержимого текущей дирктории
            echo $fs->sh('ls');
            // выполнить svn update 
            $fs->sh(sprintf('svn update %s', sfConfig::get('sf_root_dir')));
            
  • copy($origin, $target, $options = array()) - копировать $origin в $target. В качестве $options принимает массив с опциями. Допустимая опция - override определяет перезаписывать ли файл, если он существует. Даже если override = false и в $target уже существует копируемый файл, но его дата последней модификации меньше, чем у оригинального, то файл будет перезаписан.
  • mirror($origin, $target, sfFinder $finder, $options = array()) - зеркалировать $origin в $target. При зеркалировании используется в том числе и метод copy($origin, $target, $options) и ему передается массив опций $options. Параметр $finder это экземпляр класса sfFinder, который осуществит поиск элементов в $origin.
  • touch($file) - создать пустой файл.
  • remove($file) - удалить файл или файлы. Принимает строку либо массив
  • rename($origin, $target) - переименовать
  • chmod($files, $mode, $umask = 0000) - изменить права. $files - либо строка путь к файлу/директории, либо массив.
  • symlink($origin, $target, $copyOnWindows = false) - создать символическую ссылку.
  • relativeSymlink($origin, $target, $copyOnWindows = false) - создать символическую ссылку с относительным путем.
  • replaceTokens($files, $beginToken, $endToken, $tokens) - производит замену в файлах. $files - файлы, в которых надо производить замену. $beginToken - открывающий обрамитель, $endToken - закрывающий обрамитель, $tokens - хеш массив замен. Пример:
    
            // во всех файлах .js и .css в директории web
            // заменить вставки "{revision}" на "137"
            $files = sfFinder::type('file')
                        ->ignore_revision_control()
                        ->follow_link()
                        ->name('*.css', '*.js')
                        ->in(sfConfig::get('sf_web_dir'));
            $fs = new sfFilesystem;
            $fs->replaceTokens($files, '{', '}', array('revision' => 137));
            

Использовать данные инструменты можно для различных задач, например: deploy, экспорт файлов (например массовая загрузка изображений), массовое переименнование файлов, создание превью для изображений, конвертация медиа файлов ну и т.д.

[ читать дальше ]

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

Selenium и PHPUnit

Итак продолжая тему тестирования веб приложений, кратко опишу возможности Selenium RC в сочетании c PHPUnit.

Для написания тестов под selenium в PHPUnit существует расширение PHPUnit_Extensions_SeleniumTestCase. Это класс от которого следует наследоваться при написании тестовых случаев. При написании тестовых случаев следует учитывать, что каждый файл может содержать только один класс, т.е. например в файле FirstTest.php должен размещаться класс FirstTest.

Браузеры и платформы

При объявлении класса можно указать под какими браузерами и платформами должны выполняться тесты. Для этого служит свойство класса $browsers. Например так:


require_once 'PHPUnit/Extensions/SeleniumTestCase.php';

class FirstTest extends PHPUnit_Extensions_SeleniumTestCase
{
    public static $browsers = array
    (
        array
        (
            'name' => 'Firefox On Linux', // имя, будет отображаться в сообщениях
            'browser' => '*firefox', // браузер
            'host' => 'localhost', // хост где запущен selenium-server
            'port' => 4444, // порт
            'timeout' => 30000, // таймаут
        ),
        array
        (
            'name' => 'Konqueror On Linux',
            'browser' => '*konqueror',
            'host' => 'localhost',
            'port' => 4444,
            'timeout' => 30000,
        ),
        array
        (
            'name' => 'Safari On Windows',
            'browser' => '*custom C\Program Files\Safari\Safari.exe -url',
            'host' => 'windows.machine',
            'port' => 4444,
            timeout => 30000,
        )
        // ...
    );
}
Так же указать браузер можно в методе setUp используя метод setBrowser

require_once 'PHPUnit/Extensions/SeleniumTestCase.php';

class FirstTest extends PHPUnit_Extensions_SeleniumTestCase
{
    public function setUp()
    {
        $this->setBrowser('*firefox');
    }
}
Если в методе setUp устанавливается браузер, то это перекроет свойство $browsers. Если вы хотите чтобы использовался набор браузеров из свойства $browsers, то не используйте метод setBrowser.

В методе setUp следует указать начальный URL используя метод setBrowserUrl


public function setUp()
{
    $this->setBrowserUrl('http//myproject.sf');
}

Вообще метод setUp используется для конфигурирования сессии Selenium RC. Для этого существует набор методов


public function setUp()
{
    $this->setBrowser('*firefox'); // устанавливает браузер
    $this->setBrowserUrl('http//myproject.sf'); // базовый URL
    $this->setHost('localhost'); // хост где запущен selenium-server
    $this->setPort(4444); // порт на котором висит selenium-server
    $this->setTimeout(30000); // таймаут соединения с selenium-server
    // кол-во секунд между командами отправляемыми Selenium RС
    $this->setSleep(1); 
}

Названия тестовых методов должны начинаться с test. Например:


require_once 'PHPUnit/Extensions/SeleniumTestCase.php';

class FirstTest extends PHPUnit_Extensions_SeleniumTestCase
{
    // ...
    public function testHomepage()
    {
        // ...
    }
}

PHPUnit_Extensions_SeleniumTestCase реализует методы утверждений и действий. Например:


public function testHomepage()
{
    $this->open('/'); // открыть корневой URL
    $this->assertTitleEquals('My Project'); // утверждение title=My Project
}
Практически ко всем методам действий и утверждений доступных в Selenium RC можно обратиться как $this->methodName(). Список доступных методов см. ниже в тексте.

Так же используя PHPUnit_Extensions_SeleniumTestCase можно выполнять Selenese тесты. Это тесты написанные в HTML файлах, с расширением .htm. В таких файлах тесты пишутся в таблицах, из трех колонок:

  • команда
  • цель
  • значение
Так же можно записывать тесты используя Selenium IDE, которая позволяет писать тесты не только в HTML формате, но и на всех доступных в Selenium RC языках. Для выполнения Selenese тестов (тестов записанных в html формате) можно воспользоваться методом runSelenese($filename), либо при объявлении тестового класса указать свойство $seleneseDirectory в котором прописать путь к тестам в html формате.

require_once 'PHPUnit/Extensions/SeleniumTestCase.php';

class SeleneseTest extends PHPUnit_Extensions_SeleniumTestCase
{
    // в этой директории рекурсивно будут искаться все файлы с расширением .htm
    public static $seleneseDirectory = 'test/selenium/html';
}

Методы PHPUnit_Extensions_SeleniumTestCase

Таблица утверждений

Утверждение Значение
assertAlertPresent Уведомляет об ошибке если не представлен alert
assertNoAlertPresent() Уведомляет об ошибке если alert представлен.
assertChecked(string $locator) Уведомляет об ошибке, если элемент найденый по $locator не отмечен (checked) для checkbox и radiobutton.
assertNotChecked(string $locator) Противоположен предыдущему.
assertConfirmationPresent() Уведомляет об ошибке если подтверждение (confirm) не представлено.
assertNoConfirmationPresent() Противоположен предыдущему.
assertEditable(string $locator) Уведомляет об ошибке если элемент найденный по $locator не редактируем (editable).
assertNotEditable(string $locator) Противоположен предыдущему.
assertElementValueEquals(string $locator, string $text) Уведомляет об ошибке, если значение элемента найденного по $locator не эквивалентно переданному $text.
assertElementValueNotEquals(string $locator, string $text) Противоположен предыдущему.
assertElementContainsText(string $locator, string $text) Уведомляет об ошибке если элемент найденый по $locator не содержит текст переданный в $text.
assertElementNotContainsText(string $locator, string $text) Противоположен предыдущему.
assertElementPresent(string $locator) Уведомляет об ошибке если элемент найденный по $locator не представлен.
assertElementNotPresent(string $locator) Противоположен предыдущему.
assertLocationEquals(string $location) Уведомляет об ошибке если текущий "location" не эквивалентен переданному $location.
assertLocationNotEquals(string $location) Противоположен предыдущему.
assertPromptPresent() Уведомляет об ошибке если prompt не представлен.
assertNoPromptPresent() Противоположен предыдущему.
assertSelectHasOption(string $selectLocator, string $option) Уведомляет об ошибке если переданная опция не доступна.
assertSelectNotHasOption(string $selectLocator, string $option) Противоположен предыдущему.
assertSelected($selectLocator, $option) Уведомляет об ошибке если переданный элемент не выбран (select списки).
assertNotSelected($selectLocator, $option) Противоположен предыдущему.
assertIsSelected(string $selectLocator, string $value) Уведомляет об ошибке если в списке не выбран элемент с переданным значением (select списки).
assertIsNotSelected(string $selectLocator, string $value) Противоположен предыдущему.
assertSomethingSelected(string $selectLocator) Уведомляет об ошибке, если опция идентифицируемая как $selectLocator не выбрана.
assertNothingSelected(string $selectLocator) Противоположен предыдущему.
assertTextPresent(string $pattern) Уведомляет об ошибке, если не представлен текст соответствующий переданному $pattern.
assertTextNotPresent(string $pattern) Противоположен предыдущему.
assertTitleEquals(string $title) Уведомляет об ошибке если заголовок страницы (title) не соответствует переданному $title.
assertTitleNotEquals(string $title) Противоположен предыдущему.
assertVisible(string $locator) Уведомляет об ошибке если элемент найденный по $locator не виден (visible).
assertNotVisible(string $locator) Противоположен предыдущему.

Доступные методы действий, более подробное описание:

  • addLocationStrategy
  • addSelection
  • allowNativeXpath
  • altKeyDown
  • altKeyUp
  • answerOnNextPrompt
  • assignId
  • break
  • captureEntirePageScreenshot
  • captureScreenshot
  • check
  • chooseCancelOnNextConfirmation
  • chooseOkOnNextConfirmation
  • click
  • clickAt
  • close
  • contextMenu
  • contextMenuAt
  • controlKeyDown
  • controlKeyUp
  • createCookie
  • deleteAllVisibleCookies
  • deleteCookie
  • doubleClick
  • doubleClickAt
  • dragAndDrop
  • dragAndDropToObject
  • dragDrop
  • echo
  • fireEvent
  • focus
  • goBack
  • highlight
  • ignoreAttributesWithoutValue
  • keyDown
  • keyPress
  • keyUp
  • metaKeyDown
  • metaKeyUp
  • mouseDown
  • mouseDownAt
  • mouseMove
  • mouseMoveAt
  • mouseOut
  • mouseOver
  • mouseUp
  • mouseUpAt
  • mouseUpRight
  • mouseUpRightAt
  • open
  • openWindow
  • pause
  • refresh
  • removeAllSelections
  • removeSelection
  • runScript
  • select
  • selectFrame
  • selectWindow
  • setBrowserLogLevel
  • setContext
  • setCursorPosition
  • setMouseSpeed
  • setSpeed
  • shiftKeyDown
  • shiftKeyUp
  • store
  • storeAlert
  • storeAlertPresent
  • storeAllButtons
  • storeAllFields
  • storeAllLinks
  • storeAllWindowIds
  • storeAllWindowNames
  • storeAllWindowTitles
  • storeAttribute
  • storeAttributeFromAllWindows
  • storeBodyText
  • storeChecked
  • storeConfirmation
  • storeConfirmationPresent
  • storeCookie
  • storeCookieByName
  • storeCookiePresent
  • storeCursorPosition
  • storeEditable
  • storeElementHeight
  • storeElementIndex
  • storeElementPositionLeft
  • storeElementPositionTop
  • storeElementPresent
  • storeElementWidth
  • storeEval
  • storeExpression
  • storeHtmlSource
  • storeLocation
  • storeMouseSpeed
  • storeOrdered
  • storePrompt
  • storePromptPresent
  • storeSelectOptions
  • storeSelectedId
  • storeSelectedIds
  • storeSelectedIndex
  • storeSelectedIndexes
  • storeSelectedLabel
  • storeSelectedLabels
  • storeSelectedValue
  • storeSelectedValues
  • storeSomethingSelected
  • storeSpeed
  • storeTable
  • storeText
  • storeTextPresent
  • storeTitle
  • storeValue
  • storeVisible
  • storeWhetherThisFrameMatchFrameExpression
  • storeWhetherThisWindowMatchWindowExpression
  • storeXpathCount
  • submit
  • type
  • typeKeys
  • uncheck
  • windowFocus
  • windowMaximize

[ читать дальше ]

четверг, 27 ноября 2008 г.

Selenium и symfony

Selenium

В symfony есть вполне удобные инструменты для создания тестов, как юнит тестов, так и функциональных тестов. Но в некоторых случаях инструментов symfony для функционального тестирования не достаточно. Например когда тестами надо покрыть функциональность частично реализованную на javascript. Или для тестирования определенной функциональности под разными браузерами. Для этих целей можно использовать Selenium.

Selenium - это набор инструментов для автоматизированного тестирования веб приложений под разными платформами. Со всеми инструментами тестирования можно ознакомиться на официальном сайте. Здесь я хочу рассмотреть Selenium Remote Control (RC) - инструмент для написания тестов для Selenium с использованием PHP Client Driver. Проще говоря - я хочу рассказать о написании тестов под Selenium используя PHP при разработке на базе symfony.

Интеграция Selenium и Symfony

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

Итак, скачаем Selenium-Rc (на текущий момент лучше скачивать "latest nightly build", потому как у доступной сейчас версии "1.0 beta 1" есть некоторые проблемы с firefox 3). Собственно из всего архива нам потребуются selenium-server*/selenium-server.jar и selenium-php-client-driver*/PEAR/Testing. По мимо этого нам так же потребуется PHPUnit. Соответственно Testing и PHPUnit необходимо разместить так, что бы они были доступны при обращениях типа


require_once 'PHPUnit/Extensions/SeleniumTestCase.php';
Можно их разместить в директории lib/vendor и прописать данный путь в include_path.

Создадим в нашем проекте (предпологается, что у нас создан проект и приложение frontend) плагин, скажем spSeleniumTestPlugin со следующей структурой:


plugins
  spSeleniumTestPlugin
    config
      settings.yml
    lib
      task
Теперь, создадим команду позволяющую запускать "selenium" тесты примерно следующей командой:

$ ./symfony selenium:test frontend
Для этого выполним следующую команду

$ ./symfony generate:task selenium:test --dir=plugins/spSeleniumTestPlugin/lib/task
Данная команда создаст класс-каркас для нашей команды в директории plugins/spSeleniumTestPlugin/lib/task. Так же разместим selenium-server.jar в дироектории plugins/spSeleniumTestPlugin/lib. И возьмемся за написание команды запускающей "selenium" тесты. Я не будет подробно останавливаться на создании команд для symfony, а просто выложу исходный код и немного его прокомментирую

require_once 'PHPUnit/Framework/TestSuite.php';
require_once 'PHPUnit/TextUI/TestRunner.php';

class seleniumTestTask extends sfBaseTask
{
    protected function configure()
    {
        $this->namespace        = 'selenium';
        $this->name             = 'test';
        $this->briefDescription = '';
        $this->detailedDescription = <<<EOF
The [selenium:test|INFO] task does things.
Call it with:

  [php symfony selenium:test|INFO]
EOF;
        $this->addArgument('application', sfCommandArgument::REQUIRED, 'The application name');
        $this->addArgument('directory', sfCommandArgument::OPTIONAL, 'Directory');
        $this->addOption('env', null, sfCommandOption::PARAMETER_REQUIRED, 'The environment', 'test');
    }

    protected function execute($arguments = array(), $options = array())
    {
        $dbManager = new sfDatabaseManager($this->configuration);
        $connection = Doctrine_Manager::connection();

        list($resource, $pipes) = $this->runSelenium(sfConfig::get('sf_selenium_port', 4444));

        $ds = DIRECTORY_SEPARATOR;
        $app = $arguments['application'];

        $testAppDir = sfConfig::get('sf_test_dir') . $ds . 'selenium' . $ds . $app;
        if ($arguments['directory'])
        {
            $testAppDir .=  $ds . $arguments['directory'];
        }

        $this->logSection('find tests in', $testAppDir);

        $files = sfFinder::type('file')
            ->ignore_version_control()
            ->follow_link()
            ->name('*Test.php')
            ->relative()
            ->in($testAppDir)
            ;

        $suite = new PHPUnit_Framework_TestSuite();

        foreach($files as $file)
        {
            $suite->addTestFile($testAppDir . $ds. $file);
        }

        $runner = new PHPUnit_TextUI_TestRunner;
        $runner->doRun($suite, array());

        proc_terminate($resource);
    }

    protected function runSelenium($port)
    {
        $ds = DIRECTORY_SEPARATOR;
        $pluginPath = realpath(dirname(__FILE__) . $ds . '..' . $ds . '..');

        $files = sfFinder::type('file')->ignore_version_control()->follow_link()->name('*selenium*server*.jar')->in($pluginPath);

        $seleniumJar = $files[0];
        $javaBin = sfConfig::get('sf_selenium_java_bin', 'java');

        $args = ' -port ' . $port;

        $cmd = escapeshellcmd($javaBin . ' -jar ' . $seleniumJar . $args);

        $dSpec = array
        (
            0 => array('pipe', 'r'),
            1 => array('pipe', 'w'),
            2 => array('pipe', 'r')
        );

        $pipes = array();
        $this->logSection('selenium', 'run');
        $resource = proc_open($cmd, $dSpec, $pipes);

        sleep(1);

        return array($resource, $pipes);
    }
}
В команде мы определяем два аргумента application и directory. Первый отвечает за то какое приложение будем тестировать, а второй не обязательный определяет в какой относительной директории искать тесты, это для того, что бы не запускать каждый раз все тесты, так как они могут выполняться достаточно долго.

Тесты по умолчанию ищутся в директории test/selenium/app_name - это для того, чтобы не смешивать их с остальными тестами (Примечание: по-хорошему надо бы реализовать интеграцию чтобы тесты могли располагаться вместе с остальными и запускаться общей для всех командой и этого добиться в общем не сложно, было бы время и желание). Так же в нашей команде определена одна не обязательная опция --env служащая для задания окружения.

Команда при выполнении пытается запуcтить selenium которого ищет в директории плагина. После запуска selenium команда собирает найденные тесты в тестовый набор и выполняет их средствами PHPUnit. Данный исходный код, можно взять как стартовую точку, для реализации под собственные нужды.

Пример теста

Для примера создадим файл test/selenium/GoogleTest.php со следующим содержанием:


setBrowser('*firefox');
        $this->setBrowserUrl('http://google.com/');
    }

    public function testGoogle()
    {
        $this->open("http://google.com/");
        $this->type("q", "hello world");
        $this->click("btnG");
        $this->waitForPageToLoad(10000);
        $this->assertRegExp("/helloworld\.ru/i", $this->getBodyText());

    }
}
Данный тест выполняет следующее, запускает браузер firefox, переходит на google.com, вводит в строку поиска (поле "q") "hello world", кликает на кнопку "btnG", ждет ответа и проверяет, что в тексте body содержится строка "helloworld.ru". А теперь запустим его

$ ./symfony selenium:test frontend
Если на выходе у вас, что-то вроде:

>> selenium  run
>> find tests in /var/workspace/sf11/example/test/selenium/backend
PHPUnit 3.2.21 by Sebastian Bergmann.

.

Time: 7 seconds


OK (1 tests)
значит все прошло успешно. Из выводимых сообщений можно понять, что тесты выполнялись 7 секунд, был выполнен 1 тест, который успешно прошел. Если у вас что-то не сработало, пишите.

На этом пока все, в следующей заметке я постараюсь описать основные возможности selenium и API PHPUnit.

[ читать дальше ]

вторник, 14 октября 2008 г.

Новое в symfony 1.2, часть 3

Request

Настройки path_info_array, path_info_key и relative_url_root перенесены из settings.yml в factories.yml (в секцию param). Это изменение убирает зависимость sfRequest и sfConfig. Эти три опции передаются в конструктор четвертым параметром. Форматы так же передаются как опция.

Константы класса sfRequest теперь имеют текстовое значение:

Константа Старое значение Новое значение
GET 2 GET
POST 4 POST
PUT 5 PUT
DELETE 6 DELETE
HEAD 7 HEAD
NONE 1 -
Методы getMethod() и getMethodName() теперь возвращают одинаковые значения. Метод getMethodName() - устарел. sfAction::getMethodNames() и соответствующий код в sfValidationExecutionFilter из sfCompat10Plugin удалены.

Response

Добавилась новый параметр для request в factory - send_http_headers. По умолчанию эта настройка установлена в true, исключая test окружение. Это изменение удаляет зависимость sfResponse и sfConfig.

Ответ (response) имеет новый метод getCharset(), который возвращает текущую кодировку ответа. Возвращаемая кодировка, меняется если изменить кодировку через установку "content-type".

Методы getStylesheets, getJavascripts могут возвращать все файлы упорядочено, если передать sfWebResponse::ALL как первый аргумент.


    $response = new sfWebResponse(new sfEventDispatcher());
    $response->addStylesheet('foo.css');
    $response->addStylesheet('bar.css', 'first');

    var_export($response->getStylesheets());

    // outputs
    array(
      'bar.css' => array(),
      'foo.css' => array(),
    )
    
sfWebResponse::ALL - это теперь значение по умолчанию. Добиться поведения как в symfony 1.1 можно передав в качестве параметра sfResponse::RAW

    var_export($response->getStylesheets(sfWebResponse::RAW));

    // outputs
    array(
      'first' =>
        array(
          'bar.css' => array (),
        ),
      '' =>
        array(
        'foo.css' => array(),
        ),
      'last' => array(),
    )
    
Все позиции (first, '', last) теперь также доступны как константы

    sfWebResponse::FIRST  === 'first'
    sfWebResponse::MIDDLE === ''
    sfWebResponse::LAST   === 'last'
    
Методы removeStylesheet() и removeJavascript() теперь принимают только один аргумент, имя файла, который надо удалить из ответа. Файл будет удален из всех доступных позиций.

[ читать дальше ]

суббота, 11 октября 2008 г.

Шаблоны ошибок и форматы запроса

В symonfy 1.1 была введена поддержка различных форматов запроса. Но там не доставало одного важного фрагмента: поддержки ошибок. Благодаря огромной работе проделаной Kris Wallsmith в symfony 1.2 этот маленький недостаток уже исправлен. Крис создал плагин реализующий поддержку ошибок в разных форматах. Позднее его плагин был включен в ядро symfony 1.2.

Что бы проще было объяснить как это работает, рассмотрим небольшой пример. Допустим есть приложение с модулем API возвращающим HTML, XML или JSON представление статьи (модель Article). Определим правила маршрутизации:


    // apps/frontend/config/routing.yml
    api_article:
      url: /api/article/:id.:sf_format
      param: { module: api, action: atricle }
      requirements:
      sf_format: (?:html|xml|json)

И соответствующее действие (action) в нашем модуле:

    class apiActions extends sfActions
    {
        public function executeArticle($request)
        {
            $this->article = Doctrine::getTable('Article')->find((int)$request->getParameter('id'));
            $this->forward404Unless($this->article);
        }
    }

Теперь, если в запросе передать идентификатор не существующей статьи, запрос будет перенаправлен на страницу с 404 ошибкой. Если при этом используется HTML формат (http://localhost/frontend_dev.php/api/article/1.html) в development окружении вы получите сообщение об ошибке sfError404Exception.

В production окружении будет показана страница с 404 ошибкой "Oops! Page Not Found".

Если изменить формат запроса на XML, то в development окружении получим ответ:

и в production окружении просто:

Т.е. ошибки теперь возвращаются в том же формате, что и запрос.

И это еще не все. Сообщения об ошибках можно настраивать для каждого формата, добавляя шаблоны в директорию проекта config/error/ или конкретного приложения apps/frontend/config/error/. Например, для того чтобы настроить сообщения об ошибках для XML запросов можно создать шаблон config/error/error.xml.php примерно следующего содержания:


    <?xml version="1.0" encoding="<?php echo sfConfig::get('sf_charset', 'UTF-8') ?>"?>
    <error>
      <code><?php echo $code ?></code>
      <message><?php echo $text ?></message>
    </error>

Что даст примерно следующий вывод:

В шаблонах ошибок доступны следующие переменные:

  • $code - код статуса ответа
  • $text - текст статуса ответа
  • $name - имя класса исключения
  • $message - сообщение исключения
  • $traces - массив содержащий полный PHP trace
  • $format - формат запроса

Но например в development окружении вместо страниц типа 404 ошибки отображаются страницы с выброшенным исключением. Как например при запросе не существующей статьи, отображается страница с исключением sfError404Exception.
Эти страницы можно настраивать за счет создания шаблонов исключений, например config/error/exception.xml.php.
Шаблоны исключений используемые по умолчанию хранятся в директории lib/exception/data/ в дистрибутиве symfony.
Для создания своего шаблона для какого-либо формата запроса, необходимо создать шаблон вида config/error/exception.FORMAT_NAME.php, где FORMAT_NAME - название формата, для которого создается шаблон. В своих шаблонах, можно подгружать шаблоны symfony используя следующую конструкцию:


    <?php include sfException::getTemplatePathForError('xml', true) ?>

навеяно: New in symfony 1.2: Error Templates and Request Formats

[ читать дальше ]

четверг, 9 октября 2008 г.

Новое в symfony 1.2, часть 2

Действия (Action)

В действиях теперь можно генерировать URL используя объект маршрутизатора (routing object) благодаря новому методу generateUrl

        public function executeIndex()
        {
            $this->redirect($this->generateUrl('homepage'));
        }
    
Метод generateUrl в параметрах принимает имя маршрута, массив параметров и флаг генерировать ли абсолютный URL.

Формы (Forms)

Добавлены два новых метода облегчающих работу с формами в шаблонах. Первый - hasErrors возвращает true если форма содержит какие-либо ошибки и false если ошибок нет. Этот метод так же возвращает false если форма не была привязана к данным. Это используется в шаблонах когда нужно вывести сообщение о том, что форма содержит ошибки.

    <?php if ($form->hasError()): ?>
      Форма содержит ошибки, исправть пожалуйста.
    <?php endif; ?>
    
Второй метод renderFormTag генерирует открывающий тег для формы. Он так же добавляет enctype атрибут, если это необходимо и скрытый тег, если метод не POST и не GET

    <?php echo $form->renderFormTag('@article_update', array('method' => 'PUT')) ?>
    
Если форма связана с Propel объектом метод renderFormTag() автоматически изменяет HTTP метод на POST для создания объекта и на PUT для редактирования объекта.

[ читать дальше ]

среда, 8 октября 2008 г.

Новое в symfony 1.2, часть 1

Имя приложения в CLI командах

Некоторые команды требуют в аргументах имя приложения, так как они используют соединение с базой данных. Но в некоторых из этих команд требовать имя приложения не логично. В symfony1.2 этот аргумент был заменен на опцию --application. Если не указать эту опцию, то symfony возьмет настройки для соединения с базой данных из файла config/databases.yml Примеры таких команд

        #propel
        ./symfony propel:build-all-load 
        ./symfony propel:build-all-load --application=frontend

        #doctrine
        ./symfony doctrine:build-all-load
        ./symfony doctrine:build-all-load --application=frontend
        ./symfony doctrine:migrate
        ./symfony doctrine:migrate --application=frontend
    

Поддержка методов PUT и DELETE в браузере

Теперь можно имитировать поддержку методов PUT и DELETE браузером используя метод POST и добавив специальный скыртый (hidden) параметр sf_method

        <form action='#' method='POST'>
            <input type='hidden' name='sf_method' value='PUT' />

            <!-- // ... -->
        </form>
    
используя эту форму вызвав sfRequest::getMethod получим PUT

Улучшения в "response"

Иногда требуется получить файлы таблиц стилей (stylesheets) и файлы с javascript кодом текущего ответа. Но в версии 1.1 методы getJavascripts и getStylesheets возрващают внутрее представление данных, что в общем оказывается не тем что ожидалось. В symfony 1.2 данные возвращаются в упорядоченом, готовом к использованию виде.

        array
        (
            'bar.css' => array(),
            'foo.css' => array()
        );
    

sfValidatorSchemaCompare валидатор

sfValidatorSchemaCompare стал немного удобнее в использовании

        // symfony 1.1 and 1.2
        $v = new sfValidatorSchemaCompare('left', sfValidatorSchemaCompare::EQUAL, 'right');

        // symfony 1.2 only
        $v = new sfValidatorSchemaCompare('left', '==', 'right');
    

[ читать дальше ]

четверг, 31 июля 2008 г.

Команды (Tasks). Создание

Начиная с версии 1.1 в symfony каждая команда представляет из себя отдельный класс, в отличие от предыдущих версий, где каждая команда была представлена двумя функциями (описание и сама команда). Команды разделены на пространства имен, что позволяет избегать конфликтов в именовании команд. Команды можно наследовать и переопределять. При совпадении названий внутри одного пространства имен, команды перекрываются, такми образом команды установленных плагинов переопределяют команды ядра симфони, а команды проекта переопределяют команды плагинов и ядра симфони.

Для создания своей команды, можно воспользоваться генерацией каркаса команды. Например создадим команду, которая выводит "Привет, Мир!" и назовем ее "hello". Определим ее в пространстве имен "example"


       ./symfony generate:task example:hello

Теперь для того чтобы убедиться, что каркас был создан введем команду:


       ./symfony list example
   
и если мы на выходе получим примерно следующее:

       Available tasks for the "example" namespace:
         :hello
   
значит каркас был успешно создан и в директории "lib/task" можно найти файл "exampleHelloTask.class.php".

В этом файле и создан класс-каркас нашей команды. В котором определены два необходимых метода "configure" и "execute", которые конфигурируют и запускают команду соответственно.


   class exampleHelloTask extends sfBaseTask
   {
       protected function configure()
       {
           // ...
       }

       protected function execute($arguments = array(), $options = array())
       {
           // ...
       }
   }
   

По умолчанию, команда "generate:task" создаст каркас в директории "lib/task", но можно указать путь, где создать каркас, например при создании своей команды в плагине:


     ./symfony generate:task --dir=plugins/spExamplePlugin/lib/task example:hello
   

В классе команды должны быть определены два метода, метод описания и конфигурации команды - "configure" и метод в котором выполняются действия команды - "execute". Метод configure для нашей команды может выглядеть примерно так:


   protected function configure()
    {
        $this->namespace = 'example'; // пространство имен
        $this->name      = 'hello'; // название команды 
        // короткое описание 
        // выводимое по команде ./symfony list
        $this->briefDescription = ''; 
        // подробное описание 
        // выводимое по команде help, в этом описании 
        // форматируя особым образом текст можно добиться 
        // необходимой подсветки.
        // например [text|INFO] выведет text зеленым цветом, а
        // [text|COMMENT] выведет text коричневым цветом
        $this->detailedDescription = '
        Команда [example:hello|INFO] просто привествует мир.
        Запускайте ее так:

          [./symfony example:hello|INFO]

        ';
        $this->aliases = array(); // массив синонимов
    }

   
Теперь можно выполнить команду:

     ./symfony help example:hello
   
и убедиться, что справочная информация выводится так как и было задумано.

Теперь добавим действий команде, дополнив код метода "execute":


   protected function execute($arguments = array(), $options = array())
   {
       $this->logSection("example", "Привет, Мир!");
   }
   
Метод "logSection" печатает отформатированное сообщение на стандартный вывод. Запускаем:

     ./symfony example:hello
   
и получаем:

   >>example   Привет, Мир!
   

Зачастую команды должны быть конфигурируемы, например выполнять действия в указанном окружении, или для определенного приложения. А так же должны уметь принимать аргументы как имя автора в команде "configure:author" или название приложения в команде "generate:app".
Эти задачи реализуются за счет описания опций и аргументов в методе "configure".
В качестве примера добавим нашей команде способность привествовать не только мир, но и разработчика вызвавшего ее. Для этого добавим аргумент "name", в котором будем принимать имя разработчика и немного изменим подробное описание команды, продемонстрировав еще одну возможность расскраски выводимого текста:


    protected function configure()
    {
        $this->namespace = 'example'; // пространство имен
        $this->name      = 'hello'; // название команды 
        // короткое описание 
        // выводимое по команде ./symfony list
        $this->briefDescription = ''; 
        // подробное описание 
        // выводимое по команде help, в этом описании 
        // форматируя особым образом текст можно добиться 
        // необходимой подсветки.
        // например [text|INFO] выведет text зеленым цветом, а
        // [text|COMMENT] выведет text коричневым цветом
        $this->detailedDescription = '
        Команда [example:hello|INFO] просто привествует мир.
        Запускайте ее так:

          [./symfony example:hello|INFO]

        Команда может привествовать не только мир, но и вас лично,
        для этого используйте аргумент [name|COMMENT]:

          [./symfony example:hello name|INFO]

        ';
        $this->aliases = array(); // массив синонимов
        $this->addArgument
        (
            'name', // название аргумента
            sfCommandArgument::OPTIONAL, // тип аргумента
            'Ваше имя, о величайший создатель', // справка
            'Мир' // значение по умолчанию
        )
    }
   
Если теперь выполнить команду:

     ./symfony help example:hello
   
то можно увидеть, что описание этого аргумента добавилось в вывод справки нашей команды. Теперь изменим код команды:

   protected function execute($arguments = array(), $options = array())
   {
       $this->logSection('example', 'Привет, '.$arguments['name'].'!');
   }
   
и запустим в двух вариантах:

     ./symfony example:hello
     >>example    Привет, Мир!
     ./symfony example:hello Сергей
     >>example    Привет, Сергей!
   

Рассмотрим подробнее описание аргумента.
Для описания используется метод:


   /**
    * name - имя аргумента
    * mode - свойства аргумента, возможные значения
    *         sfCommandArgument::OPTIONAL,
    *         sfCommandArgument::REQUIRED,
    *         sfCommandArgument::IS_ARRAY
    * help - справка
    * default - значение по умолчанию, нельзя указать при REQUIRED
    */
   addArgument($name, $mode = null, $help = '', $default = null);
   
Предназначение параметров вроде понятно из их названий, а вот о "mode" немного дополню.
sfCommandArgument::OPTIONAL - говорит о том, что аргумент является не обязательным
sfCommandArgument::REQUIRED - напртив, говорит о том, что аргумент является обязательным. При указании sfCommandArgument::REQUIRED нельзя задать значение по умолчанию для аргумента.
sfCommandArgument::IS_ARRAY - говорит о том, что аргумент является массивом. В этом случае в методе "execute" он будет доступен именно как массив и в значении по умолчанию его тоже надо задавать в виде массива. При вызове команды из консоли массивы передаются так:

     ./symfony example:hello {Сергей,Мир}
     // преобразуется в массив array('Сергей', 'Мир');
     ./symfony example:hello Сергей Мир
     // преобразуется в массив, если у аргумента указан IS_ARRAY
   
"mode" можно задать и так, объединив свойства аргумента:

   $this->addArgument
   (
       'hello',
       sfCommandArgument::OPTIONAL | sfCommandArgument::IS_ARRAY
   );
   
Для добавления нескольких аргументов за раз, можно воспользоваться методом "addArguments":

   $this->addArguments
   (
       new sfCommandArgument
       (
           'hello',
           sfCommandArgument::OPTIONAL,
           'help',
           'default'
       ),
       new sfCommandArgument
       (
           'bye',
           sfCommandArgument::OPTIONAL,
           'help',
           'default'
       )
       //, ...
   );
   

Опции команде можно добавить используя методы "addOption" или "addOptions".


   /**
    * Описывает принимаемую опцию
    *
    * name - имя опции
    * shortname - короткое имя опции
    * mode - свойства параметра опции
    * help - справка
    * default - значение по умолчанию
    */
   addOption($name, $shortname = null, $mode = null, $help = '', $default = null);
   

Добавим нашей команде возможность указания способа привествия, для этого опишем опцию "greeting" в методе "configure":


   protected function configure()
   {
       // ... пропускаю то, что сделали выше

       // обязателен только первый параметр
       $this->addOption
       (
           'greeting', // имя опции
           'g', // короткое имя опции
           sfCommandOption::PARAMETER_OPTIONAL, // mode
           'Как попривествовать', // справка
           'Привет', // значение по умолчанию
       )
   }

   protected function execute($arguments = array(), $options = array())
   {
       $this->logSection('example', $options['greeting'].', '.$arguments['name'].'!');
   }
   
И запускаем в двух вариантах:

     ./symfony example:hello -g Здравствуй
     >> example    Здравствуй, Мир!
     ./symfony example:hello --greeting=Здравствуй
     >> example    Здравствуй, Мир!
   
Предназначение параметров понятны из названия и комментариев.
"mode" может иметь следующие значения или их комбинации:
sfCommandOption::PARAMETER_NONE - если опция не нуждается в параметре, т.е. например:

     ./symfony task_name --debug
   
в метод "execute" в "$options['debug']" будет передано либо "true" если опция указана, либо "false" если опция не указана.
sfCommandOption::PARAMETER_REQUIRED - параметр у опции обязателен
sfCommandOption::PARAMETER_OPTIONAL - параметр у опции не обязателен
sfCommandOption::IS_ARRAY - параметр у опции массив, можно указать несколько раз:

     ./symfony task_name --option='param1' --option='param2'
     // преобразуется в array('param1', 'param2')
   
Ну и вариант для добавления нескольких опций за раз:

   protected function configure()
   {
       $this->addOptions
       (
           new sfCommandOption
           (
               'greeting',
               'g',
               sfCommandOption::PARAMETER_REQUIRED,
               'help',
               'Привет'
           ),
           new sfCommandOption
           (
               'env'
           )
           //, ...
       );
   }
   

Несколько полезных советов

Вызов команды внутри другой команды.


   protected function execute($arguments = array(), $options = array())
   {
       $task = new myOtherTask($this->dispatcher, $this->formatter);
       $task->run
       (
           $arguments = array
           (
               'foo' => 'bar'
           ),
           $options = array
           (
               'bar' => 'foo'
           )
       );
   }
   

Использование базы данных Если в команде используется подключение к базе данных, то необходимо в методе "configure" добавить аргумент "appliction" и опцию "--env" (опция не обязательна) для указания какие данные использовать для подключения к базе данных. После этого необходимо в методе "execute" выполнить инициализацию соединения с базой данных.


   protected function configure()
   {
       $this->addArgument('application', sfCommandArgument::REQUIRED, 'Имя приложения');
       $this->addOption('env', null, sfCommandOption::PARAMETER_REQUIRED, 'Окружение', 'dev');
       $this->addOption('connection', null, sfCommandOption::PARAMETER_REQUIRED, 'Имя соединения', 'doctrine')
       // ...
   }
   protected function execute($arguments = array(), $options = array())
   {
       $dbManager = new sfDatabaseManager($this->configuration);
       $connection = Doctrine_Manager::connection();
       // ...
   }
   
Можно и при создании каркаса команды указать опцию "--use-database", но потом поправить некоторые значения если используется "Doctrine", а в частности:
В методе "configure" при описании опции "connection" изменить имя по умолчанию.
В метод "execute":

   // заменить
   $connection = Propel::getConnection($options['connection'] ? $options['connection'] : '');
   // на
   $connection = Doctrine_Manager::connection();
   

[ читать дальше ]