Мы вчера много узнали о symfony: получение данных с помощью Doctrine, фикстуры (начальные и тестовые данные), маршрутизация, отладка и конфигурация проекта.
Если Вы вчера поработали самостоятельно, Вы лучше воспримите этот урок.
Итак, рассмотрим возможный вариант реализации.
Маршрутизация для страницы категорий
Для начала мы должны добавить правило маршрутизации, чтобы создать "красивый" URL для страницы категорий.
# apps/frontend/config/routing.yml category: url: /category/:slug class: sfDoctrineRoute param: { module: category, action: show } options: { model: JobeetCategory, type: object }
tip
Вообще, лучше всегда сначала подумать об URL и создать соответствующее правило маршрутизации перед тем, как реализовывать новую задачу. И конечно, это обязательное условие, если Вы удалили дефолтные правила из routing.yml.
В правиле маршрутизации, в качестве параметра запроса, можно использовать любое свойство связанного объекта.
А также, можно использовать любое произвольное значение, если связанный объект имеет соответствующий геттер.
Поскольку в таблице category
нет колонки slug
, нам необходимо добавить
соответствующий геттер в JobeetCategory
, чтобы правило заработало:
// lib/model/doctrine/JobeetCategory.class.php public function getSlug() { return Jobeet::slugify($this->getName()); }
Ссылка на страницу категорий
Теперь измените шаблон job/indexSuccess.php
, чтобы добавить ссылку на страницу категорий.
<!-- some HTML code --> <h1> <?php echo link_to($category, 'category', $category) ?> </h1> <!-- some HTML code --> </table> <?php if (($count = $category->countActiveJobs() - sfConfig::get('app_max_jobs_on_homepage')) > 0): ?> <div class="more_jobs"> and <?php echo link_to($count, 'category', $category) ?> more... </div> <?php endif; ?> </div> <?php endforeach; ?> </div>
Мы добавляем ссылку только в том случае, если в конкретной категории содержится больше 10 вакансий.
В тексте ссылки укажем число оставшихся вакансий, которые не поместились в общий список.
Чтобы этот шаблон смог работать, добавим метод countActiveJobs()
в JobeetCategory
:
// lib/model/doctrine/JobeetCategory.class.php public function countActiveJobs() { $q = Doctrine_Query::create() ->from('JobeetJob j') ->where('j.category_id = ?', $this->getId()); return Doctrine_Core::getTable('JobeetJob')->countActiveJobs($q); }
Метод JobeetCategory::countActiveJobs()
использует не существующий пока метод
JobeetJobTable::countActiveJobs()
. Добавим следующий код в JobeetJobTable.php
:
// lib/model/doctrine/JobeetJobTable.class.php class JobeetJobTable extends Doctrine_Table { public function retrieveActiveJob(Doctrine_Query $q) { return $this->addActiveJobsQuery($q)->fetchOne(); } public function getActiveJobs(Doctrine_Query $q = null) { return $this->addActiveJobsQuery($q)->execute(); } public function countActiveJobs(Doctrine_Query $q = null) { return $this->addActiveJobsQuery($q)->count(); } public function addActiveJobsQuery(Doctrine_Query $q = null) { if (is_null($q)) { $q = Doctrine_Query::create() ->from('JobeetJob j'); } $alias = $q->getRootAlias(); $q->andWhere($alias . '.expires_at > ?', date('Y-m-d h:i:s', time())) ->addOrderBy($alias . '.expires_at DESC'); return $q; } }
Как Вы видите, мы полностью отрефакторили код JobeetJobTable
,
чтобы дополнительно выделить метод addActiveJobsQuery()
.
Это позволит обойти дублирование и сделает код более DRY (Don't Repeat Yourself).
tip
Если Вы просто скопируете какую-то часть кода, на первое время, этого будет достаточно. Но, если Вам придется копировать еще раз, тогда стоит провести рефакторинг и выделить отдельный метод.
В методе JobeetJobTable::countActiveJobs()
вместо того, чтобы использовать execute()
и считать кол-во возвращенных элементов, мы использовали более эффективный метод count()
,
который сразу возвращает необходимый результат.
Для такой небольшой задачи мы уже изменили довольно много файлов. Но всякий раз, когда мы вносили правки, мы старались делать это в правильном направлении, разграничивая ответственности по разным слоям. Попутно мы проводили рефакторинг, чтобы сделать код повторно используемым. Это нормальный процесс, когда Вы работаете с symfony.
Создание модуля для категорий
Теперь создадим отдельный модуль для категорий:
$ php symfony generate:module frontend category
Возможно, для создания модуля Вы будете использовать команду doctrine:generate-module
.
Но, поскольку нам не потребуется 90% сгенеренного кода, я буду использовать generate:module
,
который просто создаст пустой модуль.
tip
Почему мы не добавили еще один контроллер category
в модуль job
?
В принципе, это возможно. Но, поскольку главной сущностью страницы является непосредственно "категория",
будет более правильным выделить отдельный модуль под эту задачу.
Когда мы запрашиваем страницу категории, маршрут category
будет пытаться
найти соответствующий объект по параметру slug
. Поскольку slug не хранится в базе данных,
и мы не можем вычислить категорию зная slug, найти объект по slug оказывается невозможным.
Редактирование базы данных
Нам необходимо добавить колонку slug
в таблицу category
:
Эту задачу может решить Doctrine behavior Sluggable
.
Надо просто включить это "поведение" для JobeetCategory
и оно самостоятельно
добавит колонку slug
и реализует всю логику.
# config/doctrine/schema.yml JobeetCategory: actAs: Timestampable: ~ Sluggable: fields: [name] columns: name: type: string(255) notnull: true
Теперь, когда slug
появился в базе данных, мы можем удалить метод JobeetCategory::getSlug()
note
При сохранении объекта, slug будет автоматически вычислен и установлен на основе свойства name
.
Используйте команду doctrine:build --all --and-load
, чтобы обновить таблицы базы данных
и заполнить тестовыми данными (фикстурой).
$ php symfony doctrine:build --all --and-load --no-confirmation
Теперь все готово, чтобы создать метод executeShow()
.
Замените содержимое контроллера category
следующим кодом:
// apps/frontend/modules/category/actions/actions.class.php class categoryActions extends sfActions { public function executeShow(sfWebRequest $request) { $this->category = $this->getRoute()->getObject(); } }
note
Поскольку мы удалили метод executeIndex()
, также мы можем удалить
автоматически созданный шаблон indexSuccess.php
(apps/frontend/modules/category/templates/indexSuccess.php
).
И напоследок, остается создать шаблон showSuccess.php
:
// apps/frontend/modules/category/templates/showSuccess.php <?php use_stylesheet('jobs.css') ?> <?php slot('title', sprintf('Jobs in the %s category', $category->getName())) ?> <div class="category"> <div class="feed"> <a href="">Feed</a> </div> <h1><?php echo $category ?></h1> </div> <table class="jobs"> <?php foreach ($category->getActiveJobs() as $i => $job): ?> <tr class="<?php echo fmod($i, 2) ? 'even' : 'odd' ?>"> <td class="location"> <?php echo $job->getLocation() ?> </td> <td class="position"> <?php echo link_to($job->getPosition(), 'job_show_user', $job) ?> </td> <td class="company"> <?php echo $job->getCompany() ?> </td> </tr> <?php endforeach; ?> </table>
Partial (Фрагмент шаблона)
Обратите внимание, что мы скопировали часть шаблона, ответственного за отрисовку
списка вакансий из job/indexSuccess.php
. Но это не эффективно.
Пришло время познакомиться с новым приемом: когда Вам необходимо повторно использовать
какую-то часть шаблона, Вы можете выделить ее в partial.
Это фрагмент кода, который может быть использован в нескольких шаблонах.
Практически это такой же шаблон, только название файла начинается со знака подчеркивания (_
).
Создайте файл job/templates/_list.php
:
// apps/frontend/modules/job/templates/_list.php <table class="jobs"> <?php foreach ($jobs as $i => $job): ?> <tr class="<?php echo fmod($i, 2) ? 'even' : 'odd' ?>"> <td class="location"> <?php echo $job->getLocation() ?> </td> <td class="position"> <?php echo link_to($job->getPosition(), 'job_show_user', $job) ?> </td> <td class="company"> <?php echo $job->getCompany() ?> </td> </tr> <?php endforeach; ?> </table>
Чтобы подключить partial используйте хелпер include_partial()
:
<?php include_partial('job/list', array('jobs' => $jobs)) ?>
В качестве первого аргумента include_partial()
используется связка "модуль/partial"
(знак подчеркивания в названии partial'а пропускается). Второй аргумент - это
массив переменных, которые должны быть переданы в partial.
note
Почему мы используем хелпер include_partial()
вместо того, чтобы использовать
родную конструкцию PHP include()
? Принципиальное отличие include_partial()
- это
встроенная поддержка кеширования шаблонов.
Замените соответствующий HTML-код в обоих шаблонах на вызов include_partial()
:
// in apps/frontend/modules/job/templates/indexSuccess.php <?php include_partial('job/list', array('jobs' => $category->getActiveJobs(sfConfig::get('app_max_jobs_on_homepage')))) ?> // in apps/frontend/modules/category/templates/showSuccess.php <?php include_partial('job/list', array('jobs' => $category->getActiveJobs())) ?>
Постраничная навигация
В соответствии с требованиями, описанными в "День 2":
"Список разбивается на страницы по 20 вакансий"
Для постраничной выборки объектов для Doctrine в symfony используется класс
sfDoctrinePager
.
Теперь, вместо того, чтобы передавать в шаблон category/showSuccess
массив объектов JobeetJob
, мы будем передавать pager:
// apps/frontend/modules/category/actions/actions.class.php public function executeShow(sfWebRequest $request) { $this->category = $this->getRoute()->getObject(); $this->pager = new sfDoctrinePager( 'JobeetJob', sfConfig::get('app_max_jobs_on_category') ); $this->pager->setQuery($this->category->getActiveJobsQuery()); $this->pager->setPage($request->getParameter('page', 1)); $this->pager->init(); }
tip
Метод sfRequest::getParameter()
вторым аргументом принимает значение параметра по-умолчанию.
Если параметр page
отсутствует, тогда getParameter()
вернет 1
.
Конструктор sfDoctrinePager
в качестве аргументов принимает класс модели и кол-во элементов на страницу.
Перенесем в конфиг соответствующее настройки для постраничной навигации:
# apps/frontend/config/app.yml all: active_days: 30 max_jobs_on_homepage: 10 max_jobs_on_category: 20
Метод sfDoctrinePager::setQuery()
принимет объект Doctrine_Query
для ограничения выборки.
Добавим метод JobeetCategory::getActiveJobsQuery()
:
// lib/model/doctrine/JobeetCategory.class.php public function getActiveJobsQuery() { $q = Doctrine_Query::create() ->from('JobeetJob j') ->where('j.category_id = ?', $this->getId()); return Doctrine_Core::getTable('JobeetJob')->addActiveJobsQuery($q); }
Теперь, когда мы определили метод getActiveJobsQuery()
, мы можем отрефакторить
класс JobeetCategory
:
// lib/model/doctrine/JobeetCategory.class.php public function getActiveJobs($max = 10) { $q = $this->getActiveJobsQuery() ->limit($max); return $q->execute(); } public function countActiveJobs() { return $this->getActiveJobsQuery()->count(); }
И, в заключение, отредактируем шаблон:
<!-- apps/frontend/modules/category/templates/showSuccess.php --> <?php use_stylesheet('jobs.css') ?> <?php slot('title', sprintf('Jobs in the %s category', $category->getName())) ?> <div class="category"> <div class="feed"> <a href="">Feed</a> </div> <h1><?php echo $category ?></h1> </div> <?php include_partial('job/list', array('jobs' => $pager->getResults())) ?> <?php if ($pager->haveToPaginate()): ?> <div class="pagination"> <a href="<?php echo url_for('category', $category) ?>?page=1"> <img src="/legacy/images/first.png" alt="First page" title="First page" /> </a> <a href="<?php echo url_for('category', $category) ?>?page=<?php echo $pager->getPreviousPage() ?>"> <img src="/legacy/images/previous.png" alt="Previous page" title="Previous page" /> </a> <?php foreach ($pager->getLinks() as $page): ?> <?php if ($page == $pager->getPage()): ?> <?php echo $page ?> <?php else: ?> <a href="<?php echo url_for('category', $category) ?>?page=<?php echo $page ?>"><?php echo $page ?></a> <?php endif; ?> <?php endforeach; ?> <a href="<?php echo url_for('category', $category) ?>?page=<?php echo $pager->getNextPage() ?>"> <img src="/legacy/images/next.png" alt="Next page" title="Next page" /> </a> <a href="<?php echo url_for('category', $category) ?>?page=<?php echo $pager->getLastPage() ?>"> <img src="/legacy/images/last.png" alt="Last page" title="Last page" /> </a> </div> <?php endif; ?> <div class="pagination_desc"> <strong><?php echo count($pager) ?></strong> jobs in this category <?php if ($pager->haveToPaginate()): ?> - page <strong><?php echo $pager->getPage() ?>/<?php echo $pager->getLastPage() ?></strong> <?php endif; ?> </div>
Большая часть кода оперирует ссылками на другие страницы.
Ниже представлен список методов sfDoctrinePager
, использованных в шаблоне:
getResults()
: Возвращает массив Doctrine-объектов для текущей страницыgetNbResults()
: Общее количество найденных объектовhaveToPaginate()
: Возвращаетtrue
, если количество страниц больше 1getLinks()
: Массив ссылок на все страницыgetPage()
: Текущий номер страницыgetPreviousPage()
: Номер предыдущей страницыgetNextPage()
: Номер следующей страницыgetLastPage()
: Номер последней страницы
Поскольку класс sfDoctrinePager
также реализует интерфейсы Iterator
и Countable
,
Вы можете использовать функцию count()
, чтобы получить количество объектов, вместо
метода getNbResults()
.
Увидимся завтра!
Напоследок, хочу повторить, что процесс реализации новой задачи начинается с создания URL'а, затем контроллера, потом изменяете модель и добавляете шаблоны. Если при этом Вы будете придерживаться хороших практик разработки, то совсем скоро почуствуете философию symfony.
Завтра мы поговорим о совершенно новой теме - о тестировании приложения.
This work is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.