Practical symfony

День 7: Категории

You are currently browsing
the website for symfony 1

Visit the Symfony2 website


About

You are currently reading "Practical symfony" which is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.

Jobeet Links

Master symfony

Be trained by SensioLabs experts (2 to 6 day sessions -- French or English).
trainings.sensiolabs.com

Books on symfony

Learn more about symfony with the official guides.
books.sensiolabs.com

L'audit Qualité par SensioLabs

200 points de contrôle de votre applicatif web.
audit.sensiolabs.com

Chapter Content

Маршрутизация для страницы категорий

Ссылка на страницу категорий

Создание модуля для категорий

Редактирование базы данных

Partial (Фрагмент шаблона)

Постраничная навигация

Увидимся завтра!

symfony training
Be trained by symfony experts
Feb 21: Köln (Getting Started with Symfony2 - English)
Feb 27: Köln (Mastering Symfony2 - English)
Mar 05: Köln (Web Development with Symfony2 - Deutsch)
Mar 05: Montreal (Web Development with Symfony2 - English)
Mar 05: Montreal (Getting Started with Symfony2 - English)
and more...

Search


powered by google
You are currently browsing "Practical symfony" in Russian for the 1.4 version - Propel edition - Switch to version: - Switch to ORM: - Switch to language:
Creative Commons License This work is licensed under a Creative Commons Attribution-Share Alike 3.0 Unported License.
Practical symfony (Propel edition)
Support symfony!
Buy this book
or donate.
Buy Practical symfony (Propel edition) from amazon.com

Мы вчера много узнали о symfony: получение данных с помощью Propel, фикстуры (начальные и тестовые данные), маршрутизация, отладка и конфигурация проекта.

Если Вы вчера поработали самостоятельно, Вы лучше воспримите этот урок.

Итак, рассмотрим возможный вариант реализации.

Маршрутизация для страницы категорий

Для начала мы должны добавить правило маршрутизации, чтобы создать "красивый" URL для страницы категорий.

# apps/frontend/config/routing.yml
category:
  url:      /category/:slug
  class:    sfPropelRoute
  param:    { module: category, action: show }
  options:  { model: JobeetCategory, type: object }

Вообще, лучше всегда сначала подумать об URL и создать соответствующее правило маршрутизации перед тем, как реализовывать новую задачу. И конечно, это обязательное условие, если Вы удалили дефолтные правила из routing.yml.

В правиле маршрутизации, в качестве параметра запроса, можно использовать любое свойство связанного объекта. А также, можно использовать любое произвольное значение, если связанный объект имеет соответствующий геттер. Поскольку в таблице category нет колонки slug, нам необходимо добавить соответствующий геттер в JobeetCategory, чтобы правило заработало:

// lib/model/JobeetCategory.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/JobeetCategory.php
public function countActiveJobs()
{
  $criteria = new Criteria();
  $criteria->add(JobeetJobPeer::CATEGORY_ID, $this->getId());
 
  return JobeetJobPeer::countActiveJobs($criteria);
}

Метод JobeetCategory::countActiveJobs() использует не существующий пока метод JobeetJobPeer::countActiveJobs(). Добавим следующий код в JobeetJobPeer.php:

// lib/model/JobeetJobPeer.php
class JobeetJobPeer extends BaseJobeetJobPeer
{
  static public function getActiveJobs(Criteria $criteria = null)
  {
    return self::doSelect(self::addActiveJobsCriteria($criteria));
  }
 
  static public function countActiveJobs(Criteria $criteria = null)
  {
    return self::doCount(self::addActiveJobsCriteria($criteria));
  }
 
  static public function addActiveJobsCriteria(Criteria $criteria = null)
  {
    if (is_null($criteria))
    {
      $criteria = new Criteria();
    }
 
    $criteria->add(self::EXPIRES_AT, time(), Criteria::GREATER_THAN);
    $criteria->addDescendingOrderByColumn(self::CREATED_AT);
 
    return $criteria;
  }
 
  static public function doSelectActive(Criteria $criteria)
  {
    return self::doSelectOne(self::addActiveJobsCriteria($criteria));
  }
}

