Замечания по переводу: ahudenko[at]yandex.ru
Вторая неделя Jobeet блестяще началась со знакомства с инструментами тестирования в Symfony. Сегодня мы займемся формами.
Фреймворк Форм
На любом сайте есть формы. От простых контактных до сложных комплексных с огромным количеством полей. Написание форм это еще одна из нелегких и скучных повседневных обязанностей веб-разработчика: написать HTML код формы, применить валидацию к каждому полю, обработать данные и сохранить их в базу, Вывести сообщения об ошибках, а так же повторно заполнить и отобразить форму.
Разумеется, чтобы не повторять одни и теже действия снова и снова, Symfony предоставляет инструментарий для легкой работы с формами. Фреймворк форм состоит из трех частей:
- validation: Валидация - это подфреймворк, которые предоставляет классы валидаторы для проверки входных данных (integer, string, email address, ...)
- widgets: Виджеты - это подфреймворк, который предоставляет классы для отображения полей формы в HTML(input, textarea, select, ...)
- forms: Формы - это классы, которые объединяют в себе виджеты и валидаторы и предоставляют методы для удобной работы с ними. Каждое поле в форме, имеет валидатор и виджет.
Формы
В Symfony класс формы состоит из полей. Каждое поле имеет имя, валидатор и
виджет. Простая контактная форма (ContactForm
) может быть представлена
следующим классом:
class ContactForm extends sfForm { public function configure() { $this->setWidgets(array( 'email' => new sfWidgetFormInputText(), 'message' => new sfWidgetFormTextarea(), )); $this->setValidators(array( 'email' => new sfValidatorEmail(), 'message' => new sfValidatorString(array('max_length' => 255)), )); } }
Поля формы настраиваются в методе configure()
при помощи методов
setValidators()
и setWidgets()
.
tip
В комплекте с фреймворком форм идет много виджетов и валидаторов. В документации по API подробно описаны все их параметры, ошибки, стандартные сообщения об ошибках.
Имена валидаторов и виджетов вполне очевидны: поле email
отобразится в HTML,
как тэг <input>
(sfWidgetFormInput
) и будет проверяться, как адрес
электронной почты (sfValidatorEmail
). Поле message
отобразится как тэг
<textarea>
(sfWidgetFormTextarea
) и будет проверяться как строка длиной не
более 255 символов (sfValidatorString
).
Изначально все поля формы обязательны, так как по умолчанию значение параметра
required
установлено в true
. Так определение валидатора для email
равносильно Вызову new sfValidatorEmail(array('required' => true))
.
tip
Вы можете объединить две формы, используя метод mergeForm()
, или вставить одну
форму в другую, используя метод embedForm()
:
$this->mergeForm(new AnotherForm()); $this->embedForm('name', new AnotherForm());
Doctrine формы
Данные форм постоянно сохраняются в базу данных. Symfony знает все о вашей
модели и может автоматически создавать формы, осноВываясь на этой информации.
Например, когда Вы на третий день Выполняли команду doctrine:build --all
, Symfony
Вызывал команду doctrine:build --forms
:
$ php symfony doctrine:build --forms
Команда doctrine:build --forms
создает классы для форм и сохраняет их в директории
lib/form/
. Организация сгенерированных файлов похожа на lib/model/
. Каждый
класс модели связан с классом форм (например JobeetJob
и JobeetJobForm
),
по умолчанию они наследуют базоВый класс форм и не содержат собственных методов:
// lib/form/doctrine/JobeetJobForm.class.php class JobeetJobForm extends BaseJobeetJobForm { public function configure() { } }
tip
Просматривая файлы в lib/form/doctrine/base/
, Вы можете найти множество
прекрасных примеров использования встроенных в Symfony виджетов и валидаторов
TIP
Вы можете отменить генерацию формы для конкретной модели, указав соответствующие параметры
в опции symfony
:
SomeModel: options: symfony: form: false filter: false
Изменение формы вакансии
Форма вакансии прекрасный образец для изучения работы с формами. Рассмотрим ее изменения шаг за шагом.
Во первых, изменим ссылку "Post a job" в лэйауте, так чтобы можно было видеть все изменения в браузере:
<!-- apps/frontend/templates/layout.php -->
<a href="<?php echo url_for('@job_new') ?>">Post a Job</a>
По умолчанию, Doctrine отображает поля для всех столбцов таблицы. Но для формы вакансии некоторые из них не должны быть доступны для редактирования конечному пользователю. Удалим эти поля из формы, просто исключив их:
// lib/form/doctrine/JobeetJobForm.class.php class JobeetJobForm extends BaseJobeetJobForm { public function configure() { unset( $this['created_at'], $this['updated_at'], $this['expires_at'], $this['is_activated'] ); } }
Исключение поля подразумевает, что соответствующие виджет и валидатор, будут удалены.
Вместо удаления полей, который вы не хотите отображать, Вы также можете
явно перечислить поля, который нужны на форме, используя метод useFields()
:
// lib/form/doctrine/JobeetJobForm.class.php class JobeetJobForm extends BaseJobeetJobForm { public function configure() { $this->useFields(array('category_id', 'type', 'company', 'logo', 'url', 'position', 'location', 'description', 'how_to_apply', 'token', 'is_public', 'email')); } }
Метод useFields()
делает для Вас автоматически две вещи: добавляет скрытые
и массив полей, используемый для изменения порядка их отображения.
tip
Явно указание полей, которые Вы хотите отображать, приводит к тому, что при добавлении новых полей в базовую форму, они не будут автоматически появляться на Вашей форме (помните о форме для модели, когда добавляете новый столбец в соответствующую таблицу базы данных).
Порой форму необходимо настроить более точно, чем просто унаследовать свойства
базы данных. Например, столбец email
это varchar
в базе, а нам нужно
валидировать его как адрес электронной почты. Давайте изменим, стоящий по
умолчанию, sfValidatorString
на sfValidatorEmail
:
// lib/form/doctrine/JobeetJobForm.class.php public function configure() { // ... $this->validatorSchema['email'] = new sfValidatorEmail(); }
Замена стандартного валидатора не всегда лучшее решение, так как при этой
операции теряются правила унаследованные от
базы(new sfValidatorString(array('max_length' => 255))
). Почти всегда
лучше добавить ноВый валидатор к уже существующему используя,
специальный валидатор sfValidatorAnd
:
// lib/form/doctrine/JobeetJobForm.class.php public function configure() { // ... $this->validatorSchema['email'] = new sfValidatorAnd(array( $this->validatorSchema['email'], new sfValidatorEmail(), )); }
Валидатор sfValidatorAnd
принимает в качестве параметра массив валидаторов,
которые будут добавлены для проверки значения. Трюк в том, что мы передаем
ссылку на текущий валидатор, и добавляем один ноВый.
note
Так же Вы можете использовать валидатор sfValidatorOr
, в таком случае
значение будет валидным если провалидируется хотя бы одним из валидаторов.
Разумеется, Вы можете объединять валидаторы sfValidatorAnd
и sfValidatorOr
для создания валидаторов со сложной логикой.
Хотя столбец type
у нас в базе тоже varchar
, мы хотим ограничить его
значение, заранее определенным списком: full time, part time или freelance.
Во перВых, давайте определим возможные значения в JobeetJobTable
:
// lib/model/doctrine/JobeetJobTable.class.php class JobeetJobTable extends Doctrine_Table { static public $types = array( 'full-time' => 'Full time', 'part-time' => 'Part time', 'freelance' => 'Freelance', ); public function getTypes() { return self::$types; } // ... }
Далее, для поля type
используем виджет sfWidgetFormChoice
:
$this->widgetSchema['type'] = new sfWidgetFormChoice(array( 'choices' => Doctrine_Core::getTable('JobeetJob')->getTypes(), 'expanded' => true, ));
Виджет sfWidgetFormChoice
реализует виджет Выбора значений, он может по
разному отображаться в зависимости от конфигурации (expanded
и
multiple
):
- Dropdown list (
<select>
):array('multiple' => false, 'expanded' => false)
- Dropdown box (
<select multiple="multiple">
):array('multiple' => true, 'expanded' => false)
- List of radio buttons:
array('multiple' => false, 'expanded' => true)
- List of checkboxes:
array('multiple' => true, 'expanded' => true)
note
Если Вы хотите сделать чтобы какое-то значение было Выбрано по
умолчанию(на пример full-time
), измените значение по умолчанию для этого
поля в базе данных.
Хотя Вы и думаете, что теперь никто не сможет отправить некорректные данные, взломщик с легкостью может обойти виджет, используя например curl или Firefox Web Developer Toolbar. Давайте изменим валидатор и ограничим допустимые значения:
$this->validatorSchema['type'] = new sfValidatorChoice(array( 'choices' => array_keys(Doctrine_Core::getTable('JobeetJob')->getTypes()), ));
В поле logo
будет хранится имя файла логотипа, относящегося к этой вакансии,
нам нужно изменить виджет на тэг file input:
$this->widgetSchema['logo'] = new sfWidgetFormInputFile(array( 'label' => 'Company logo', ));
Для каждого поля формы Symfony автоматически генерирует
label(который используется при отображении тэга label
). Его
можно изменить используя метод setLabels()
:
$this->widgetSchema->setLabels(array( 'category_id' => 'Category', 'is_public' => 'Public?', 'how_to_apply' => 'How to apply?', ));
Еще нам нужно изменить стандартный валидатор:
$this->validatorSchema['logo'] = new sfValidatorFile(array( 'required' => false, 'path' => sfConfig::get('sf_upload_dir').'/jobs', 'mime_types' => 'web_images', ));
sfValidatorFile
довольно интересен, что же он делает:
- Удостоверяется, что загруженный файл это изображение в формате (
mime_types
), пригодном для веб - Дает файлу уникальное имя
- Сохраняет файл по указанному пути (
path
) - Обновляет в базе поле
logo
с новым сгенерированным именем
note
Вы должны создать директорию для хранения логотипов(web/uploads/jobs/
) и
удостовериться, что она доступна для записи пользователю, от имени которого
запущен веб-сервер.
Измените путь к логотипу, используемый в шаблоне showSuccess
, так как
валидатор сохраняет относительный путь в базу данных:
// apps/frontend/modules/job/templates/showSuccess.php <img src="/uploads/jobs/<?php echo $job->getLogo() ?>" alt="<?php echo $job->getCompany() ?> logo" />
tip
Если в модели определен метод generateLogoFilename()
, он будет Вызван
валидатором и результат его Выполнения заменит сгенерированное по умолчанию
имя файла для logo
. Метод принимает в качестве аргумента объект sfValidatedFile
.
Вы можете определить пояснение для поля формы, так же легко, как заменить
стандартный label. Давайте добавим сообщение к полю is_public
чтобы лучше
раскрыть его значение:
$this->widgetSchema->setHelp('is_public', 'Whether the job can also be published on affiliate websites or not.');
В итоге класс JobeetJobForm
Выглядит следующим образом:
// lib/form/doctrine/JobeetJobForm.class.php class JobeetJobForm extends BaseJobeetJobForm { public function configure() { unset( $this['created_at'], $this['updated_at'], $this['expires_at'], $this['is_activated'] ); $this->validatorSchema['email'] = new sfValidatorAnd(array( $this->validatorSchema['email'], new sfValidatorEmail(), )); $this->widgetSchema['type'] = new sfWidgetFormChoice(array( 'choices' => Doctrine_Core::getTable('JobeetJob')->getTypes(), 'expanded' => true, )); $this->validatorSchema['type'] = new sfValidatorChoice(array( 'choices' => array_keys(Doctrine_Core::getTable('JobeetJob')->getTypes()), )); $this->widgetSchema['logo'] = new sfWidgetFormInputFile(array( 'label' => 'Company logo', )); $this->widgetSchema->setLabels(array( 'category_id' => 'Category', 'is_public' => 'Public?', 'how_to_apply' => 'How to apply?', )); $this->validatorSchema['logo'] = new sfValidatorFile(array( 'required' => false, 'path' => sfConfig::get('sf_upload_dir').'/jobs', 'mime_types' => 'web_images', )); $this->widgetSchema->setHelp('is_public', 'Whether the job can also be published on affiliate websites or not.'); } }
Шаблон формы
Теперь, когда класс формы настроен, нам нужно отобразить его. Шаблон формы для
job используется, когда Вы хотите создать новую вакансию или отредактировать
существующую. Кстати, шаблоны newSuccess.php
и editSuccess.php
очень
похожи:
<!-- apps/frontend/modules/job/templates/newSuccess.php --> <?php use_stylesheet('job.css') ?> <h1>Post a Job</h1> <?php include_partial('form', array('form' => $form)) ?>
note
Если Вы еще не добавили файл стилей job
, то самое время добавить его в
оба шаблона(<?php use_stylesheet('job.css') ?>
).
Форма отображает сама себя в фрагменте шаблона(partial) _form
. Замените
содержимое сгенерированного фрагмента _form
следующим кодом:
<!-- apps/frontend/modules/job/templates/_form.php --> <?php use_stylesheets_for_form($form) ?> <?php use_javascripts_for_form($form) ?> <?php echo form_tag_for($form, '@job') ?> <table id="job_form"> <tfoot> <tr> <td colspan="2"> <input type="submit" value="Preview your job" /> </td> </tr> </tfoot> <tbody> <?php echo $form ?> </tbody> </table> </form>
Хелперы use_javascripts_for_form() и use_stylesheets_for_form() подключают соответственно JavaScript и CSS файлы необходимые для виджета формы.
tip
Даже если для формы вакансии не нужны JavaScript или CSS дополнения, будет не плохо оставить их "на всякий случай". Это сэкономит Вам время, если когда-нибудь решите изменить форму и добавить немного JavaScript или стилей.
Хелпер form_tag_for
генерирует для передаваемой формы тэг <form>
, роутинг,
устанавливает метод POST
или PUT
в зависимости от того, ноВый это объект или нет. Также хелпер позаботится об
атрибуте multipart
, который будет добавлен если
содержится хотя бы один тэгinput file
.
В конце концов <?php echo $form ?>
рендерит виджет формы.
Действия формы
И так у нас есть класс формы и шаблон, который отображает ее. Самое время сделать чтобы все это работало с некоторыми действиями.
Форма вакансии используется в пяти методах модуля job
:
- new: Отображает пустую форму при создании вакансии
- edit: Отображает форму для редактирования существующей вакансии
- create: Создает вакансии с данными, полученными от пользователя
- update: Обновляет вакансию с данными, полученными от пользователя
- processForm: Вызывается методами
create
иupdate
(осуществляет валидацию, заполнение полей, сериализацию формы в базу)
Все формы имеют следующий жизненный цикл:
Так как 5 дней назад мы создали набор Doctrine роутингов для модуля job
, мы
можем упростить код для методов, использующих форму:
// apps/frontend/modules/job/actions/actions.class.php public function executeNew(sfWebRequest $request) { $this->form = new JobeetJobForm(); } public function executeCreate(sfWebRequest $request) { $this->form = new JobeetJobForm(); $this->processForm($request, $this->form); $this->setTemplate('new'); } public function executeEdit(sfWebRequest $request) { $this->form = new JobeetJobForm($this->getRoute()->getObject()); } public function executeUpdate(sfWebRequest $request) { $this->form = new JobeetJobForm($this->getRoute()->getObject()); $this->processForm($request, $this->form); $this->setTemplate('edit'); } public function executeDelete(sfWebRequest $request) { $request->checkCSRFProtection(); $job = $this->getRoute()->getObject(); $job->delete(); $this->redirect('job/index'); } protected function processForm(sfWebRequest $request, sfForm $form) { $form->bind( $request->getParameter($form->getName()), $request->getFiles($form->getName()) ); if ($form->isValid()) { $job = $form->save(); $this->redirect('job_show', $job); } }
Когда Вы открываете страницу /job/new
, создается ноВый объект формы и
передается в шаблон (new
action).
Когда пользователь отправляет данные формы (create
action), форма связывается
(метод bind()
) с параметрами запроса и Вызывается валидация.
Когда форма связана, можно проверить ее валидность Вызвав метод isValid()
.
Если данные формы валидны, то вакансия сохраняется в базу данных($form->save()
),
а пользователь перенаправляется на страницу только что созданной вакансии.
Если нет, повторно отображается шаблон newSuccess.php
со всеми соответствующими
сообщениями об ошибках.
tip
Метод setTemplate()
изменяет html шаблон для текущего действия. Если форма
не валидна, то методы create
и update
используют те же шаблоны что и методы
new
и edit
соответственно, для повторного отображения формы с сообщениями
об ошибках.
Изменение существующей вакансии очень похоже. Единственное различие между
действиями new
и edit
это то, что в конструктор формы перВым параметром
передается объект редактируемой вакансии. Этот объект будет использован для
заполнения значений виджетов формы(значения по умолчанию представляются в
виде объектов для Doctrine форм, и в виде обычных массивов для простых форм).
Вы также можете задачть значения по умолчанию для формы создания объекта. Можно
определить значения в базе данных, или передавать в конструктор формы заранее
приготовленный объект Job
.
Изменим метод executeNew()
, установим для поля type
значение по умолчанию
full-time
:
// apps/frontend/modules/job/actions/actions.class.php public function executeNew(sfWebRequest $request) { $job = new JobeetJob(); $job->setType('full-time'); $this->form = new JobeetJobForm($job); }
note
Когда форма связана, значения по умолчанию заменяются введенными пользователем значениями. Пользовательские данные используются для повторного заполнения формы, в случае обнаружения ошибок.
Защита формы при помощи токена
Теперь все должно работать хорошо. Теперь пользователь должен ввести уникальный токен (уникальный маркер, метка) для вакансии. Но токен должен быть сгенерирован когда вакансия создается, так как мы не можем быть уверены, что пользователь введет уникальный токен.
Обновим метод save()
в классе JobeetJob
, добавим логику для генерации
токена перед тем как вакансия будет сохранена:
// lib/model/doctrine/JobeetJob.class.php public function save(Doctrine_Connection $conn = null) { // ... if (!$this->getToken()) { $this->setToken(sha1($this->getEmail().rand(11111, 99999))); } return parent::save($conn); }
Вы можете удалить поле токена из формы:
// lib/form/doctrine/JobeetJobForm.class.php class JobeetJobForm extends BaseJobeetJobForm { public function configure() { unset( $this['created_at'], $this['updated_at'], $this['expires_at'], $this['is_activated'], $this['token'] ); // ... } // ... }
Возможно Вы помните пользовательский сценарий из второго дня, вакансия может
быть отредактирована пользователем, только если он знает связанный с ней токен.
Сейчас легко можно отредактировать или удалить вакансию просто угадав ее URL.
Это происходит потому, что URL редактирования Выглядит как /job/ID/edit
, где
ID
это первичный ключ вакансии.
По умолчанию роутинг sfDoctrineRouteCollection
генерирует URL'ы с первичным
ключом, но его можно заменить на любое уникальное поле, передав параметр
column
:
# apps/frontend/config/~routing|Routing~.yml job: class: sfDoctrineRouteCollection options: { model: JobeetJob, column: token } requirements: { token: \w+ }
Обратите внимание, что параметру token
соответствует любая строка \w+
,
так как в Symfony по умолчанию уникальному ключу соответствует число \d+
.
Теперь все роутинги на вакансии, исключая job_show_user
, содержат токен. На
пример, роутинг для редактирования вакансии теперь соответствует следующему
шаблону:
http://www.jobeet.com.localhost/job/TOKEN/edit
Теперь нам нужно будет изменить ссылку на редактирование в шаблоне
showSuccess
:
<!-- apps/frontend/modules/job/templates/showSuccess.php -->
<a href="<?php echo url_for('job_edit', $job) ?>">Edit</a>
Страница просмотра
Страница просмотра - это страница на которой отображается вакансия. Благодаря роутингу, если пользователь приходит с правильным токеном, токен будет содержаться в параметрах запроса.
Если пользователь приходит по URL с токеном, то мы будем добавлять вверху
админ-панель. В начале шаблона showSuccess
добавим фрагмент(partical)
админ-панели и удалим ссылку edit
в конце страницы:
<!-- apps/frontend/modules/job/templates/showSuccess.php --> <?php if ($sf_request->getParameter('token') == $job->getToken()): ?> <?php include_partial('job/admin', array('job' => $job)) ?> <?php endif ?>
Затем создадим фрагмент _admin
:
<!-- apps/frontend/modules/job/templates/_admin.php --> <div id="job_actions"> <h3>Admin</h3> <ul> <?php if (!$job->getIsActivated()): ?> <li><?php echo link_to('Edit', 'job_edit', $job) ?></li> <li><?php echo link_to('Publish', 'job_edit', $job) ?></li> <?php endif ?> <li><?php echo link_to('Delete', 'job_delete', $job, array('method' => 'delete', 'confirm' => 'Are you sure?')) ?></li> <?php if ($job->getIsActivated()): ?> <li<?php $job->expiresSoon() and print ' class="expires_soon"' ?>> <?php if ($job->isExpired()): ?> Expired <?php else: ?> Expires in <strong><?php echo $job->getDaysBeforeExpires() ?></strong> days <?php endif ?> <?php if ($job->expiresSoon()): ?> - <a href="">Extend</a> for another <?php echo sfConfig::get('app_active_days') ?> days <?php endif ?> </li> <?php else: ?> <li> [Bookmark this <?php echo link_to('URL', 'job_show', $job, true) ?> to manage this job in the future.] </li> <?php endif ?> </ul> </div>
Кода получилось много, но большая часть из него вполне понятна.
Для того чтобы сделать шаблон более читабельным, мы добавили несколько методов
в класс JobeetJob
:
// lib/model/doctrine/JobeetJob.class.php public function getTypeName() { $types = Doctrine_Core::getTable('JobeetJob')->getTypes(); return $this->getType() ? $types[$this->getType()] : ''; } public function isExpired() { return $this->getDaysBeforeExpires() < 0; } public function expiresSoon() { return $this->getDaysBeforeExpires() < 5; } public function getDaysBeforeExpires() { return ceil(($this->getDateTimeObject('expires_at')->format('U') - time()) / 86400); }
Админ-панель отображает различные действия доступные для вакансии в зависимости от ее статуса:
note
В следующем разделе Вы увидите админ-панель для "activated" вакансии.
Активация и публикация вакансии
В предыдущем разделе есть ссылка для публикации вакансии. Ссылка должна
Вызывать действие publish
. Вместо того чтобы создавать ноВый роутинг для
этого, мы просто изменим существующий роутинг для вакансии:
# apps/frontend/config/routing.yml job: class: sfDoctrineRouteCollection options: model: JobeetJob column: token object_actions: { publish: put } requirements: token: \w+
Параметр object_actions
представляет собой массив дополнительных действий
для данного объекта. Теперь мы можем изменить ссылку на публикацию вакансии
("Publish"):
<!-- apps/frontend/modules/job/templates/_admin.php --> <li> <?php echo link_to('Publish', 'job_publish', $job, array('method' => 'put')) ?> </li>
Последним шагом создадим действие publish
:
// apps/frontend/modules/job/actions/actions.class.php public function executePublish(sfWebRequest $request) { $request->checkCSRFProtection(); $job = $this->getRoute()->getObject(); $job->publish(); $this->getUser()->setFlash('notice', sprintf('Your job is now online for %s days.', sfConfig::get('app_active_days'))); $this->redirect('job_show_user', $job); }
Внимательный читатель мог заметить, что ссылка "Publish" срабатывает с HTTP методом PUT. Для того чтобы сэмулировать PUT метод, ссылка, когда Вы кликаете по ней, автоматически конвертируется в форму.
Поскольку у нас включена защита от CSRF, хелпер link_to()
включает CSRF токен
в ссылку и метод checkCSRFProtection()
объекта запроса проверяет валидность
приходящих данных.
Метод executePublish()
использует ноВый метод publish()
, который может быть
определен так:
// lib/model/doctrine/JobeetJob.class.php public function publish() { $this->setIsActivated(true); $this->save(); }
Теперь Вы можете протестировать в браузере ноВый функционал - публикация.
Все еще есть что исправить. Неактивированные вакансии должны быть недоступными.
Подразумевается, что они не отображаются на главной странице Jobeet и не
открываются по соответствующим им URL'ам. Так как у нас уже есть метод
addActiveJobsQuery()
для ограничения Doctrine_Query
по активным вакансиям,
мы должны просто добавить в него новое условие:
// lib/model/doctrine/JobeetJobTable.class.php public function addActiveJobsQuery(Doctrine_Query $q = null) { // ... $q->andWhere($alias . '.is_activated = ?', 1); return $q; }
Это все. Теперь можете протестировать это в браузере. Все неактивированные вакансии исключены с главной страницы; даже если Вы знаете прямые ссылки на вакансии, Вы все равно не сможете просмотреть их. Не смотря на это, Вы можете получить к ним доступ, зная токен. В это случае вакансия будет отображаться с админ-панелью.
Мы использовали еще одно преимущество патерна MVC и рефакторинга. Единственное изменение одного метода было необходимо для добавления нового условия.
note
Когда мы создавали метод getWithJobs()
, мы не использовали
addActiveJobsQuery()
. Нужно исправить это и добавить новое условие:
class JobeetCategoryTable extends Doctrine_Table { public function getWithJobs() { // ... $q->andWhere('j.is_activated = ?', 1); return $q->execute(); }
Увидимся завтра
В сегодняшнем руководстве содержится слишком много новой информации, но она полезна для лучшего понимания работы с формами в Symfony.
Я знаю, некоторые из Вас заметили, что мы кое-что забыли сегодня... Мы не создали ни одного теста для нового функционала. Так как написание тестов очень важный момент в разработке приложения, это будет первое, чем мы займемся завтра.
This work is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.