Skip to content
Caution: You are browsing the legacy symfony 1.x part of this website.

Ngày 5: Routing

Language
ORM

Trước khi bắt đầu

Hôm qua, chúng ta đã nói về design cho Jobeet. Nếu bạn muốn tham gia, chúng tôi đã chuẩn bị một file nén với các trang chính chúng tôi sẽ phát triển trong tutorial (file nén chứa các file HTML tĩnh, các file stylesheets, và các file ảnh). Vào ngày thứ 21 chúng tôi sẽ tổ chức bầu chọn, bạn có thể gửi cho tôi (fabien.potencier [.at.] symfony-project.com) bản thiết kế của bạn trước ngày này. Chúc may mắn!

Tóm tắt

Nếu bạn đã hoàn thành ngày 4, bạn đã quen với MVC pattern và cảm thấy rằng đó là một cách code thật tự nhiên. Hãy dành một chút thời gian để tìm hiểu nó và chúng ta không còn phải bận tâm về nó nữa. Ngày hôm qua, chúng ta đã chỉnh sửa vài trang Jobeet, đồng thời tìm hiểu một vài khái niệm trong symfony như layout, helpers, và slots.

Hôm nay chúng ta sẽ tìm hiểu về một phần thú vị của symfony: routing.

URLs

Nếu bạn click xem một công việc từ trang chủ, URL sẽ kiểu như: /job/show/id/1. Nếu bạn đã từng phát triển một website bằng PHP, có lẽ bạn sẽ quen với URLs kiểu /job.php?id=1. Symfony xử lý URLs như thế nào? Làm thế nào để symfony biết được action nào được gọi dựa trên URL? Tại sao $request->getParameter('id') trả về id của job ? Hôm nay, chúng ta sẽ trả lời những câu hỏi này.

Nhưng trước tiên, chúng ta hãy nói về URLs. Trong một web context, mỗi URL xác định duy nhất một web resource. Khi bạn truy cập một URL, bạn yêu cầu trình duyệt lấy resource xác định bởi URL đó. Do URL là giao diện tương tác giữa website và người dùng, nên nó cần chứa một vài thông tin hữu ích về resource mà nó tham chiếu đến. Nhưng URLs "truyền thống" không thực sự mô tả resource, nó phơi bày ra cấu trúc bên trong của ứng dụng. Người dùng không quan tâm đến việc website được phát triển bằng ngôn ngữ nào và dữ liệu được chứa trong cơ sở dữ liệu ra sao.Việc lộ rõ cấu trúc bên trong còn tạo ra các vấn đề về bảo mật: người dùng có thể đoán được URL của các resources mà anh ta không được phép truy cập? Tất nhiên, lập trình viên phải bảo mật cho những khu vực này, nhưng tốt nhất là hãy ẩn đi các thông tin nhạy cảm.

URLs trong symfony quan trọng đến mức chúng ta có một framework riêng để quản lý chúng: routing framework. Routing quản lý internal URIs và external URLs. Khi có một request đến, routing phân tích URL và chuyển thành internal URI.

Bạn đã thấy internal URI cho trang job ở template showSuccess.php:

'job/show?id='.$job->getId()

Helper url_for() chuyển internal URI này thành URL:

/job/show/id/1

Một internal URI được tạo thành từ các phần: job là module, show là action và query string thêm tham số để gửi cho action. Cấu trúc thông dụng của một internal URIs như sau:

MODULE/ACTION?key=value&key_1=value_1&...

Trong symfony, routing được tách riêng ra, bạn có thể thay đổi URLs mà không ảnh hưởng đến việc xử lý bên trong. Đó là một trong những lợi ích của front-controller design pattern.

Cấu hình Routing

Sự tương ứng giữa internal URIs và external URLs được ghi trong file routing.yml:

# apps/frontend/config/routing.yml
homepage:
  url:   /
  param: { module: default, action: index }
 
default_index:
  url:   /:module
  param: { action: index }
 
