Caution: You are browsing the legacy symfony 1.x part of this website.
Cover of the book Symfony 5: The Fast Track

Symfony 5: The Fast Track is the best book to learn modern Symfony development, from zero to production. +300 pages showcasing Symfony with Docker, APIs, queues & async tasks, Webpack, SPAs, etc.

Ngày 7: Tiếp tục với trang Category

Tóm tắt

Hôm qua chúng ta đã mở rộng kiến thức về symfony trên nhiều mặt: đối tượng Doctrine, fixtures, routing, debugging,và custom configuration. Và chúng ta đã kết thúc với một bài tập nhỏ cho ngày hôm nay.

Tôi hi vọng bạn đã làm trang Jobeet category để hướng dẫn hôm nay trở nên hữu ích hơn.

Bạn đã sẵn sàng chưa? Chúng ta sẽ tiếp tục bổ sung những thứ cần thiết.

Category Route

Đầu tiên, chúng ta cần thêm một route để tạo URL dễ nhìn cho trang category. Thêm nó vàn đầu file routing:

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

tip

Bất cứ khi nào bạn bắt đầu thêm một tính năng mới, bạn nên nghĩ đến URL đầu tiên và tạo một route cho nó.

slug không phải là một cột trong bảng category, chúng ta cần thêm một virtual accessor vào JobeetCategory để route có thể hoạt động:

// lib/model/doctrine/JobeetCategory.class.php
public function getSlug()
{
  return Jobeet::slugify($this->getName());
}

Category Link

Bây giờ, chỉnh sửa template indexSuccess.php của module job và thêm đường dẫn đến trang category:

<!-- 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>

Chúng ta chỉ thêm link nếu category đó có nhiều hơn 10 công việc. Để template chạy được, chúng ta cần thêm phương thức countActiveJobs() vào lớp 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::getTable('JobeetJob')->countActiveJobs($q);
}

Phương thức countActiveJobs() dùng một phương thức countActiveJobs() chưa có trong JobeetJobTable:

// 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;
  }
}

Như bạn đã thấy, chúng ta đã refactor lại toàn bộ mã nguồn của JobeetJobTable và thêm một phương thức mới addActiveJobsQuery() để tránh việc lặp lại code (DRY (Don't Repeat Yourself)).

tip

Khi code được sử dụng lại một lần, ta có thể chỉ cần copy lại code đó. Nhưng nếu code được lặp lại nhiều lần, bạn cần refactor chúng để cùng sử dụng một function hay method, như chúng ta đã làm ở trên.

Trong phương thức countActiveJobs(), thay vì sử dụng execute() và đếm kết quả trả về, chúng ta sử dụng count().

Chúng ta đã sửa rất nhiều file cho một tính năng đơn giản!. Mỗi khi viết code chúng ta cần đặt chúng ở đúng layer của ứng dụng để code có thể dùng lại được. Trong quá trình đó, chúng ta cũng cần refactor lại những code đã có. Đó là tiến trình công việc đặc trưng khi chúng ta làm việc trong một dự án symfony.

Tạo module Job Category

Bây giờ chúng ta cần tạo module category:

$ php symfony generate:module frontend category

Bạn đã biết cách tạo module sử dụng lệnh doctrine:generate-module. Cách đó cũng tốt nhưng ở đây chúng ta sẽ không dùng 90% mã nguồn được tạo ra, lệnh generate:module sẽ tạo ra một module rỗng.

tip

Tại sao không thêm một action category và module job? Chúng ta có thể làm vậy, nhưng nội dung chính của trang category là về category, do đó ta nên tạo một module riêng cho nó.

Khi truy cập trang category, route category sẽ tìm category liên quan đến biến slug. Nhưng slug không được chứa trong database, và chúng ta cũng không thể suy ra tên của category từ slug, nên không có cách nào để tìm được category liên quan đến slug yêu cầu.

Update the Database

Chúng ta cần thêm cột slug cho bảng category:

Cột slug được quản lý tự động bởi Doctrine behavior Sluggable. Chúng ta chỉ cần enable behavior này trong model JobeetCategory của chúng ta và nó sẽ làm mọi thứ cho bạn.

# config/doctrine/schema.yml
JobeetCategory:
  actAs:
    Timestampable: ~
    Sluggable:
      fields: [name]
  columns:
    name:
      type: string(255)
      notnull:  true

Bây giờ slug đã là một cột thực sự, bạn cần xóa phương thức getSlug()JobeetCategory.

note

Giá trị của cột slug được tạo tự động mỗi khi bạn lưu một bản ghi. Giá trị này được tạo từ giá trị của trường name.

Dùng lệnh doctrine:build-all-reload để sửa lại các bảng trong database, và phục hồi lại các dữ liệu từ file fixtures:

$ php symfony doctrine:build-all-reload

Bây giờ chúng ta đã có thể tạo phương thức executeShow():

// apps/frontend/modules/category/actions/actions.class.php
class categoryActions extends sfActions
{
  public function executeShow(sfWebRequest $request)
  {
    $this->category = $this->getRoute()->getObject();
  }
}

Cuối cùng, tạo template showSuccess.php:

// apps/frontend/modules/category/template/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>

Partials

Bạn có thể nhận thấy rằng chúng ta đã copied and pasted nội dung trong tag <table> từ template indexSuccess.php ở trang chủ để hiển thị danh sách các công việc. Điều đó không được tốt lắm. Chúng ta sẽ học một thủ thuật mới để giải quyết vấn đề này. Khi bạn cần dùng lại một phần nào đó trong template, bạn cần tạo một partial. Một partial là một snippet của template có thể được dùng chung trong nhiều templates. Một partial là một file template bắt đầu bởi kí tự (_):

// 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>

Bạn có thể sử dụng một partial nhờ helper include_partial():

<?php include_partial('job/list', array('jobs' => $jobs)) ?>

Tham số đầu tiên của include_partial() là tên partial (tạo bởi tên của module , kí tự /, và tên của partial bỏ đi kí tự _). Tham số thứ 2 là mảng các biến truyền vào cho partial.

note

Tại sao không sử dụng phương thức include() có sẵn trong PHP mà lại dùng helper include_partial()? Sự khác biệt chính giữa hai cách này là hệ thống cache hỗ trợ helper include_partial().

Thay thế đoạn code <table> HTML từ cả 2 templates bằng lời gọi 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())) ?>

