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ạigetNbResults()
: trả về tổng số kết quảhaveToPaginate()
: trả vềtrue
nếu có nhiều hơn một tranggetLinks()
: trả về danh sách link đến các tranggetPage()
: trả về số của trang hiện tạigetPreviousPage()
: trả về số của trang trướcgetNextPage()
: trả về số của trang tiếpgetLastPage()
: 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/
This work is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.