Как Вы видите, мы полностью отрефакторили JobeetJobPeer, чтобы дополнительно выделить метод addActiveJobsCriteria(). Это позволит обойти дублирование и сделает код более DRY (Don't Repeat Yourself).

Если Вы просто скопируете какую-то часть кода, на первое время, этого будет достаточно. Но, если Вам придется копировать еще раз, тогда стоит провести рефакторинг и выделить отдельный метод.

В методе JobeetJobPeer::countActiveJobs() вместо того, чтобы использовать doSelect() и считать кол-во возвращенных элементов, мы использовали более эффективный метод doCount(), который сразу возвращает необходимый результат.

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

Homepage

Создание модуля для категорий

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

$ php symfony generate:module frontend category

Возможно, для создания модуля Вы будете использовать команду propel:generate-module. Но, поскольку нам не потребуется 90% сгенеренного кода, я буду использовать generate:module, который просто создаст пустой модуль.

Почему мы не добавили еще один контроллер category в модуль job? В принципе, это возможно. Но, поскольку главной сущностью страницы является непосредственно "категория", будет более правильным выделить отдельный модуль под эту задачу.

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

Редактирование базы данных

Нам необходимо добавить колонку slug в таблицу category:

# config/schema.yml
propel:
  jobeet_category:
    id:           ~
    name:         { type: varchar(255), required: true }
    slug:         { type: varchar(255), required: true, index: unique }

Теперь, когда slug появился в базе данных, мы можем удалить метод JobeetCategory::getSlug()

Каждый раз, когда изменяется название категории, нам необходимо обновить slug. Давайте отредактируем метод JobeetCategory::setName():

// lib/model/JobeetCategory.php
public function setName($name)
{
  parent::setName($name);
 
  $this->setSlug(Jobeet::slugify($name));
}

Используйте команду propel:build --all --and-load, чтобы обновить таблицы базы данных и заполнить тестовыми данными (фикстурой).

$ php symfony propel: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();
  }
}

Поскольку мы удалили метод 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.

Почему мы используем хелпер 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 вакансий"

Для постраничной выборки объектов для Propel в symfony используется класс sfPropelPager. Теперь, вместо того, чтобы передавать в шаблон 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 sfPropelPager(
    'JobeetJob',
    sfConfig::get('app_max_jobs_on_category')
  );
  $this->pager->setCriteria($this->category->getActiveJobsCriteria());
  $this->pager->setPage($request->getParameter('page', 1));
  $this->pager->init();
}

Метод sfRequest::getParameter() вторым аргументом принимает значение параметра по-умолчанию. Если параметр page отсутствует, тогда getParameter() вернет 1.

Конструктор sfPropelPager в качестве аргументов принимает класс модели и кол-во элементов на страницу. Перенесем в конфиг соответствующее настройки для постраничной навигации:

# apps/frontend/config/app.yml
all:
  active_days:          30
  max_jobs_on_homepage: 10
  max_jobs_on_category: 20

Метод sfPropelPager::setCriteria() принимет объект Criteria для ограничения выборки.

Добавим метод JobeetCategory::getActiveJobsCriteria():

// lib/model/JobeetCategory.php
public function getActiveJobsCriteria()
{
  $criteria = new Criteria();
  $criteria->add(JobeetJobPeer::CATEGORY_ID, $this->getId());
 
  return JobeetJobPeer::addActiveJobsCriteria($criteria);
}

Теперь, когда мы определили метод getActiveJobsCriteria(), мы можем отрефакторить класс JobeetCategory:

// lib/model/JobeetCategory.php
public function getActiveJobs($max = 10)
{
  $criteria = $this->getActiveJobsCriteria();
  $criteria->setLimit($max);
 
  return JobeetJobPeer::doSelect($criteria);
}
 
public function countActiveJobs()
{
  $criteria = $this->getActiveJobsCriteria();
 
  return JobeetJobPeer::doCount($criteria);
}

И, в заключение, отредактируем шаблон:

<!-- 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="/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="/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="/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="/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>

Большая часть кода оперирует ссылками на другие страницы. Ниже представлен список методов sfPropelPager, использованных в шаблоне:

Поскольку класс sfPropelPager также реализует интерфейсы Iterator и Countable, Вы можете использовать функцию count(), чтобы получить количество объектов, вместо метода getNbResults().

Pagination

Увидимся завтра!

Напоследок, хочу повторить, что процесс реализации новой задачи начинается с создания URL'а, затем контроллера, потом изменяете модель и добавляете шаблоны. Если при этом Вы будете придерживаться хороших практик разработки, то совсем скоро почуствуете философию symfony.

Завтра мы поговорим о совершенно новой теме - о тестировании приложения.

День 8: Модульное тестирование »
« День 6: Модель в подробностях

Questions & Feedback

If you find a typo or an error, please register and open a ticket.

If you need support or have a technical question, please post to the official user mailing-list.