default:
  url:   /:module/:action/*

File routing.yml chứa thông tin về các route. Một route bao gồm tên (homepage), pattern (/:module/:action/*), và một vài tham số (nằm sau từ khóa param).

Khi có một request, routing sẽ so sánh URL với các pattern đã có. Route tìm thấy đầu tiên sẽ được sử dụng, do đó thứ tự trong routing.yml rất quan trọng. Chúng ta sẽ xem xét vài ví dụ để hiểu rõ hơn cách hoạt động của nó.

Khi bạn request trang chủ, có URL là /job , route đầu tiên phù hợp là default_index. Trong một pattern, chuỗi nằm sau dấu hai chấm (:) là một biến, vì thế pattern /:module có nghĩa là: / theo sau là một chuỗi nào đó. Trong ví dụ của chúng ta, biến module có giá trị là job. Giá trị này có thể nhận bằng cách $request->getParameter('module'). Route này cũng xác định sẵn một giá trị mặc định cho biến action. Vì thế, với mọi URLs tương ứng với route này, tham số action luôn có giá trị là index.

Nếu bạn request trang /job/show/id/1, symfony sẽ match với pattern cuối cùng: /:module/:action/*. Ở pattern này, dấu (*) tương ứng với tập các cặp biến/giá trị cách nhau bởi dấu (/):

Request parameter Value
module job
action show
id 1

note

moduleaction là các biến đặc biệt, symfony dùng để xác định action được thực thị.

URL /job/show/id/1 có thể tạo từ template bằng helper url_for():

url_for('job/show?id='.$job->getId())

Bạn cũng có thể dùng tên của route với kí tự @ đằng trước:

url_for('@default?id='.$job->getId())

Hai cách gọi là tương đương nhưng cách gọi sau là nhanh hơn do routing không phải phân tích mọi route để tìm ra route thích hợp nhất, và nó cũng dễ khi sử dụng (không cần dùng tên module và action trong internal URI).

Route Customizations

Hiện tại, khi bạn truy cập URL / từ trình duyệt, bạn sẽ thấy trang chào mừng mặc định của symfony. Đó là vì URL này matches với route homepage. Ta cần thay đổi route này thành trang chủ của Jobeet: sửa giá trị biến module thành job:

# apps/frontend/config/routing.yml
homepage:
  url:   /
  param: { module: job, action: index }

Bây giờ, chúng ta có thể sửa lại link ở Jobeet logo trong layout dùng route homepage:

<h1>
  <a href="<?php echo url_for('@homepage') ?>">
    <img src="/legacy/images/jobeet.gif" alt="Jobeet Job Board" />
  </a>
</h1>

Thật đơn giản! Phức tạp hơn một chút, chúng ta cần đổi URL của trang job thành:

/job/sensio-labs/paris-france/1/web-developer

Không cần xem chi tiết nội dung, chỉ cần nhìn URL bạn cũng có thể biết được rằng Sensio Labs đang tìm một Web developer làm việc ở Paris, France.

note

Một URLs đẹp rất quan trọng bởi vì nó truyền tải thông tin đến người dùng. Nó cũng hữu dụng khi bạn copy và paste URL vào email hay optimize website cho các search engines.

Pattern dưới đây match với URL trên:

/job/:company/:location/:id/:position

Sửa file routing.yml và thêm route job vào đầu file:

job:
  url:   /job/:company/:location/:id/:position
  param: { module: job, action: show }

Refresh lại trang chủ, link tới các job vẫn không thay đổi. Đó là vì để tạo route, bạn cần cung cấp tất cả các biến cần thiết. Vì thế, url_for() trong indexSuccess.php cần sửa lại thành:

url_for('job/show?id='.$job->getId().'&company='.$job->getCompany().
  '&location='.$job->getLocation().'&position='.$job->getPosition())

Một internal URI cũng có thể viết ở dạng array:

url_for(array(
  'module'   => 'job',
  'action'   => 'show',
  'id'       => $job->getId(),
  'company'  => $job->getCompany(),
  'location' => $job->getLocation(),
  'position' => $job->getPosition(),
))

Requirements

Trong ngày đầu tiên, chúng ta đã nói về validation và error handling. Hệ thống routing có sẵn tính năng validation. Mỗi biến trong pattern được validate bởi một regular expression xác định trong mục requirements của route:

job:
  url:   /job/:company/:location/:id/:position
  param: { module: job, action: show }
  requirements:
    id: \d+

Mục requirements ở trên bắt buộc id phải là số. Nếu không, route sẽ không match.

Route Class

Mỗi route xác định trong file routing.yml được chuyển đổi thành một object của lớp sfRoute. Có thể đổi lớp này bằng lớp khác xác định trong mục class của route. Ta đã biết rằng giao thức HTTP có vài "methods": GET, POST, HEAD, DELETE, và PUT. 3 method đầu được hỗ trợ bởi tất cả các trình duyệt, trong khi 2 method sau thì không.

Để route chỉ match với một loại request methods nào đó, bạn có thể đổi route class thành sfRequestRoute và thêm một giá trị cho biến sf_method trong requirements:

job:
  url:   /job/:company/:location/:id/:position
  class: sfRequestRoute
  param: { module: job, action: show }
  requirements:
    id: \d+
    sf_method: [GET]

note

bắt buộc route chỉ match với một HTTP methods nào đó tương đương với việc sử dụng sfWebRequest::isMethod() trong action.

Object Route Class

Mỗi internal URI cho một job thật là dài và bất tiện khi viết, nhưng như chúng ta đã biết ở phần trước, có thể thay đổi route class. Với job route, ta nên dùng lớp sfDoctrineRoute để mô tả các Doctrine objects và collections of Doctrine objects:

job_show_user:
  url:     /job/:company/:location/:id/:position
  class:   sfDoctrineRoute
  options: { model: JobeetJob, type: object }
  param:   { module: job, action: show }
  requirements:
    id: \d+
    sf_method: [GET]

Ở đây, option model xác định lớp Doctrine model (JobeetJob) liên quan đến route, và option type cho biết route này liên quan đến 1 object (bạn có thể dùng list nếu route mô tả tập các objects).

Route job_show_user bây giờ liên quan đến JobeetJob vì thế url_for() có thể viết đơn giản như sau:

url_for(array('sf_route' => 'job_show_user', 'sf_subject' => $job))

hoặc:

url_for('job_show_user', $job)

note

Điều này trở nên tiện lợi khi bạn cần cung cấp nhiêu tham số.

Nó hoạt động được bởi vì mỗi biến trong route đều tương ứng với một phương thức accessor trong lớp JobeetJob (ví dụ, biến company được thay bằng giá trị trả về của phương thức getCompany()).

Nếu bạn nhìn URLs đã được tạo ra, bạn sẽ thấy rằng đó chưa thực sự là những gì chúng ta muốn:

http://jobeet.localhost/frontend_dev.php/job/Sensio+Labs/Paris%2C+France/1/Web+Developer

Chúng ta cần "slugify" giá trị các cột bằng cách thay thế các kí tự non ASCII bằng kí tự -. Mở file JobeetJob và thêm các phương thức sau vào trong lớp:

// lib/model/JobeetJob.php
public function getCompanySlug()
{
  return Jobeet::slugify($this->getCompany());
}
 
public function getPositionSlug()
{
  return Jobeet::slugify($this->getPosition());
}
 
public function getLocationSlug()
{
  return Jobeet::slugify($this->getLocation());
}

Sau đó, tạo file lib/Jobeet.class.php và thêm phương thức slugify vào:

// lib/Jobeet.class.php
class Jobeet
{
  static public function slugify($text)
  {
    // replace all non letters or digits by -
    $text = preg_replace('/\W+/', '-', $text);
 
    // trim and lowercase
    $text = strtolower(trim($text, '-'));
 
    return $text;
  }
}

Chúng ta đã tạo ra 3 "virtual" accessors mới: getCompanySlug(), getPositionSlug(), và getLocationSlug(). Bây giờ, bạn có thể thay thế tên của các cột bằng các tên mới này trong route job_show_user:

job_show_user:
  url:     /job/:company_slug/:location_slug/:id/:position_slug
  class:   sfDoctrineRoute
  options: { model: JobeetJob, type: object }
  param:   { module: job, action: show }
  requirements:
    id: \d+
    sf_method: [GET]

Trước khi refresh trang chủ, bạn cần xóa cache do đã tạo một lớp mới (Jobeet):

$ php symfony cc

Bây giờ URLs đã hoàn hảo như mong đợi:

http://jobeet.localhost/frontend_dev.php/job/sensio-labs/paris-france/4/web-developer

Nhưng đó chỉ là một phần của câu chuyện. Route có thể tạo ra URL dựa trên một object, ngược lại ta cũng có thể xác định một object dựa vào URL liên quan đến nó.Object này có thể nhận bằng phương thức getObject() của đối tượng route. Khi phân tích một request đến, routing lưu lại object, vì thế bạn có thể dùng trong actions. Bạn có thể sửa lại phương thức executeShow() :

class jobActions extends sfActions
{
  public function executeShow(sfWebRequest $request)
  {
    $this->job = $this->getRoute()->getObject();
    $this->forward404Unless($this->getRoute()->getObject());
  }
 
  // ...
}

Nếu bạn thử xem một job có id không đúng, bạn sẽ được chuyển sang trang 404 error nhưng thông báo lỗi đã thay đổi:

404 with sfDoctrineRoute

Đó là bởi vì phương thức getRoute() đã tự động bắt lỗi 404 giúp bạn. Do đó, phương thức executeShow có thể sửa lại thành:

class jobActions extends sfActions
{
  public function executeShow(sfWebRequest $request)
  {
    $this->job = $this->getRoute()->getObject();
  }
 
  // ...
}

tip

Nếu bạn không muốn route tạo một lỗi 404, bạn có thể để lựa chọn allow_empty routing là true.

Routing trong Actions và Templates

Trong temlate, helper url_for() chuyển đổi internal URI thành external URL. Một vài symfony helpers khác nhận internal URI làm tham số, như helper link_to() sẽ tạo một thẻ <a>:

<?php echo link_to($job->getPosition(), 'job_show_user', $job) ?>

Mã HTML được tạo ra:

<a href="/job/sensio-labs/paris-france/1/web-developer">Web Developer</a>

Cả url_for()link_to() đều tạo ra đường dẫn tuyệt đối:

url_for('job_show_user', $job, true);
 
link_to($job->getPosition(), 'job_show_user', $job, true);

Nếu bạn muốn tạo ra một URL trong action, bạn có thể dùng phương thức generateUrl():

$this->redirect($this->generateUrl('job_show_user', $job));

sidebar

Các phương thức "redirect"

Hôm qua, chúng ta đã nói về các phương thức "forward". Những phương thức đó chuyển yêu cầu đến action khác mà không thay đổi URL

Các phương thức "redirect" chuyển người dùng tới URL khác. Giống như forward, ta có các phương thức redirect(), redirectIf()redirectUnless().

Collection Route Class

Với module job, chúng ta đã chỉnh sửa route cho action show, còn URLs cho các phương thức khác (index, new, edit, create, update, và delete) vẫn được quản lý bởi route default:

default:
  url: /:module/:action/*

The default route is a great way to start coding without defining too many routes. But as the route acts as a "catch-all", it cannot be configured for specific needs.

Tất các các action trong module job đều liên quan đến JobeetJob model class, do đó chúng ta có thể dễ dàng dùng class sfDoctrineRoute route cho từng action như đã làm với action show. Tuy nhiên chúng ta có thể dùng lớp sfDoctrineRouteCollection cho cả 7 action này:

// apps/frontend/config/routing.yml
 
# put this definition just before the job_show_user one
job:
  class:   sfDoctrineRouteCollection
  options: { model: JobeetJob }

Route job tương đương với 7 sfDoctrineRoute routes sau:

job:
  url:     /job.:sf_format
  class:   sfDoctrineRoute
  options: { model: JobeetJob, type: list }
  param:   { module: job, action: index, sf_format: html }
  requirements: { sf_method: GET }
 
job_new:
  url:     /job/new.:sf_format
  class:   sfDoctrineRoute
  options: { model: JobeetJob, type: object }
  param:   { module: job, action: new, sf_format: html }
  requirements: { sf_method: GET }
 
job_create:
  url:     /job.:sf_format
  class:   sfDoctrineRoute
  options: { model: JobeetJob, type: object }
  param:   { module: job, action: create, sf_format: html }
  requirements: { sf_method: POST }
 
job_edit:
  url:     /job/:id/edit.:sf_format
  class:   sfDoctrineRoute
  options: { model: JobeetJob, type: object }
  param:   { module: job, action: edit, sf_format: html }
  requirements: { sf_method: GET }
 
job_update:
  url:     /job/:id.:sf_format
  class:   sfDoctrineRoute
  options: { model: JobeetJob, type: object }
  param:   { module: job, action: update, sf_format: html }
  requirements: { sf_method: PUT }
 
job_delete:
  url:     /job/:id.:sf_format
  class:   sfDoctrineRoute
  options: { model: JobeetJob, type: object }
  param:   { module: job, action: delete, sf_format: html }
  requirements: { sf_method: DELETE }
 
job_show:
  url:     /job/:id.:sf_format
  class:   sfDoctrineRoute
  options: { model: JobeetJob, type: object }
  param:   { module: job, action: show, sf_format: html }
  requirements: { sf_method: GET }

note

Một vài route tạo ra bởi sfDoctrineRouteCollection có URL giống nhau. Routing vẫn có thể dùng chúng bởi vì chúng yêu cầu các HTTP method khác nhau.

Route job_deletejob_update yêu cầu các HTTP methods không được hỗ trợ bởi trình duyệt (DELETEPUT). Nó vẫn hoạt động bởi vì symfony đã mô phỏng chúng. Mở template _form.php để xem một ví dụ:

// apps/frontend/modules/job/templates/_form.php
<form action="..." ...>
<?php if (!$form->getObject()->isNew()): ?>
  <input type="hidden" name="sf_method" value="PUT" />
<?php endif; ?>
 
<?php echo link_to(
  'Delete',
  'job/delete?id='.$form->getObject()->getId(),
  array('method' => 'delete', 'confirm' => 'Are you sure?')
) ?>

Các symfony helpers có thể nhận bất kì HTTP method nào thông qua tham số sf_method.

note

symfony có nhiều tham số tương tự sf_method, tất cả đều bắt đầu bởi sf_. Ở routes bên trên, bạn có thể thấy tham số:sf_format, sẽ được làm rõ trong một vài ngày tới.

Route Debugging

Khi bạn dùng collection routes, có thể bạn cần list danh sách các route được tạo ra. Lệnh app:routes liệt kê tất cả các routes của một application:

$ php symfony app:routes frontend

Bạn cũng có thể có nhiều thông tin hơn để debug cho route bằng cách cung cấp thêm tham số:

$ php symfony app:routes frontend job_edit

Routes mặc định

Tốt nhất là tạo routes cho tất cả các URL. Nếu bạn làm được điều đó, hãy xóa bỏ hoặc comment các routes mặc định trong file routing.yml:

// apps/frontend/config/routing.yml
#default_index:
#  url:   /:module
#  param: { action: index }
#
#default:
#  url:   /:module/:action/*

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

Hôm nay, chúng ta đã biết thêm rất nhiều kiến thức mới. Chúng ta đã học cách sử dụng routing framework của symfony và cách tách URLs ra khỏi hệ thống xử lý.

Chúng ta sẽ học tutorial mới vào ngày mai, mặc dù mai là thứ 7. Chúng tôi sẽ không giới thiệu một nội dung mới nào, thay vào đó chúng ta sẽ đi sâu vào tìm hiểu những gì đã biết.

Mã nguồn của ngày hôm nay đã được đưa lên kho chứa SVN:

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

This work is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.