Phân trang

Yêu cầu trong ngày 2:

"Danh sách công việc được phân trang với 20 job mỗi trang."

Để phân trang một list các Doctrine object, symfony cung cấp lớp class: sfDoctrinePager. Thay vì truyền các job objects cho template, ta truyền một pager:

// apps/frontend/job/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

Phương thức getParameter() nhận giá trị mặc định từ tham số thứ 2. Trong action trên, nếu tham số page request không tồn tại, getParameter() sẽ trả về giá trị 1.

Phương thức khởi tạo sfDoctrinePager nhận 2 tham số: model class và số lượng lớn nhất các đối tượng trả về ở một trang. Thêm giá trị này vào file cấu hình:

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

Phương thức sfDoctrinePager::setQuery() dùng một đối tượng Doctrine_Query để lấy các đối tượng từ database. Một lần nữa, chúng ta cần refactoring một chút trong Model:

// lib/model/doctrine/JobeetCategory.class.php
public function getActiveJobsQuery()
{
  $q = Doctrine_Query::create()
    ->from('JobeetJob j')
    ->where('j.category_id = ?', $this->getId());
 
  return Doctrine::getTable('JobeetJob')->addActiveJobsQuery($q);
}

Bây giờ chúng ta đã có phương thức getActiveJobsQuery(), chúng ta cần refactor các phương thức khác trong 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();
}

Cuối cùng, sửa lại template:

<!-- apps/frontend/modules/category/templates/showSuccess.php -->
<?php use_stylesheet('jobs.css') ?>
 
<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" />
    </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 $pager->getNbResults() ?></strong> jobs in this category
 
  <?php if ($pager->haveToPaginate()): ?>
    - page <strong><?php echo $pager->getPage() ?>/<?php echo $pager->getLastPage() ?></strong>
  <?php endif; ?>
</div>

Dưới đây là danh sách các phương thức của lớp sfDoctrinePager dùng trong template trên:

  • getResults(): trả về mảng các đối tượng Propel cho trang hiện tại
  • getNbResults(): trả về tổng số kết quả
  • haveToPaginate(): trả về true nếu có nhiều hơn một trang
  • getLinks(): trả về danh sách link đến các trang
  • getPage(): trả về số của trang hiện tại
  • getPreviousPage(): trả về số của trang trước
  • getNextPage(): trả về số của trang tiếp
  • getLastPage(): trả về số của trang cuối cùng

Hẹn gặp lại ngày mai

Nếu bạn đã tự làm những công việc này ngày hôm qua và cảm thấy hôm nay bạn không học được nhiều thứ mới, điều đó có nghĩa là bạn đã quen dần với symfony. Quá trình thêm một tính năng mới vào một website symfony luôn được tiến hành qua các bước: tạo URLs, tạo một vài action, sửa lại model, và viết một vài template. Và nếu bạn tuân theo một vài good development practices, bạn sẽ nhanh chóng trở thành một symfony master.

Ngày mai chúng ta sẽ bắt đầu một tuần mới của Jobeet. Chúng ta sẽ nói về một chủ đề mới: tests.

Subversion tag release_day_07 chứa code đã update của ngày hôm nay:

http://svn.jobeet.org/tags/release_day_07/