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
module
và action
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:
Đó 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()
và 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));
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_delete
và job_update
yêu cầu các HTTP methods không được hỗ trợ
bởi trình duyệt (DELETE
và PUT
). 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.