tag:blogger.com,1999:blog-62229422998857967592024-03-13T20:47:46.517+03:00разработка на symfony или симфония веб-разработкио разработке веб приложений на базе symfony + doctrine и все что с этим связаноSergeyhttp://www.blogger.com/profile/06129401613024492304noreply@blogger.comBlogger9125tag:blogger.com,1999:blog-6222942299885796759.post-4767908687930874342009-02-04T02:03:00.002+03:002009-02-11T14:06:02.200+03:00Работа с файловой системой<p>
В symfony существует много полезных инструментов предзначенных для решения различных задач. И ковыряя "The API Documentation", либо исходники можно найти очень полезные инструменты. В частности в symfony существуют удобные инструменты для работы с файловой системой, о которых я кратенько и напишу.
</p>
<div class="fullpost">
<h2>sfFinder - ищейка</h2>
<p>
Первый инструмент - это <strong>sfFinder</strong>. Предзначен для нахождения файлов и директорий.
Пожалуй первый метод с которого начинается работа - это <strong>type($type)</strong>, который в общем задает тип искомых элементов и может принимать в качестве параметра:
<ul>
<li><strong>file</strong> включает в разультат только файлы</li>
<li><strong>dir</strong> включает в разультат только директории</li>
<li><strong>any</strong> и файлы и директории</li>
</ul>
</p>
<p>
И последний метод - это пожалуй <strong>in($path[, $path2, ..])</strong>. Метод который запускает поиск и возвращает результат. Метод <strong>in()</strong> в параметрах принимает путь, где собственно искать и принимает он переменное кол-во параметров, т.е. можно задать <strong>->in('.')</strong>, а можно <strong>->in('apps', 'lib')</strong>. Метод этот последний не в том смысле, что больше нет методов, а в том что он как правило завершает цепочку вызовов методов.
</p>
<p>
Между вызовом <strong>->type()</strong> и <strong>->in()</strong> вызываются методы задающие параметры поиска.
Полный список методов можно найти <a href="http://www.symfony-project.org/api/1_2/sfFinder">здесь</a>.<br />
Я же хочу рассмотреть лишь некоторые из них, наиболее часто используемые.
<ul>
<li><strong>ignore_version_control()</strong> - игнорировать директории и файлы систем контроля версий, такие как .svn</li>
<li><strong>maxdepth($level)</strong> - уровень вложенности элементов, глубже которого не искать</li>
<li><strong>follow_link()</strong> - следовать по сим. линкам</li>
<li><strong>relative()</strong> - возвращать относительный путь, если не указать, то для всех найденных элементов возвращается полный путь от корня</li>
<li><strong>name($patter[, $pattern[, ..]])</strong> - шаблон имени для поиска. Метод принимает переменное кол-во аргументов, т.е. можно задать несколько шаблонов</li>
<li><strong>not_name($patter[, $pattern2[, patternN]])</strong> - противоположен предыдущему. Исключает из результатов все элементы у которых имена совпадают с заданными шаблонами</li>
<li><strong>mindepth($level)</strong> - задает минимальный уровень вложенности</li>
<li><strong>size($size[, $size2[, ... $sizen]])</strong> задает шаблон размера файла. Так же может принимать несколько шаблонов. Шаблоны можно задавать вида <strong>"< 10K"</strong> <strong>"> 10M"</strong>
<li><strong>discard($name[, $name2[, $nameN]])</strong> - удаляет из результатов директорию совпадающую с указанными шаблонами. Принимает переменное кол-во аргументов</li>
<li><strong>prune($name[, $name2[, $nameN]])</strong> - удаляет из результатов вложенные элементы директорий совпадающих с задаными шаблонами</li>
<li><strong>exec($callable[, $callable2[, $callableN]])</strong> - вызвать функцию для каждого элемента. Например ->exec(array($object, 'methodName'))</li>
</ul>
Так же все методы принимающие переменное кол-во аргументов, можно вызывать несколько раз, задавая разные параметры, например: <strong>$finder->name('*.js', '*.css')</strong> то же самое, что и <strong>$finder->name('*.js')->name('*.css')</strong>
</p>
<p>
Ну и несколько примеров.
<pre><code class="php">
// найти все классы таблиц моделей
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');
</code></pre>
</p>
<h2>sfFilesystem</h2>
<p>Второй инструмент <strong>sfFilesystem</strong> представляет базовые методы для манипуляций с файловой системой. Подробнее с методами можно ознакомиться в <a href="http://www.symfony-project.org/api/1_2/sfFilesystem">The API Documentation</a>.
</p>
<p>
Здесь я лишь коротко рассмотрю некоторые методы.
<ul>
<li><strong>sh($cmd)</strong> - выполнить команду оболочки, возвращает результат выполненной команды. Например:
<pre><code class="php">
$fs = new sfFilesystem;
// вывести листинг содержимого текущей дирктории
echo $fs->sh('ls');
// выполнить svn update
$fs->sh(sprintf('svn update %s', sfConfig::get('sf_root_dir')));
</code></pre>
</li>
<li><strong>copy($origin, $target, $options = array())</strong> - копировать <strong>$origin</strong> в <strong>$target</strong>. В качестве <strong>$options</strong> принимает массив с опциями. Допустимая опция - <strong>override</strong> определяет перезаписывать ли файл, если он существует. Даже если override = false и в $target уже существует копируемый файл, но его дата последней модификации меньше, чем у оригинального, то файл будет перезаписан.</li>
<li><strong>mirror($origin, $target, sfFinder $finder, $options = array())</strong> - зеркалировать <strong>$origin</strong> в <strong>$target</strong>. При зеркалировании используется в том числе и метод <strong>copy($origin, $target, $options)</strong> и ему передается массив опций <strong>$options</strong>. Параметр <strong>$finder</strong> это экземпляр класса <strong>sfFinder</strong>, который осуществит поиск элементов в <strong>$origin</strong>.</li>
<li><strong>touch($file)</strong> - создать пустой файл.</li>
<li><strong>remove($file)</strong> - удалить файл или файлы. Принимает строку либо массив</li>
<li><strong>rename($origin, $target)</strong> - переименовать</li>
<li><strong>chmod($files, $mode, $umask = 0000)</strong> - изменить права. <strong>$files</strong> - либо строка путь к файлу/директории, либо массив.</li>
<li><strong>symlink($origin, $target, $copyOnWindows = false)</strong> - создать символическую ссылку.</li>
<li><strong>relativeSymlink($origin, $target, $copyOnWindows = false)</strong> - создать символическую ссылку с относительным путем.</li>
<li><strong>replaceTokens($files, $beginToken, $endToken, $tokens)</strong> - производит замену в файлах. <strong>$files</strong> - файлы, в которых надо производить замену. <strong>$beginToken</strong> - открывающий обрамитель, <strong>$endToken</strong> - закрывающий обрамитель, <strong>$tokens</strong> - хеш массив замен. Пример:
<pre><code class="php">
// во всех файлах .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));
</code></pre>
</li>
</ul>
</p>
<p>
Использовать данные инструменты можно для различных задач, например: <strong>deploy</strong>, экспорт файлов (например массовая загрузка изображений), массовое переименнование файлов, создание превью для изображений, конвертация медиа файлов ну и т.д.
</p>
</div>Sergeyhttp://www.blogger.com/profile/06129401613024492304noreply@blogger.com1tag:blogger.com,1999:blog-6222942299885796759.post-8443591415603493252008-12-01T17:34:00.005+03:002009-02-09T03:06:00.549+03:00Selenium и PHPUnit<p>
Итак продолжая тему <a href="http//symfonyru.blogspot.com/2008/11/selenium-symfony.html" target="_blank">тестирования веб приложений</a>, кратко опишу возможности <a href="http//selenium-rc.seleniumhq.org/" target="_blank">Selenium RC</a> в сочетании c <a href="http//www.phpunit.de" target="_blank">PHPUnit</a>.
</p>
<div class="fullpost">
<p>
Для написания тестов под selenium в PHPUnit существует расширение <strong>PHPUnit_Extensions_SeleniumTestCase</strong>.
Это класс от которого следует наследоваться при написании тестовых случаев.
При написании тестовых случаев следует учитывать, что каждый файл может содержать только один класс, т.е. например в файле <strong>FirstTest.php</strong> должен размещаться класс <strong>FirstTest</strong>.
</p>
<h2>Браузеры и платформы</h2>
<p>При объявлении класса можно указать под какими браузерами и платформами должны выполняться тесты. Для этого служит свойство класса <strong>$browsers</strong>.
Например так:
<pre><code class=php>
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,
)
// ...
);
}
</code></pre>
Так же указать браузер можно в методе <strong>setUp</strong> используя метод <strong>setBrowser</strong>
<pre><code class=php>
require_once 'PHPUnit/Extensions/SeleniumTestCase.php';
class FirstTest extends PHPUnit_Extensions_SeleniumTestCase
{
public function setUp()
{
$this->setBrowser('*firefox');
}
}
</code></pre>
Если в методе <strong>setUp</strong> устанавливается браузер, то это перекроет свойство <strong>$browsers</strong>. Если вы хотите чтобы использовался набор браузеров из свойства <strong>$browsers</strong>, то не используйте метод <strong>setBrowser</strong>.
</p>
<p>
В методе <strong>setUp</strong> следует указать начальный URL используя метод <strong>setBrowserUrl</strong>
<pre><code class=php>
public function setUp()
{
$this->setBrowserUrl('http//myproject.sf');
}
</code></pre>
</p>
<p>
Вообще метод <strong>setUp</strong> используется для конфигурирования сессии <strong>Selenium RC</strong>. Для этого существует набор методов
<pre><code class=php>
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);
}
</code></pre>
</p>
<p>
Названия тестовых методов должны начинаться с <strong>test</strong>.
Например:
<pre><code class=php>
require_once 'PHPUnit/Extensions/SeleniumTestCase.php';
class FirstTest extends PHPUnit_Extensions_SeleniumTestCase
{
// ...
public function testHomepage()
{
// ...
}
}
</code></pre>
</p>
<p>
<strong>PHPUnit_Extensions_SeleniumTestCase</strong> реализует методы утверждений и действий.
Например:
<pre><code class=php>
public function testHomepage()
{
$this->open('/'); // открыть корневой URL
$this->assertTitleEquals('My Project'); // утверждение title=My Project
}
</code></pre>
Практически ко всем методам действий и утверждений доступных в Selenium RC можно обратиться как <strong>$this->methodName()</strong>. Список доступных методов см. ниже в тексте.
</p>
<p>
Так же используя PHPUnit_Extensions_SeleniumTestCase можно выполнять <strong>Selenese</strong> тесты. Это тесты написанные в <strong>HTML</strong> файлах, с расширением <strong>.htm</strong>. В таких файлах тесты пишутся в таблицах, из трех колонок:
<ul>
<li>команда</li>
<li>цель</li>
<li>значение</li>
</ul>
Так же можно записывать тесты используя <a href="http://selenium-ide.seleniumhq.org/" target="_blank">Selenium IDE</a>, которая позволяет писать тесты не только в <strong>HTML</strong> формате, но и на всех доступных в <strong>Selenium RC</strong> языках.
Для выполнения <strong>Selenese</strong> тестов (тестов записанных в html формате) можно воспользоваться методом <strong>runSelenese($filename)</strong>, либо при объявлении тестового класса указать свойство <strong>$seleneseDirectory</strong> в котором прописать путь к тестам в html формате.
<pre><code class='php'>
require_once 'PHPUnit/Extensions/SeleniumTestCase.php';
class SeleneseTest extends PHPUnit_Extensions_SeleniumTestCase
{
// в этой директории рекурсивно будут искаться все файлы с расширением .htm
public static $seleneseDirectory = 'test/selenium/html';
}
</code></pre>
</p>
<h2>Методы PHPUnit_Extensions_SeleniumTestCase</h2>
<p>
Таблица утверждений
<table>
<thead>
<th>Утверждение</th>
<th>Значение</th>
</thead>
<tr>
<td>assertAlertPresent</td>
<td>Уведомляет об ошибке если не представлен alert</td>
</tr>
<tr>
<td>assertNoAlertPresent()</td>
<td>Уведомляет об ошибке если alert представлен.</td>
</tr>
<tr>
<td>assertChecked(string $locator)</td>
<td>Уведомляет об ошибке, если элемент найденый по $locator не отмечен (checked) для checkbox и radiobutton.</td>
</tr>
<tr>
<td>assertNotChecked(string $locator)</td>
<td>Противоположен предыдущему.</td>
</tr>
<tr>
<td>assertConfirmationPresent()</td>
<td>Уведомляет об ошибке если подтверждение (confirm) не представлено.</td>
</tr>
<tr>
<td>assertNoConfirmationPresent()</td>
<td>Противоположен предыдущему.</td>
</tr>
<tr>
<td>assertEditable(string $locator)</td>
<td>Уведомляет об ошибке если элемент найденный по $locator не редактируем (editable).</td>
</tr>
<tr>
<td>assertNotEditable(string $locator)</td>
<td>Противоположен предыдущему.</td>
</tr>
<tr>
<td>assertElementValueEquals(string $locator, string $text)</td>
<td>Уведомляет об ошибке, если значение элемента найденного по $locator не эквивалентно переданному $text.</td>
</tr>
<tr>
<td>assertElementValueNotEquals(string $locator, string $text)</td>
<td>Противоположен предыдущему.</td>
</tr>
<tr>
<td>assertElementContainsText(string $locator, string $text)</td>
<td>Уведомляет об ошибке если элемент найденый по $locator не содержит текст переданный в $text.</td>
</tr>
<tr>
<td>assertElementNotContainsText(string $locator, string $text)</td>
<td>Противоположен предыдущему.</td>
</tr>
<tr>
<td>assertElementPresent(string $locator)</td>
<td>Уведомляет об ошибке если элемент найденный по $locator не представлен.</td>
</tr>
<tr>
<td>assertElementNotPresent(string $locator)</td>
<td>Противоположен предыдущему.</td>
</tr>
<tr>
<td>assertLocationEquals(string $location)</td>
<td>Уведомляет об ошибке если текущий "location" не эквивалентен переданному $location.</td>
</tr>
<tr>
<td>assertLocationNotEquals(string $location)</td>
<td>Противоположен предыдущему.</td>
</tr>
<tr>
<td>assertPromptPresent()</td>
<td>Уведомляет об ошибке если prompt не представлен.</td>
</tr>
<tr>
<td>assertNoPromptPresent()</td>
<td>Противоположен предыдущему.</td>
</tr>
<tr>
<td>assertSelectHasOption(string $selectLocator, string $option)</td>
<td>Уведомляет об ошибке если переданная опция не доступна.</td>
</tr>
<tr>
<td>assertSelectNotHasOption(string $selectLocator, string $option)</td>
<td>Противоположен предыдущему.</td>
</tr>
<tr>
<td>assertSelected($selectLocator, $option)</td>
<td>Уведомляет об ошибке если переданный элемент не выбран (select списки).</td>
</tr>
<tr>
<td>assertNotSelected($selectLocator, $option)</td>
<td>Противоположен предыдущему.</td>
</tr>
<tr>
<td>assertIsSelected(string $selectLocator, string $value)</td>
<td>Уведомляет об ошибке если в списке не выбран элемент с переданным значением (select списки).</td>
</tr>
<tr>
<td>assertIsNotSelected(string $selectLocator, string $value)</td>
<td>Противоположен предыдущему.</td>
</tr>
<tr>
<td>assertSomethingSelected(string $selectLocator)</td>
<td>Уведомляет об ошибке, если опция идентифицируемая как $selectLocator не выбрана.</td>
</tr>
<tr>
<td>assertNothingSelected(string $selectLocator)</td>
<td>Противоположен предыдущему.</td>
</tr>
<tr>
<td>assertTextPresent(string $pattern)</td>
<td>Уведомляет об ошибке, если не представлен текст соответствующий переданному $pattern.</td>
</tr>
<tr>
<td>assertTextNotPresent(string $pattern)</td>
<td>Противоположен предыдущему.</td>
</tr>
<tr>
<td>assertTitleEquals(string $title)</td>
<td>Уведомляет об ошибке если заголовок страницы (title) не соответствует переданному $title.</td>
</tr>
<tr>
<td>assertTitleNotEquals(string $title)</td>
<td>Противоположен предыдущему.</td>
</tr>
<tr>
<td>assertVisible(string $locator)</td>
<td>Уведомляет об ошибке если элемент найденный по $locator не виден (visible).</td>
</tr>
<tr>
<td>assertNotVisible(string $locator)</td>
<td>Противоположен предыдущему.</td>
</tr>
</table>
</p>
<p>
Доступные методы действий, более <a href="http://selenium-core.seleniumhq.org/reference.html" target="_blank">подробное описание</a>:
<ul>
<li>addLocationStrategy</li>
<li>addSelection</li>
<li>allowNativeXpath</li>
<li>altKeyDown</li>
<li>altKeyUp</li>
<li>answerOnNextPrompt</li>
<li>assignId</li>
<li>break</li>
<li>captureEntirePageScreenshot</li>
<li>captureScreenshot</li>
<li>check</li>
<li>chooseCancelOnNextConfirmation</li>
<li>chooseOkOnNextConfirmation</li>
<li>click</li>
<li>clickAt</li>
<li>close</li>
<li>contextMenu</li>
<li>contextMenuAt</li>
<li>controlKeyDown</li>
<li>controlKeyUp</li>
<li>createCookie</li>
<li>deleteAllVisibleCookies</li>
<li>deleteCookie</li>
<li>doubleClick</li>
<li>doubleClickAt</li>
<li>dragAndDrop</li>
<li>dragAndDropToObject</li>
<li>dragDrop</li>
<li>echo</li>
<li>fireEvent</li>
<li>focus</li>
<li>goBack</li>
<li>highlight</li>
<li>ignoreAttributesWithoutValue</li>
<li>keyDown</li>
<li>keyPress</li>
<li>keyUp</li>
<li>metaKeyDown</li>
<li>metaKeyUp</li>
<li>mouseDown</li>
<li>mouseDownAt</li>
<li>mouseMove</li>
<li>mouseMoveAt</li>
<li>mouseOut</li>
<li>mouseOver</li>
<li>mouseUp</li>
<li>mouseUpAt</li>
<li>mouseUpRight</li>
<li>mouseUpRightAt</li>
<li>open</li>
<li>openWindow</li>
<li>pause</li>
<li>refresh</li>
<li>removeAllSelections</li>
<li>removeSelection</li>
<li>runScript</li>
<li>select</li>
<li>selectFrame</li>
<li>selectWindow</li>
<li>setBrowserLogLevel</li>
<li>setContext</li>
<li>setCursorPosition</li>
<li>setMouseSpeed</li>
<li>setSpeed</li>
<li>shiftKeyDown</li>
<li>shiftKeyUp</li>
<li>store</li>
<li>storeAlert</li>
<li>storeAlertPresent</li>
<li>storeAllButtons</li>
<li>storeAllFields</li>
<li>storeAllLinks</li>
<li>storeAllWindowIds</li>
<li>storeAllWindowNames</li>
<li>storeAllWindowTitles</li>
<li>storeAttribute</li>
<li>storeAttributeFromAllWindows</li>
<li>storeBodyText</li>
<li>storeChecked</li>
<li>storeConfirmation</li>
<li>storeConfirmationPresent</li>
<li>storeCookie</li>
<li>storeCookieByName</li>
<li>storeCookiePresent</li>
<li>storeCursorPosition</li>
<li>storeEditable</li>
<li>storeElementHeight</li>
<li>storeElementIndex</li>
<li>storeElementPositionLeft</li>
<li>storeElementPositionTop</li>
<li>storeElementPresent</li>
<li>storeElementWidth</li>
<li>storeEval</li>
<li>storeExpression</li>
<li>storeHtmlSource</li>
<li>storeLocation</li>
<li>storeMouseSpeed</li>
<li>storeOrdered</li>
<li>storePrompt</li>
<li>storePromptPresent</li>
<li>storeSelectOptions</li>
<li>storeSelectedId</li>
<li>storeSelectedIds</li>
<li>storeSelectedIndex</li>
<li>storeSelectedIndexes</li>
<li>storeSelectedLabel</li>
<li>storeSelectedLabels</li>
<li>storeSelectedValue</li>
<li>storeSelectedValues</li>
<li>storeSomethingSelected</li>
<li>storeSpeed</li>
<li>storeTable</li>
<li>storeText</li>
<li>storeTextPresent</li>
<li>storeTitle</li>
<li>storeValue</li>
<li>storeVisible</li>
<li>storeWhetherThisFrameMatchFrameExpression</li>
<li>storeWhetherThisWindowMatchWindowExpression</li>
<li>storeXpathCount</li>
<li>submit</li>
<li>type</li>
<li>typeKeys</li>
<li>uncheck</li>
<li>windowFocus</li>
<li>windowMaximize</li>
</ul>
</p>
</div>Sergeyhttp://www.blogger.com/profile/06129401613024492304noreply@blogger.com0tag:blogger.com,1999:blog-6222942299885796759.post-29049096203276313452008-11-27T18:14:00.016+03:002009-02-09T03:07:36.075+03:00Selenium и symfony<h2>Selenium</h2>
<p>
В <b>symfony</b> есть вполне удобные <a href="http://www.symfony-project.org/book/1_1/15-Unit-and-Functional-Testing" taget="_blank">инструменты для создания тестов</a>, как юнит тестов, так и функциональных тестов.
Но в некоторых случаях инструментов symfony для функционального тестирования не достаточно. Например когда тестами надо покрыть функциональность частично реализованную на javascript. Или для тестирования определенной функциональности под разными браузерами. Для этих целей можно использовать <a href="http://selenium.seleniumhq.org/" target="_blank">Selenium</a>.
</p>
<div class="fullpost">
<p>
<b>Selenium</b> - это набор инструментов для автоматизированного тестирования веб приложений под разными платформами. Со всеми инструментами тестирования можно ознакомиться на официальном сайте. Здесь я хочу рассмотреть <a href="http://selenium-rc.seleniumhq.org/" target="_blank">Selenium Remote Control (RC)</a> - инструмент для написания тестов для Selenium с использованием <a href="http://selenium-rc.seleniumhq.org/php.html" target="_blank">PHP Client Driver</a>. Проще говоря - я хочу рассказать о написании тестов под <b>Selenium</b> используя <b>PHP</b> при разработке на базе <b>symfony</b>.
</p>
<h2>Интеграция Selenium и Symfony</h2>
<p>
В первую очередь, хотелось бы хоть как-то интегрировать <b>selenium</b> в <b>symfony</b>. В идеале конечно хотелось бы иметь удобный плагин, которым можно поделиться с сообществом. К сожалению на разработку полноценного плагина нет времени, поэтому сделаем просто небольшую заготовку, которая возможно в дальнейшем перерастет в полноценный плагин.
</p>
<p>
Итак, скачаем <a href="http://selenium-rc.seleniumhq.org/download.html" target="_blank">Selenium-Rc</a> (на текущий момент лучше скачивать "latest nightly build", потому как у доступной сейчас версии "1.0 beta 1" есть некоторые проблемы с firefox 3).
Собственно из всего архива нам потребуются <b>selenium-server*/selenium-server.jar</b> и <b>selenium-php-client-driver*/PEAR/Testing</b>. По мимо этого нам так же потребуется <a href="http://www.phpunit.de/manual/3.3/en/installation.html" target="_blank">PHPUnit</a>.
Соответственно <b>Testing</b> и <b>PHPUnit</b> необходимо разместить так, что бы они были доступны при обращениях типа
<pre><code class='php'>
require_once 'PHPUnit/Extensions/SeleniumTestCase.php';
</code></pre>
Можно их разместить в директории <b>lib/vendor</b> и прописать данный путь в <b>include_path</b>.
</p>
<p>
Создадим в нашем проекте (предпологается, что у нас создан проект и приложение frontend) плагин, скажем <b>spSeleniumTestPlugin</b> со следующей структурой:
<pre><code>
plugins
spSeleniumTestPlugin
config
settings.yml
lib
task
</code></pre>
Теперь, создадим команду позволяющую запускать "selenium" тесты примерно следующей командой:
<pre><code>
$ ./symfony selenium:test frontend
</code></pre>
Для этого выполним следующую команду
<pre><code>
$ ./symfony generate:task selenium:test --dir=plugins/spSeleniumTestPlugin/lib/task
</code></pre>
Данная команда создаст класс-каркас для нашей команды в директории <b>plugins/spSeleniumTestPlugin/lib/task</b>.
Так же разместим <b>selenium-server.jar</b> в дироектории <b>plugins/spSeleniumTestPlugin/lib</b>.
И возьмемся за написание команды запускающей "selenium" тесты. Я не будет подробно останавливаться на <a href="http://symfonyru.blogspot.com/2008/07/tasks_31.html" target="_blank">создании команд для symfony</a>, а просто выложу исходный код и немного его прокомментирую
<pre><code class='php'>
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);
}
}
</code></pre>
В команде мы определяем два аргумента <b>application</b> и <b>directory</b>. Первый отвечает за то какое приложение будем тестировать, а второй не обязательный определяет в какой относительной директории искать тесты, это для того, что бы не запускать каждый раз все тесты, так как они могут выполняться достаточно долго.
</p>
<p>
Тесты по умолчанию ищутся в директории <b>test/selenium/app_name</b> - это для того, чтобы не смешивать их с остальными тестами (<i>Примечание: по-хорошему надо бы реализовать интеграцию чтобы тесты могли располагаться вместе с остальными и запускаться общей для всех командой и этого добиться в общем не сложно, было бы время и желание</i>). Так же в нашей команде определена одна не обязательная опция <b>--env</b> служащая для задания окружения.
</p>
<p>
Команда при выполнении пытается запуcтить <b>selenium</b> которого ищет в директории плагина. После запуска <b>selenium</b> команда собирает найденные тесты в тестовый набор и выполняет их средствами PHPUnit.
Данный исходный код, можно взять как стартовую точку, для реализации под собственные нужды.
</p>
<h2>Пример теста</h2>
<p>
Для примера создадим файл <b>test/selenium/GoogleTest.php</b> со следующим содержанием:
<pre><code class='php'>
<?php
require_once 'Testing/Selenium.php';
require_once 'PHPUnit/Framework/TestCase.php';
require_once 'PHPUnit/Extensions/SeleniumTestCase.php';
class GoogleTest extends PHPUnit_Extensions_SeleniumTestCase
{
protected function setUp()
{
$this->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());
}
}
</code></pre>
Данный тест выполняет следующее, запускает браузер <b>firefox</b>, переходит на google.com, вводит в строку поиска (поле "q") "hello world", кликает на кнопку "btnG", ждет ответа и проверяет, что в тексте body содержится строка "helloworld.ru".
А теперь запустим его
<pre><code>
$ ./symfony selenium:test frontend
</code></pre>
Если на выходе у вас, что-то вроде:
<pre><code>
>> 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)
</code></pre>
значит все прошло успешно. Из выводимых сообщений можно понять, что тесты выполнялись 7 секунд, был выполнен 1 тест, который успешно прошел. Если у вас что-то не сработало, пишите.
</p>
<p>
На этом пока все, в следующей заметке я постараюсь описать основные возможности selenium и API PHPUnit.
</p>
</div>Sergeyhttp://www.blogger.com/profile/06129401613024492304noreply@blogger.com0tag:blogger.com,1999:blog-6222942299885796759.post-85535294299313215512008-10-23T01:17:00.009+04:002009-02-09T03:08:04.123+03:00Email рассылка средствами Swift<p>
Email рассылка достаточно часто возникающая, перед разработчиком, задача. В простейшем случае это рассылка новостей сайта.
На сайте symfony в <a href="http://www.symfony-project.org/cookbook/1_1/en/email">The symfony Cookbook</a> для отправки электронной почты предлагается использовать <a href="http://www.swiftmailer.org/">Swift Mailer</a>.
И действительно Swift Mailer - это мощная библиотека решающая практически все задачи связанные с отправкой электронных писем. Именно с ее помощью мы и создадим нашу email рассылку.
</p>
<div class="fullpost">
<p>
<h2>Установка Swift Mailer</h2>
Для начала надо установить Swift Mailer. Если проект находится под контролем Subversion, тогда добавим свойство <b>svn:externals</b> директории <b>lib/vendor</b>, если такой директории еще нет, то можно ее создать.
<pre><code>
$ mkdir -p lib/vendor
$ svn propedit svn:externals lib/vendor
</code></pre>
добавим следующую строку
<pre><code>
swift http://swiftmailer.svn.sourceforge.net/svnroot/swiftmailer/tags/php5/3.3.3/lib/
</code></pre>
и обновим
<pre><code>
$ svn update
</code></pre>
Если проект не использует Subversion, то просто разместим Swift Mailer в директории <b>lib/vendor</b>, для этого можно выполнить следующие команды:
<pre><code>
$ mkdir -p lib/vendor
$ svn co http://swiftmailer.svn.sourceforge.net/svnroot/swiftmailer/tags/php5/3.3.3/lib/ lib/vendor/swift
</code></pre>
На этом установка завершена.
</p>
<p>
<h2>Создание моделей</h2>
Теперь определим модели данных, которые нам потребуются. В простейшем случае - это подписчик (Subscriber) и сообщение (Message).
Модели могут быть примерно следующими:
<pre><code>
--- config/doctrine/subscribe.yml
Subscriber:
columns:
first_name: string(100)
middle_name: string(100)
last_name: string(100)
email: string(200)
email_key: string(40)
Message:
columns:
subject: string(255)
body: string
sended: timestamp
</code></pre>
Несколько комментариев. В модели <b>Subscriber</b> пояснить пожалуй стоит только поле <b>email_key</b>. Это хеш, сгенерированный для каждого подписчика и используемый для возможности отписаться от рассыкли.
В моделе <b>Message</b> поле <b>sended</b> содержит отметку времени об отправке письма, если это поле null значит письмо еще не было отправлено и его следует разослать подписчикам.
Предназначение остальных полей кажется вполне понятным исходя из их названий.
Вопрос о создании интерфейса управления сообщениями, подписчиками и т.д. оставим за кадром.
</p>
<p>
<h2>Команда рассылки писем</h2>
Для рассылки писем создадим команду (task), для этого воспользуемся командой:
<pre><code>
$ ./symfony generate:task mailer:send
</code></pre>
Это создаст команду <b>mailer:send</b> определенную в файле <b>lib/task/mailerSendTask.class.php</b>.
Метод <b>configure</b> этой команды может выглядеть примерно так:
<pre><code class='php'>
protected function configure()
{
$this->namespace = 'mailer';
$this->name = 'send';
$this->briefDescription = 'Рассылает письма';
$this->detailedDescription = <<<EOF
[mailer:send|INFO] Отправляет письмо подписчикам.
Используется так:
[./symfony mailer:send appliaction --env=environment|INFO]
Простой пример:
[./symfony mailer:send frontend --env=prod|INFO]
EOF;
$this->addArgument('application', sfCommandArgument::REQUIRED, 'Приложение');
$this->addOption('env', null, sfCommandOption::PARAMETER_REQUIRED, 'Окружение', 'dev');
}
</code></pre>
Добавление аргумента <b>application</b> в данном случае обязательно, так как мы будем использовать базу данных.
В дальнейшем команду надо будет запускать примерно так:
<pre><code>
$ ./symfony mailer:send frontend
</code></pre>
</p>
<p>
<h2>Подготовка к рассылке email</h2>
Для начала необходимо получить письмо, которое следует разослать подписчикам. Для этого определим метод <b>getWaitingMessage()</b> в классе <b>MessageTable</b>, он может выглядеть примерно так:
<pre><code class='php'>
# lib/model/doctrine/MessageTable.class.php
public function getWaitingMessage()
{
return $this->createQuery('m')
->where('m.sended IS NULL')
->limit(1)
->execute()
->getFirst();
}
</code></pre>
Также необходимо получить подписчиков, но для этого нам пока достаточно простого:
<pre><code class='php'>
Doctrine::getTable('Subscriber')->findAll();
</code></pre>
</p>
<p>
<h2>Подготовка сообщения для email рассылки</h2>
Прежде, чем отправлять письмо, его необходимо подготовить. Например, если мы создаем сообщение через wysiwyg редактор, то нам как минимум необходимо вложить все изображения и изменить на них ссылки, а так же надо проставить <b>email_key</b> в ссылку по которой можно отписаться от рассылки.
Для этого определим метод <b>getMessage()</b> в классе нашей команды <b>mailerSendTask</b>, в простейшем случае он может выглядеть примерно так:
<pre><code class='php'>
protected $message;
public function getMessage()
{
// получаем сообщение для отправки
$this->message = Doctrine::getTable('Message')->getWaitingMessage();
if (!$message)
{
throw new sfException('Не найдено сообщений для отправки');
}
// создаем email сообщение используя Swift
$mimeMessage = new Swift_Message($this->message['subject']);
$mimeMessage->setCharset('UTF-8');
// находим все изображения в сообщении.
$images = array();
if (preg_match_all('/<img[^>]*?src=(?:\'|")(.*?)(?:\'|")/umsi', $this->message['body'], $matches))
{
foreach($matches[1] as $match)
{
$images[$match] = new Swift_Message_Image
(
new Swift_File($webDir . DIRECTORY_SEPARATOR . $match)
);
}
}
// приаттачиваем найденные изображения и меняем ссылки в сообщении
if (!empty($images))
{
foreach($images as $src => $image)
{
$attach = $mimeMessage->attach($image);
$this->message['body'] = str_replace($src, $attach, $this->message['body']);
}
}
// наконец прикрепляем собственно сообщение.
$mimeMessage->attach
(
new Swift_Message_Part
(
$this->message['body'],
'text/html',
'base64'
)
);
return $mimeMessage;
}
</code></pre>
Если с заменой изображений все понятно, то вот со ссылкой позволяющей отписаться мы пока подождем, так как она должна быть своей для каждого подписчика.
</p>
<p>
<h2>Составление списка получателей</h2>
Прежде чем отправлять письмо необходимо определить список получателей. Для этого воспользуемся классом <b>Swift_RecipientList</b> и определим метод <b>getRecipients</b> в классе нашей команды, код может выглядеть примерно так:
<pre><code class='php'>
// lib/task/mailerSendTask.class.php
protected $subscribers;
// ...
public function getRecipients()
{
$this->subscribers = Doctrine::getTable('Subscriber')->findAll();
$recipients = new Swift_RecipientList;
$encoder = new Swift_Message_Encoder;
foreach($subscribers as $subscriber)
{
$recipients->addTo
(
$subscriber['email'],
sprintf
(
'=?utf-8?B?%s=',
$encoder->base64Encode
(
sprintf("%s %s", $subsciber['first_name'], $subscriber['last_name'])
)
)
);
}
return $recipients;
}
</code></pre>
Здесь используется <b>Swift_Message_Encoder</b> для кодирования получателей. Это необходимо, так как если в имени получателей содержатся например кириллические символы, то могут быть проблемы. Во избежание этих проблем и для адекватного отображения почтовыми клиентами мы и используем кодирование имен.
</p>
<p>
<h2>Отправка email сообщения</h2>
Для отправки нам так же необходимы некоторые настройки, которые лучше вынести в файл настроек приложения <b>app.yml</b>
Выглядеть он может примерно так:
<pre><code>
mail:
subscribe:
host: localhost
port: 25
from: robot@example.com
</code></pre>
Сам код отправки email сообщения может выглядеть примерно так:
<pre><code class='php'>
public function execute($arguments=array(), $options=array())
{
$dbManager = new sfDatabaseManager($this->configuration);
// получаем настройки
$config = sfConfig::get('app_mail_subscribe');
// создаем экземпляр мейлера
$mailer = new Swift(new Swift_Connection_SMTP($config['host'], $config['port']));
// получаем список получателей
$recipients = $this->getRecipients();
try
{
// создаем объект пакетной рассылки и отправляем письмо
// по списку получателей
$batch = Swift_BatchMailer($mailer);
$batch->send($this->getMessage(), $recipients, $config['from']);
// после отправки письма, ставим отметку у сообщения
$message = Doctrine::getTable('Message')->find($this->message['id']);
$message->sended = date('Y-m-d H:i');
$message->save();
}
catch (Exception $e)
{
new throw sfException($e->getMessage());
}
}
</code></pre>
Мы используем <b>Swift_BatchMailer</b> для отправки email сообщения по списку получателей.
</p>
<p>
<h2>Персонализация сообщения в email рассылке</h2>
У нас уже отправляются письма по списку подписчиков. Но мы еще не решили одной важной проблемы - вставка ссылки для возможности отписаться от рассылки. Конечно можно было бы пойти простым путем и сделать ссылку для всех одинаковой, а уже на этой странице запрашивать email по которому отправлять письмо с нужной ссылкой. Но это слишком не удобно. Более того, хотелось бы хоть как-то персонализировать сообщения и начинать их например с приветствия по имени каждого подписчика.
Для этого мы используем плагин для <b>Swift</b> - <b>Swift_Plugin_Decorator</b>. Который позволяет производить замены в тексте сообщения для каждого получателя.
Нам для этого необходимо создать массив замен по следующему принципу:
<pre><code class='php'>
$replacements = array
(
'email@address.com' => array('{key}' => 'value', '{key2}' => 'value 2'),
// ...
);
</code></pre>
Все ключи (они не обязательно должны быть обрамлены {}, можно использовать что угодно, например %%key%%) заменяются на значения.
Для использования плагина необходимо для начала создать массив замен. Для простоты будем создавать массив замен в момент получения списка получателей. Для этого добавим свойство классу команды <b>mailerSendTask</b> - <b>$replacements</b> и изменим код получения списка получателей.
<pre><code class='php'>
// lib/task/mailerSendTask.class.php
protected $subscribers;
protected $replacements = array();
// ...
public function getRecipients()
{
$this->subscribers = Doctrine::getTable('Subscriber')->findAll();
$recipients = new Swift_RecipientList;
$encoder = new Swift_Message_Encoder;
foreach($subscribers as $subscriber)
{
$recipients->addTo
(
$subscriber['email'],
sprintf
(
'=?utf-8?B?%s=',
$encoder->base64Encode
(
sprintf("%s %s", $subsciber['first_name'], $subscriber['last_name'])
)
)
);
$this->replacements[$subscriber['email']] = array
(
'{first_name}' => $subscriber['first_name'],
'{middle_name}' => $subscriber['middle_name'],
'{last_name}' => $subscriber['last_name'],
'{email_key}' => $subscriber['email_key']
);
}
return $recipients;
}
</code></pre>
Теперь немного модифицируем код отправки сообщения, указав, что мы хотим использовать плагин:
<pre><code class='php'>
public function execute($arguments=array(), $options=array())
{
$dbManager = new sfDatabaseManager($this->configuration);
// получаем настройки
$config = sfConfig::get('app_mail_subscribe');
// создаем экземпляр мейлера
$mailer = new Swift(new Swift_Connection_SMTP($config['host'], $config['port']));
// получаем список получателей
$recipients = $this->getRecipients();
// подключаем плагин
$mailer->attachPlugin(new Swift_Plugin_Decorator($this->replacements), 'decorator');
try
{
// создаем объект пакетной рассылки и отправляем письмо
// по списку получателей
$batch = Swift_BatchMailer($mailer);
$batch->send($this->getMessage(), $recipients, $config['from']);
// после отправки письма, ставим отметку у сообщения
$message = Doctrine::getTable('Message')->find($this->message['id']);
$message->sended = date('Y-m-d H:i');
$message->save();
}
catch (Exception $e)
{
new throw sfException($e->getMessage());
}
}
</code></pre>
Теперь в теле сообщения все вставки вида <b>{first_name}</b>, <b>{middle_name}</b>, <b>{last_name}</b>, <b>{email_key}</b> будут заменяться на соответствующие значения. Т.е. письмо может выглядеть примерно так:
<pre><code class='html'>
Здравствуйте, {first_name} {last_name}!
...
---
Если вы хотите отписаться от данной рассылки, перейдите по <a href="http://example.com/unsubscribe/{email_key}">этой ссылке</a>
</code></pre>
На этом все. За кадром остались многие вопросы создания хорошей email рассылки, например управление подписчиками, собщениями, составление писем, создание нескольких рассылок, запланированная рассылка и т.д. Но главной целью данного поста было показать как персонализировать письма при отправке по списку получателей.
</p>
</div>Sergeyhttp://www.blogger.com/profile/06129401613024492304noreply@blogger.com2tag:blogger.com,1999:blog-6222942299885796759.post-48176328098267885652008-10-14T14:58:00.003+04:002009-02-09T03:08:33.981+03:00Новое в symfony 1.2, часть 3<p>
<h2>Request</h2>
Настройки <b>path_info_array</b>, <b>path_info_key</b> и <b>relative_url_root</b> перенесены из <b>settings.yml</b> в <b>factories.yml</b> (в секцию param). Это изменение убирает зависимость <b>sfRequest</b> и <b>sfConfig</b>.
Эти три опции передаются в конструктор четвертым параметром. Форматы так же передаются как опция.
</p>
<div class="fullpost">
<p>
Константы класса sfRequest теперь имеют текстовое значение:
<table>
<thead>
<th>Константа</th>
<th>Старое значение</th>
<th>Новое значение</th>
</thead>
<tbody>
<tr>
<td>GET</td>
<td>2</td>
<td>GET</td>
</tr>
<tr>
<td>POST</td>
<td>4</td>
<td>POST</td>
</tr>
<tr>
<td>PUT</td>
<td>5</td>
<td>PUT</td>
</tr>
<tr>
<td>DELETE</td>
<td>6</td>
<td>DELETE</td>
</tr>
<tr>
<td>HEAD</td>
<td>7</td>
<td>HEAD</td>
</tr>
<tr>
<td>NONE</td>
<td>1</td>
<td>-</td>
</tr>
</tbody>
</table>
Методы <b>getMethod()</b> и <b>getMethodName()</b> теперь возвращают одинаковые значения. Метод <b>getMethodName()</b> - устарел.
<b>sfAction::getMethodNames()</b> и соответствующий код в <b>sfValidationExecutionFilter</b> из <b>sfCompat10Plugin</b> удалены.
</p>
<p>
<h2>Response</h2>
Добавилась новый параметр для <b>request</b> в <b>factory</b> - <b>send_http_headers</b>. По умолчанию эта настройка установлена в <b>true</b>, исключая <b>test</b> окружение. Это изменение удаляет зависимость <b>sfResponse</b> и <b>sfConfig</b>.
</p>
<p>
Ответ (response) имеет новый метод <b>getCharset()</b>, который возвращает текущую кодировку ответа. Возвращаемая кодировка, меняется если изменить кодировку через установку "content-type".
</p>
<p>
Методы <b>getStylesheets</b>, <b>getJavascripts</b> могут возвращать все файлы упорядочено, если передать <b>sfWebResponse::ALL</b> как первый аргумент.
<pre><code class="php">
$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(),
)
</code></pre>
<b>sfWebResponse::ALL</b> - это теперь значение по умолчанию. Добиться поведения как в symfony 1.1 можно передав в качестве параметра <b>sfResponse::RAW</b>
<pre><code class="php">
var_export($response->getStylesheets(sfWebResponse::RAW));
// outputs
array(
'first' =>
array(
'bar.css' => array (),
),
'' =>
array(
'foo.css' => array(),
),
'last' => array(),
)
</code></pre>
Все позиции (first, '', last) теперь также доступны как константы
<pre><code class="php">
sfWebResponse::FIRST === 'first'
sfWebResponse::MIDDLE === ''
sfWebResponse::LAST === 'last'
</code></pre>
Методы <b>removeStylesheet()</b> и <b>removeJavascript()</b> теперь принимают только один аргумент, имя файла, который надо удалить из ответа. Файл будет удален из всех доступных позиций.
</p>
</div>Sergeyhttp://www.blogger.com/profile/06129401613024492304noreply@blogger.com0tag:blogger.com,1999:blog-6222942299885796759.post-41768493469516869112008-10-11T02:09:00.010+04:002009-02-09T03:09:02.634+03:00Шаблоны ошибок и форматы запроса<p>
В symonfy 1.1 была введена поддержка различных форматов запроса.
Но там не доставало одного важного фрагмента: поддержки ошибок.
Благодаря огромной работе проделаной Kris Wallsmith в symfony 1.2 этот маленький недостаток уже исправлен.
Крис создал плагин реализующий поддержку ошибок в разных форматах. Позднее его плагин был включен в ядро symfony 1.2.
</p>
<div class="fullpost">
<p>
Что бы проще было объяснить как это работает, рассмотрим небольшой пример.
Допустим есть приложение с модулем API возвращающим HTML, XML или JSON представление статьи (модель Article).
Определим правила маршрутизации:
<pre><code>
// apps/frontend/config/routing.yml
api_article:
url: /api/article/:id.:sf_format
param: { module: api, action: atricle }
requirements:
sf_format: (?:html|xml|json)
</code></pre>
И соответствующее действие (action) в нашем модуле:
<pre><code class="php">
class apiActions extends sfActions
{
public function executeArticle($request)
{
$this->article = Doctrine::getTable('Article')->find((int)$request->getParameter('id'));
$this->forward404Unless($this->article);
}
}
</code></pre>
Теперь, если в запросе передать идентификатор не существующей статьи, запрос будет перенаправлен на страницу с 404 ошибкой. Если при этом используется <b>HTML</b> формат (http://localhost/frontend_dev.php/api/article/1.html) в <b>development</b> окружении вы получите сообщение об ошибке <b>sfError404Exception</b>.
<br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://www.symfony-project.org/uploads/assets/error_templates/1.png"><img style="cursor:pointer; cursor:hand;" src="http://www.symfony-project.org/uploads/assets/error_templates/1.png" border="0" alt="" /></a><br />
В <b>production</b> окружении будет показана страница с 404 ошибкой <b>"Oops! Page Not Found"</b>.
<br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://www.symfony-project.org/uploads/assets/error_templates/2.png"><img style="cursor:pointer; cursor:hand;" src="http://www.symfony-project.org/uploads/assets/error_templates/2.png" border="0" alt="" /></a><br />
Если изменить формат запроса на <b>XML</b>, то в <b>development</b> окружении получим ответ:
<br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://www.symfony-project.org/uploads/assets/error_templates/3.png"><img style="cursor:pointer; cursor:hand;" src="http://www.symfony-project.org/uploads/assets/error_templates/3.png" border="0" alt="" /></a><br />
и в <b>production</b> окружении просто:
<br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://www.symfony-project.org/uploads/assets/error_templates/4.png"><img style="cursor:pointer; cursor:hand;" src="http://www.symfony-project.org/uploads/assets/error_templates/4.png" border="0" alt="" /></a><br />
Т.е. ошибки теперь возвращаются в том же формате, что и запрос.
</p>
<p>
И это еще не все.
Сообщения об ошибках можно настраивать для каждого формата, добавляя шаблоны в директорию проекта <b>config/error/</b> или конкретного приложения <b>apps/frontend/config/error/</b>.
Например, для того чтобы настроить сообщения об ошибках для <b>XML</b> запросов можно создать шаблон <b>config/error/error.xml.php</b> примерно следующего содержания:
<pre><code class="xml">
<?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></pre>
Что даст примерно следующий вывод:
<br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://www.symfony-project.org/uploads/assets/error_templates/5.png"><img style="cursor:pointer; cursor:hand;" src="http://www.symfony-project.org/uploads/assets/error_templates/5.png" border="0" alt="" /></a><br />
</p>
<p>
В шаблонах ошибок доступны следующие переменные:
<ul>
<li>$code - код статуса ответа</li>
<li>$text - текст статуса ответа</li>
<li>$name - имя класса исключения</li>
<li>$message - сообщение исключения</li>
<li>$traces - массив содержащий полный PHP trace</li>
<li>$format - формат запроса</li>
</ul>
</p>
<p>
Но например в <b>development</b> окружении вместо страниц типа 404 ошибки отображаются страницы с выброшенным исключением. Как например при запросе не существующей статьи, отображается страница с исключением <b>sfError404Exception</b>.<br />
Эти страницы можно настраивать за счет создания шаблонов исключений, например <b>config/error/exception.xml.php</b>.<br />
Шаблоны исключений используемые по умолчанию хранятся в директории <b>lib/exception/data/</b> в дистрибутиве symfony.<br />
Для создания своего шаблона для какого-либо формата запроса, необходимо создать шаблон вида <b>config/error/exception.FORMAT_NAME.php</b>, где <b>FORMAT_NAME</b> - название формата, для которого создается шаблон.
В своих шаблонах, можно подгружать шаблоны symfony используя следующую конструкцию:
<pre><code class="php">
<?php include sfException::getTemplatePathForError('xml', true) ?>
</code></pre>
</p>
<p>
навеяно: <a href="http://www.symfony-project.org/blog/2008/10/10/new-in-symfony-1-2-error-templates-and-request-formats" target="_blank">New in symfony 1.2: Error Templates and Request Formats</a>
</p>
</div>Sergeyhttp://www.blogger.com/profile/06129401613024492304noreply@blogger.com0tag:blogger.com,1999:blog-6222942299885796759.post-40312298440754679682008-10-09T00:17:00.002+04:002009-02-09T03:09:29.563+03:00Новое в symfony 1.2, часть 2<p>
<h2>Действия (Action)</h2>
В действиях теперь можно генерировать URL используя объект маршрутизатора (routing object) благодаря новому методу <b>generateUrl</b>
<pre><code class='php'>
public function executeIndex()
{
$this->redirect($this->generateUrl('homepage'));
}
</code></pre>
Метод <b>generateUrl</b> в параметрах принимает имя маршрута, массив параметров и флаг генерировать ли абсолютный URL.
</p>
<div class="fullpost">
<p>
<h2>Формы (Forms)</h2>
Добавлены два новых метода облегчающих работу с формами в шаблонах.
Первый - <b>hasErrors</b> возвращает <b>true</b> если форма содержит какие-либо ошибки и <b>false</b> если ошибок нет. Этот метод так же возвращает <b>false</b> если форма не была привязана к данным. Это используется в шаблонах когда нужно вывести сообщение о том, что форма содержит ошибки.
<pre><code class='php'>
<?php if ($form->hasError()): ?>
Форма содержит ошибки, исправть пожалуйста.
<?php endif; ?>
</code></pre>
Второй метод <b>renderFormTag</b> генерирует открывающий тег для формы. Он так же добавляет <b>enctype</b> атрибут, если это необходимо и скрытый тег, если метод не <b>POST</b> и не <b>GET</b>
<pre><code class='php'>
<?php echo $form->renderFormTag('@article_update', array('method' => 'PUT')) ?>
</code></pre>
Если форма связана с <b>Propel</b> объектом метод <b>renderFormTag()</b> автоматически изменяет HTTP метод на <b>POST</b> для создания объекта и на <b>PUT</b> для редактирования объекта.
</p>
</div>Sergeyhttp://www.blogger.com/profile/06129401613024492304noreply@blogger.com0tag:blogger.com,1999:blog-6222942299885796759.post-78121197011969593362008-10-08T22:58:00.005+04:002008-10-08T23:15:34.423+04:00Новое в symfony 1.2, часть 1<p>
<h2>Имя приложения в CLI командах</h2>
Некоторые команды требуют в аргументах имя приложения, так как они используют соединение с базой данных. Но в некоторых из этих команд требовать имя приложения не логично. В symfony1.2 этот аргумент был заменен на опцию <b>--application</b>. Если не указать эту опцию, то symfony возьмет настройки для соединения с базой данных из файла config/databases.yml
Примеры таких команд
<pre><code class='php'>
#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
</code></pre>
</p>
<p>
<h2>Поддержка методов PUT и DELETE в браузере</h2>
Теперь можно имитировать поддержку методов <b>PUT</b> и <b>DELETE</b> браузером используя метод <b>POST</b> и добавив специальный скыртый (hidden) параметр <b>sf_method</b>
<pre><code class='html'>
<form action='#' method='POST'>
<input type='hidden' name='sf_method' value='PUT' />
<!-- // ... -->
</form>
</code></pre>
используя эту форму вызвав <b>sfRequest::getMethod</b> получим <b>PUT</b>
</p>
<p>
<h2>Улучшения в "response"</h2>
Иногда требуется получить файлы таблиц стилей (stylesheets) и файлы с javascript кодом текущего ответа. Но в версии 1.1 методы <b>getJavascripts</b> и <b>getStylesheets</b> возрващают внутрее представление данных, что в общем оказывается не тем что ожидалось. В symfony 1.2 данные возвращаются в упорядоченом, готовом к использованию виде.
<pre><code class='php'>
array
(
'bar.css' => array(),
'foo.css' => array()
);
</code></pre>
</p>
<p>
<h2>sfValidatorSchemaCompare валидатор</h2>
<b>sfValidatorSchemaCompare</b> стал немного удобнее в использовании
<pre><code class='php'>
// symfony 1.1 and 1.2
$v = new sfValidatorSchemaCompare('left', sfValidatorSchemaCompare::EQUAL, 'right');
// symfony 1.2 only
$v = new sfValidatorSchemaCompare('left', '==', 'right');
</code></pre>
</p>Sergeyhttp://www.blogger.com/profile/06129401613024492304noreply@blogger.com2tag:blogger.com,1999:blog-6222942299885796759.post-11906069024409659772008-07-31T00:42:00.013+04:002009-02-09T03:10:24.815+03:00Команды (Tasks). Создание<p>
Начиная с версии 1.1 в symfony каждая команда представляет из себя отдельный класс, в отличие от предыдущих версий, где каждая команда была представлена двумя функциями (описание и сама команда).
Команды разделены на пространства имен, что позволяет избегать конфликтов в именовании команд.
Команды можно наследовать и переопределять. При совпадении названий внутри одного пространства имен, команды перекрываются, такми образом команды установленных плагинов переопределяют команды ядра симфони, а команды проекта переопределяют команды плагинов и ядра симфони.
</p>
<div class="fullpost">
<p>
Для создания своей команды, можно воспользоваться генерацией каркаса команды.
Например создадим команду, которая выводит "Привет, Мир!" и назовем ее <b>"hello"</b>. Определим ее в пространстве имен <b>"example"</b>
</p>
<pre><code>
./symfony generate:task example:hello
</code></pre>
<p>
Теперь для того чтобы убедиться, что каркас был создан введем команду:
<pre><code>
./symfony list example
</code></pre>
и если мы на выходе получим примерно следующее:
<pre><code>
Available tasks for the "example" namespace:
:hello
</code></pre>
значит каркас был успешно создан и в директории <b>"lib/task"</b> можно найти файл <b>"exampleHelloTask.class.php"</b>.
</p>
<p>
В этом файле и создан класс-каркас нашей команды. В котором определены два необходимых метода <b>"configure"</b> и <b>"execute"</b>, которые конфигурируют и запускают команду соответственно.
<pre><code class="php">
class exampleHelloTask extends sfBaseTask
{
protected function configure()
{
// ...
}
protected function execute($arguments = array(), $options = array())
{
// ...
}
}
</code></pre>
</p>
<p>
По умолчанию, команда <b>"generate:task"</b> создаст каркас в директории <b>"lib/task"</b>, но можно указать путь, где создать каркас, например при создании своей команды в плагине:
<pre><code>
./symfony generate:task --dir=plugins/spExamplePlugin/lib/task example:hello
</code></pre>
</p>
<p>
В классе команды должны быть определены два метода, метод описания и конфигурации команды - <b>"configure"</b> и метод в котором выполняются действия команды - <b>"execute"</b>.
Метод <b>configure</b> для нашей команды может выглядеть примерно так:
<pre><code class="php">
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(); // массив синонимов
}
</code></pre>
Теперь можно выполнить команду:
<pre><code>
./symfony help example:hello
</code></pre>
и убедиться, что справочная информация выводится так как и было задумано.
</p>
<p>
Теперь добавим действий команде, дополнив код метода <b>"execute"</b>:
<pre><code class="php">
protected function execute($arguments = array(), $options = array())
{
$this->logSection("example", "Привет, Мир!");
}
</code></pre>
Метод <b>"logSection"</b> печатает отформатированное сообщение на стандартный вывод.
Запускаем:
<pre><code>
./symfony example:hello
</code></pre>
и получаем:
<pre><code>
>>example Привет, Мир!
</code></pre>
</p>
<p>
Зачастую команды должны быть конфигурируемы, например выполнять действия в указанном окружении, или для определенного приложения. А так же должны уметь принимать аргументы как имя автора в команде <b>"configure:author"</b> или название приложения в команде <b>"generate:app"</b>.<br />
Эти задачи реализуются за счет описания опций и аргументов в методе <b>"configure"</b>.<br />
В качестве примера добавим нашей команде способность привествовать не только мир, но и разработчика вызвавшего ее. Для этого добавим аргумент <b>"name"</b>, в котором будем принимать имя разработчика и немного изменим подробное описание команды, продемонстрировав еще одну возможность расскраски выводимого текста:
<pre><code class="php">
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, // тип аргумента
'Ваше имя, о величайший создатель', // справка
'Мир' // значение по умолчанию
)
}
</code></pre>
Если теперь выполнить команду:
<pre><code>
./symfony help example:hello
</code></pre>
то можно увидеть, что описание этого аргумента добавилось в вывод справки нашей команды.
Теперь изменим код команды:
<pre><code class="php">
protected function execute($arguments = array(), $options = array())
{
$this->logSection('example', 'Привет, '.$arguments['name'].'!');
}
</code></pre>
и запустим в двух вариантах:
<pre><code>
./symfony example:hello
>>example Привет, Мир!
./symfony example:hello Сергей
>>example Привет, Сергей!
</code></pre>
</p>
<p>
Рассмотрим подробнее описание аргумента.<br />
Для описания используется метод:
<pre><code class="php">
/**
* name - имя аргумента
* mode - свойства аргумента, возможные значения
* sfCommandArgument::OPTIONAL,
* sfCommandArgument::REQUIRED,
* sfCommandArgument::IS_ARRAY
* help - справка
* default - значение по умолчанию, нельзя указать при REQUIRED
*/
addArgument($name, $mode = null, $help = '', $default = null);
</code></pre>
Предназначение параметров вроде понятно из их названий, а вот о <b>"mode"</b> немного дополню.<br />
<b>sfCommandArgument::OPTIONAL</b> - говорит о том, что аргумент является не обязательным<br />
<b>sfCommandArgument::REQUIRED</b> - напртив, говорит о том, что аргумент является обязательным. При указании <b>sfCommandArgument::REQUIRED</b> нельзя задать значение по умолчанию для аргумента.<br />
<b>sfCommandArgument::IS_ARRAY</b> - говорит о том, что аргумент является массивом. В этом случае в методе <b>"execute"</b> он будет доступен именно как массив и в значении по умолчанию его тоже надо задавать в виде массива. При вызове команды из консоли массивы передаются так:
<pre><code>
./symfony example:hello {Сергей,Мир}
// преобразуется в массив array('Сергей', 'Мир');
./symfony example:hello Сергей Мир
// преобразуется в массив, если у аргумента указан IS_ARRAY
</code></pre>
<b>"mode"</b> можно задать и так, объединив свойства аргумента:
<pre><code class="php">
$this->addArgument
(
'hello',
sfCommandArgument::OPTIONAL | sfCommandArgument::IS_ARRAY
);
</code></pre>
Для добавления нескольких аргументов за раз, можно воспользоваться методом <b>"addArguments"</b>:
<pre><code class="php">
$this->addArguments
(
new sfCommandArgument
(
'hello',
sfCommandArgument::OPTIONAL,
'help',
'default'
),
new sfCommandArgument
(
'bye',
sfCommandArgument::OPTIONAL,
'help',
'default'
)
//, ...
);
</code></pre>
</p>
<p>
Опции команде можно добавить используя методы <b>"addOption"</b> или <b>"addOptions"</b>.
<pre><code class="php">
/**
* Описывает принимаемую опцию
*
* name - имя опции
* shortname - короткое имя опции
* mode - свойства параметра опции
* help - справка
* default - значение по умолчанию
*/
addOption($name, $shortname = null, $mode = null, $help = '', $default = null);
</code></pre>
</p>
<p>
Добавим нашей команде возможность указания способа привествия, для этого опишем опцию <b>"greeting"</b> в методе <b>"configure"</b>:
<pre><code class="php">
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'].'!');
}
</code></pre>
И запускаем в двух вариантах:
<pre><code>
./symfony example:hello -g Здравствуй
>> example Здравствуй, Мир!
./symfony example:hello --greeting=Здравствуй
>> example Здравствуй, Мир!
</code></pre>
Предназначение параметров понятны из названия и комментариев.<br />
<b>"mode"</b> может иметь следующие значения или их комбинации:<br />
<b>sfCommandOption::PARAMETER_NONE</b> - если опция не нуждается в параметре, т.е. например:
<pre><code>
./symfony task_name --debug
</code></pre>
в метод <b>"execute"</b> в <b>"$options['debug']"</b> будет передано либо <b>"true"</b> если опция указана, либо <b>"false"</b> если опция не указана.<br />
<b>sfCommandOption::PARAMETER_REQUIRED</b> - параметр у опции обязателен<br />
<b>sfCommandOption::PARAMETER_OPTIONAL</b> - параметр у опции не обязателен<br />
<b>sfCommandOption::IS_ARRAY</b> - параметр у опции массив, можно указать несколько раз:
<pre><code>
./symfony task_name --option='param1' --option='param2'
// преобразуется в array('param1', 'param2')
</code></pre>
Ну и вариант для добавления нескольких опций за раз:
<pre><code class="php">
protected function configure()
{
$this->addOptions
(
new sfCommandOption
(
'greeting',
'g',
sfCommandOption::PARAMETER_REQUIRED,
'help',
'Привет'
),
new sfCommandOption
(
'env'
)
//, ...
);
}
</code></pre>
</p>
<h2>Несколько полезных советов</h2>
<p>
<b>Вызов команды внутри другой команды.</b>
<pre><code class="php">
protected function execute($arguments = array(), $options = array())
{
$task = new myOtherTask($this->dispatcher, $this->formatter);
$task->run
(
$arguments = array
(
'foo' => 'bar'
),
$options = array
(
'bar' => 'foo'
)
);
}
</code></pre>
</p>
<p>
<b>Использование базы данных</b>
Если в команде используется подключение к базе данных, то необходимо в методе <b>"configure"</b> добавить аргумент <b>"appliction"</b> и опцию <b>"--env"</b> (опция не обязательна) для указания какие данные использовать для подключения к базе данных. После этого необходимо в методе <b>"execute"</b> выполнить инициализацию соединения с базой данных.
<pre><code class="php">
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();
// ...
}
</code></pre>
Можно и при создании каркаса команды указать опцию <b>"--use-database"</b>, но потом поправить некоторые значения если используется <b>"Doctrine"</b>, а в частности:<br />
В методе <b>"configure"</b> при описании опции <b>"connection"</b> изменить имя по умолчанию.<br />
В метод <b>"execute"</b>:
<pre><code class="php">
// заменить
$connection = Propel::getConnection($options['connection'] ? $options['connection'] : '');
// на
$connection = Doctrine_Manager::connection();
</code></pre>
</p>
</div>Sergeyhttp://www.blogger.com/profile/06129401613024492304noreply@blogger.com0