Прежде, чем начнем
Нам необходимо сделать маленькое изменение в схеме JobAffiliate
, для того,
чтобы определить отношение многие ко многим с таблицей JobeetCategory
.
Вы можете посмотреть всю схему в главе "День 3" или просто посмотрите, что нужно добавить:
JobeetAffiliate: # ... relations: JobeetCategories: class: JobeetCategory refClass: JobeetCategoryAffiliate local: affiliate_id foreign: category_id foreignAlias: JobeetAffiliates
Не забудьте перестроить модели после внесения изменений:
$ php symfony doctrine:build-model
С появлением новостных лент (feeds) на сайте Jobeet, клиенты могут быть информированы о новых вакансиях в реальном времени.
С другой стороны, когда Вы добавляете новую вакансию, Вы хотите чтобы она распространилась как можно шире. Если Ваше предложение о вакансии будет упоминаться на множестве некрупных сайтов, шанс, что Вы найдете нужного Вам человека увеличится. В этом сила Long Tail. Благодаря веб-сервисам, которые мы разработаем сегодня, партнеры получат возможность размещать последние вакансии на своих сайтах.
Партнеры
Напомним требования из урока 2:
"История F7: Партнер получает текущий список активных вакансий"
Начальные данные (fixtures)
Давайте создадим новый файл с начальными данными для партнеров:
# data/fixtures/affiliates.yml JobeetAffiliate: sensio_labs: url: http://www.sensio-labs.com/ email: fabien.potencier@example.com is_active: true token: sensio_labs JobeetCategories: [programming] symfony: url: / email: fabien.potencier@example.org is_active: false token: symfony JobeetCategories: [design, programming]
Добавить записи для отношения многие ко многим легко. Просто объявите массив, ключом которого будет название самой связи. В массиве содержатся имена объектов, которые описаны в файле с начальными данными (fixtures). Вы можете связывать объекты из разных файлов, но эти имена должны быть объявлены ранее. В файле с начальными данными, токены прописаны вручную, чтобы упростить тестирование, но когда пользователь запрашивает аккаунт, токен должен быть сгенерирован.
// lib/model/doctrine/JobeetAffiliate.php class JobeetAffiliate extends BaseJobeetAffiliate { public function save(Doctrine_Connection $conn = null) { if (!$this->getToken()) { $this->setToken(sha1($this->getEmail().rand(11111, 99999))); } return parent::save($conn); } // ... }
Теперь вы можете загрузить начальные данные:
$ php symfony doctrine:data-load
Веб-сервис вакансий
Как всегда, первым делом, когда Вы создаете новый ресурс, хорошей привычкой служит определение URL в начале:
# apps/frontend/config/routing.yml api_jobs: url: /api/:token/jobs.:sf_format class: sfDoctrineRoute param: { module: api, action: list } options: { model: JobeetJob, type: list, method: getForToken } requirements: sf_format: (?:xml|json|yaml)
Для этого маршрута, указанна специальная переменная sf_format
, которая находится
в конце URL, и соответствующие ей значения будут xml
, json
или yaml
.
Метод getForToken()
вызывается, когда действие запрашивает коллекцию объектов,
связанных с маршрутом. Так как нам надо удостовериться, что партнер активирован,
мы переопределим поведение маршрута по умолчанию:
// lib/model/doctrine/JobeetJobTable.class.php class JobeetJobTable extends Doctrine_Table { public function getForToken(array $parameters) { $affiliate = Doctrine_Core::getTable('JobeetAffiliate') ->findOneByToken($parameters['token']); if (!$affiliate || !$affiliate->getIsActive()) { throw new sfError404Exception(sprintf('Affiliate with token "%s" does not exist or is not activated.', $parameters['token'])); } return $affiliate->getActiveJobs(); } // ... }
Если токен не существует в базе данных, мы генерируем исключение
sfError404Exception
. Этот класс исключений затем автоматически преобразуется в
Ошибку 404. Это простейший способ сгенерировать Ошибку 404 из модели.
Метод getForToken()
использует новый метод getActiveJobs()
и возвращает список
текущих активных вакансий.
// lib/model/doctrine/JobeetAffiliate.class.php class JobeetAffiliate extends BaseJobeetAffiliate { public function getActiveJobs() { $q = Doctrine_Query::create() ->select('j.*') ->from('JobeetJob j') ->leftJoin('j.JobeetCategory c') ->leftJoin('c.JobeetAffiliates a') ->where('a.id = ?', $this->getId()); $q = Doctrine_Core::getTable('JobeetJob')->addActiveJobsQuery($q); return $q->execute(); } // ... }
Последним шагом является создание действия api
и шаблонов. Создайте модуль
задачей generate:module
:
$ php symfony generate:module frontend api
note
Так как мы не будем использовать действие index
, вы можете его убрать из
контроллера. Также удалите шаблон indexSuccess.php
.
Действие
Все форматы используют одно и то же действие list
:
// apps/frontend/modules/api/actions/actions.class.php public function executeList(sfWebRequest $request) { $this->jobs = array(); foreach ($this->getRoute()->getObjects() as $job) { $this->jobs[$this->generateUrl('job_show_user', $job, true)] = $job->asArray($request->getHost()); } }
Вместо того, чтобы передавать массив объектов JobeetJob
, мы передаем массив строк.
Так как у нас есть три разных шаблона для одного действия, логика обработки значений
была вынесена в метод JobeetJob::asArray()
:
// lib/model/doctrine/JobeetJob.class.php class JobeetJob extends BaseJobeetJob { public function asArray($host) { return array( 'category' => $this->getJobeetCategory()->getName(), 'type' => $this->getType(), 'company' => $this->getCompany(), 'logo' => $this->getLogo() ? 'http://'.$host.'/uploads/jobs/'.$this->getLogo() : null, 'url' => $this->getUrl(), 'position' => $this->getPosition(), 'location' => $this->getLocation(), 'description' => $this->getDescription(), 'how_to_apply' => $this->getHowToApply(), 'expires_at' => $this->getCreatedAt(), ); } // ... }
Формат xml
Добавление поддержки формата xml
просто настолько же, насколько просто создание шаблона:
<!-- apps/frontend/modules/api/templates/listSuccess.xml.php --> <?xml version="1.0" encoding="utf-8"?> <jobs> <?php foreach ($jobs as $url => $job): ?> <job url="<?php echo $url ?>"> <?php foreach ($job as $key => $value): ?> <<?php echo $key ?>><?php echo $value ?></<?php echo $key ?>> <?php endforeach ?> </job> <?php endforeach ?> </jobs>
Формат json
Поддержка формата JSON:
<!-- apps/frontend/modules/api/templates/listSuccess.json.php --> [ <?php $nb = count($jobs); $i = 0; foreach ($jobs as $url => $job): ++$i ?> { "url": "<?php echo $url ?>", <?php $nb1 = count($job); $j = 0; foreach ($job as $key => $value): ++$j ?> "<?php echo $key ?>": <?php echo json_encode($value).($nb1 == $j ? '' : ',') ?> <?php endforeach ?> }<?php echo $nb == $i ? '' : ',' ?> <?php endforeach ?> ]
Формат yaml
Для встроеных форматов некоторые настройки, Symfony выполняет неявно, такие как изменение типа содержимого, или отключение декоратора (layout).
Поскольку YAML формат не находится в этом списке, необходимо изменить тип содержимого в ответе и отключить layout:
class apiActions extends sfActions { public function executeList(sfWebRequest $request) { $this->jobs = array(); foreach ($this->getRoute()->getObjects() as $job) { $this->jobs[$this->generateUrl('job_show_user', $job, true)] = $job->asArray($request->getHost()); } switch ($request->getRequestFormat()) { case 'yaml': $this->setLayout(false); $this->getResponse()->setContentType('text/yaml'); break; } } }
В действии, метод setLayout()
изменяет layout по умолчанию, или отключает его,
параметром false
.
Шаблон для YAML:
<!-- apps/frontend/modules/api/templates/listSuccess.yaml.php --> <?php foreach ($jobs as $url => $job): ?> - url: <?php echo $url ?> <?php foreach ($job as $key => $value): ?> <?php echo $key ?>: <?php echo sfYaml::dump($value) ?> <?php endforeach ?> <?php endforeach ?>
Если Вы попытаетесь вызвать веб-сервис, указав неверный токен, вы получите xml страницу с ошибкой 404, а также страницу 404 json, для json формата. Но Symfony не знает, что отображать для YAML формата.
Каждый раз, когда Вы создаете формат, должен быть создан пользовательский шаблон сообщения об ошибке. Этот шаблон будет использован для страниц ошибки 404 и других исключений.
Так как обработка исключений должна быть разной в промышленной среде и в среде разработки,
нужно создать два файла (config/error/exception.yaml.php
для среды разработки, and config/error/error.yaml.php
для промышленной среды):
// config/error/exception.yaml.php <?php echo sfYaml::dump(array( 'error' => array( 'code' => $code, 'message' => $message, 'debug' => array( 'name' => $name, 'message' => $message, 'traces' => $traces, ), )), 4) ?> // config/error/error.yaml.php <?php echo sfYaml::dump(array( 'error' => array( 'code' => $code, 'message' => $message, ))) ?>
Перед тем, как испробовать это, Вы должны создать layout для формата YAML:
// apps/frontend/templates/layout.yaml.php <?php echo $sf_content ?>
tip
Переопределение ошибки 404 и шаблонов обработки исключений для встроенных шаблонов
так же просто, как просто создание файла в папке config/error/
Тестирование веб-сервисов
Для тестирования веб-сервисов, скопируйте соответствующий файл данных из data/fixtures/
в test/fixtures/
и замените содержание файла apiActionsTest.php
, который был
сгенерирован автоматически, следующим кодом:
// test/functional/frontend/apiActionsTest.php include(dirname(__FILE__).'/../../bootstrap/functional.php'); $browser = new JobeetTestFunctional(new sfBrowser()); $browser->loadData(); $browser-> info('1 - Web service security')-> info(' 1.1 - A token is needed to access the service')-> get('/api/foo/jobs.xml')-> with('response')->isStatusCode(404)-> info(' 1.2 - An inactive account cannot access the web service')-> get('/api/symfony/jobs.xml')-> with('response')->isStatusCode(404)-> info('2 - The jobs returned are limited to the categories configured for the affiliate')-> get('/api/sensio_labs/jobs.xml')-> with('request')->isFormat('xml')-> with('response')->begin()-> isValid()-> checkElement('job', 32)-> end()-> info('3 - The web service supports the JSON format')-> get('/api/sensio_labs/jobs.json')-> with('request')->isFormat('json')-> with('response')->matches('/"category"\: "Programming"/')-> info('4 - The web service supports the YAML format')-> get('/api/sensio_labs/jobs.yaml')-> with('response')->begin()-> isHeader('content-type', 'text/yaml; charset=utf-8')-> matches('/category\: Programming/')-> end() ;
В этом тесте вы увидите два новых метода:
isValid()
: Checks whether or not the XML response is well formedisFormat()
: Проверяет формат запросаmatches()
: Для не HTML формата, проверяет, соответствует ли ответ регулярному выражению, переданному в параметре.
Форма для регистрации партнеров
Теперь когда веб-сервисы готовы к использованию, давайте создадим форму для регистрации партнеров. Мы ещё раз опишем стандартный процесс добавления нового функционала в приложение.
Маршрутизация
Вы угадали. Первым делом мы добавим маршруты:
# apps/frontend/config/routing.yml affiliate: class: sfDoctrineRouteCollection options: model: JobeetAffiliate actions: [new, create] object_actions: { wait: get }
Это обычная Doctrine коллекция маршрутов с новым конфигурационным параметром:
actions
. Так как нам не нужны все семь действий, генерируемых маршрутом по умолчанию,
параметр actions
укажет маршрутизатору, соответствовать только действиям new
и create
.
Дополнительный маршрут wait
будет использован для обратной связи с будущим партнером.
Генерация модуля
Второй стандартный шаг - это генерация модуля:
$ php symfony doctrine:generate-module frontend affiliate JobeetAffiliate --non-verbose-templates
Шаблоны
Задача doctrine:generate-module
генерирует стандартные семь действий и соответствующие
им шаблоны. Удалите все файлы в папке templates/
, кроме _form.php
и newSuccess.php
.
Замените содержимое оставшихся файлов следующим кодом:
<!-- apps/frontend/modules/affiliate/templates/newSuccess.php --> <?php use_stylesheet('job.css') ?> <h1>Become an Affiliate</h1> <?php include_partial('form', array('form' => $form)) ?> <!-- apps/frontend/modules/affiliate/templates/_form.php --> <?php include_stylesheets_for_form($form) ?> <?php include_javascripts_for_form($form) ?> <?php echo form_tag_for($form, 'affiliate') ?> <table id="job_form"> <tfoot> <tr> <td colspan="2"> <input type="submit" value="Submit" /> </td> </tr> </tfoot> <tbody> <?php echo $form ?> </tbody> </table> </form>
Создайте шаблон waitSuccess.php
:
<!-- apps/frontend/modules/affiliate/templates/waitSuccess.php --> <h1>Your affiliate account has been created</h1> <div style="padding: 20px"> Thank you! You will receive an email with your affiliate token as soon as your account will be activated. </div>
И последнее - измените ссылку в нижней части страницы, чтобы она указывала на модуль
affiliate
:
// apps/frontend/templates/layout.php <li class="last"> <a href="<?php echo url_for('affiliate_new') ?>">Become an affiliate</a> </li>
Контроллер
Так как мы будем использовать форму только для создания, откройте actions.class.php
и удалите все действия, кроме executeNew()
, executeCreate()
и processForm()
.
Для действия processForm()
измените URL переадресации на действие wait
:
// apps/frontend/modules/affiliate/actions/actions.class.php $this->redirect($this->generateUrl('affiliate_wait', $jobeet_affiliate));
Действие wait
простое, так как нам не надо передавать ничего в шаблон.
// apps/frontend/modules/affiliate/actions/actions.class.php public function executeWait(sfWebRequest $request) { }
Партнер не может выбирать свой токен, также он не может активировать свой аккаунт.
Откройте файл JobeetAffiliateForm
для изменения формы:
// lib/form/doctrine/JobeetAffiliateForm.class.php class JobeetAffiliateForm extends BaseJobeetAffiliateForm { public function configure() { $this->useFields(array( 'url', 'email', 'jobeet_categories_list' )); $this->widgetSchema['jobeet_categories_list']->setOption('expanded', true); $this->widgetSchema['jobeet_categories_list']->setLabel('Categories'); $this->validatorSchema['jobeet_categories_list']->setOption('required', true); $this->widgetSchema['url']->setLabel('Your website URL'); $this->widgetSchema['url']->setAttribute('size', 50); $this->widgetSchema['email']->setAttribute('size', 50); $this->validatorSchema['email'] = new sfValidatorEmail(array('required' => true)); } }
The new sfForm::useFields()
method allows to specify the white list of fields to keep. All non mentionned fields will be removed from the form.
Фреймворк форм поддерживает отношения многие ко многим. По умолчанию такое отношение
отображается как выпадающее меню с помощью виджета sfWidgetFormChoice
. В уроке
10, мы изменили тэг для отображения параметром expanded
.
Так как почтовые адреса и URL-ы обычно бывают длиннее указанного по умолчанию размера
поля ввода, с помощью метода setAttribute()
, мы можем указать HTML атрибуты,
которые будут использоваться по умолчанию.
Тесты
Завершающим шагом будет написание нескольких функциональных тестов.
Замените созданные тесты для модуля affiliate
следующим кодом:
// test/functional/frontend/affiliateActionsTest.php include(dirname(__FILE__).'/../../bootstrap/functional.php'); $browser = new JobeetTestFunctional(new sfBrowser()); $browser->loadData(); $browser-> info('1 - An affiliate can create an account')-> get('/affiliate/new')-> click('Submit', array('jobeet_affiliate' => array( 'url' => 'http://www.example.com/', 'email' => 'foo@example.com', 'jobeet_categories_list' => array(Doctrine_Core::getTable('JobeetCategory')->findOneBySlug('programming')->getId()), )))-> isRedirected()-> followRedirect()-> with('response')->checkElement('#content h1', 'Your affiliate account has been created')-> info('2 - An affiliate must at least select one category')-> get('/affiliate/new')-> click('Submit', array('jobeet_affiliate' => array( 'url' => 'http://www.example.com/', 'email' => 'foo@example.com', )))-> with('form')->isError('jobeet_categories_list') ;
Админка для партнеров (backend)
В приложении backend, должен быть создан модуль affiliate
для партнеров,
активированных администратором:
$ php symfony doctrine:generate-admin backend JobeetAffiliate --module=affiliate
Для доступа к только что созданному модулю, добавьте ссылку в главном меню и укажите количество партнеров, которые ждут активации.
<!-- apps/backend/templates/layout.php --> <li> <a href="<?php echo url_for('jobeet_affiliate_affiliate') ?>"> Affiliates - <strong><?php echo Doctrine_Core::getTable('JobeetAffiliate')->countToBeActivated() ?></strong> </a> </li> // lib/model/doctrine/JobeetAffiliateTable.class.php class JobeetAffiliateTable extends Doctrine_Table { public function countToBeActivated() { $q = $this->createQuery('a') ->where('a.is_active = ?', 0); return $q->count(); } // ... }
Так как единственные действия, необходимые в backend'е это активация и деактивация,
измените раздел config
в генераторе по умолчанию, для упрощения интерфейса, и
добавьте ссылку на активацию аккаунтов прямо из списка:
# apps/backend/modules/affiliate/config/generator.yml config: fields: is_active: { label: Active? } list: title: Affiliate Management display: [is_active, url, email, token] sort: [is_active] object_actions: activate: ~ deactivate: ~ batch_actions: activate: ~ deactivate: ~ actions: {} filter: display: [url, email, is_active]
Чтобы сделать работу администраторов более продуктивной, измените фильтры по умолчанию таким образом, чтобы они отображали только тех партнеров, которых нужно активировать:
// apps/backend/modules/affiliate/lib/affiliateGeneratorConfiguration.class.php class affiliateGeneratorConfiguration extends BaseAffiliateGeneratorConfiguration { public function getFilterDefaults() { return array('is_active' => '0'); } }
Единственное, что осталось написать, это код для действий activate
и deactivate
:
// apps/backend/modules/affiliate/actions/actions.class.php class affiliateActions extends autoAffiliateActions { public function executeListActivate() { $this->getRoute()->getObject()->activate(); $this->redirect('jobeet_affiliate'); } public function executeListDeactivate() { $this->getRoute()->getObject()->deactivate(); $this->redirect('jobeet_affiliate'); } public function executeBatchActivate(sfWebRequest $request) { $q = Doctrine_Query::create() ->from('JobeetAffiliate a') ->whereIn('a.id', $request->getParameter('ids')); $affiliates = $q->execute(); foreach ($affiliates as $affiliate) { $affiliate->activate(); } $this->redirect('jobeet_affiliate'); } public function executeBatchDeactivate(sfWebRequest $request) { $q = Doctrine_Query::create() ->from('JobeetAffiliate a') ->whereIn('a.id', $request->getParameter('ids')); $affiliates = $q->execute(); foreach ($affiliates as $affiliate) { $affiliate->deactivate(); } $this->redirect('jobeet_affiliate'); } } // lib/model/doctrine/JobeetAffiliate.class.php class JobeetAffiliate extends BaseJobeetAffiliate { public function activate() { $this->setIsActive(true); return $this->save(); } public function deactivate() { $this->setIsActive(false); return $this->save(); } // ... }
Увидимся завтра!
Благодаря REST-архитектуре Symfony, реализация веб-сервисов в Вашем проекте становится достаточно простой. Хотя мы написали код для веб-сервиса только для чтения данных, мы уже обладаем достаточными знаниями для написания веб-сервиса способного и читать, и записывать данные.
Создание формы регистрации партнера в приложении frontend и его админки в backend было достаточно просто, поскольку Вы уже знакомы с процедурой добавления новых возможностей в Ваш проект.
Вспомните требования из дня 2:
"Партнер также может ограничить количество получаемых вакансии, и фильтровать результат по категории."
Решение этой задачи настолько просто, что мы дадим Вам выполнить её сегодня самостоятельно.
Как только аккаунт партнера активирован администратором, партнеру должно быть отправлено электронное письмо для подтверждения подписки и чтобы дать ему токен. Отправка электронной почты - тема завтрашнего урока.
This work is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.