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 Propel, 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: sfPropelRoute 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/JobeetCategory.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/JobeetCategory.php public function countActiveJobs() { $criteria = new Criteria(); $criteria->add(JobeetJobPeer::CATEGORY_ID, $this->getId()); return JobeetJobPeer::countActiveJobs($criteria); }
The countActiveJobs()
method uses a countActiveJobs()
method that does not
exist yet in JobeetJobPeer
:
// lib/model/JobeetJobPeer.php class JobeetJobPeer extends BaseJobeetJobPeer { static public function doSelectActive(Criteria $criteria) { return self::doSelectOne(self::addActiveJobsCriteria($criteria)); } 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; } }
As you can see for yourself, we have refactored the whole code of
JobeetJobPeer
to introduce a new shared addActiveJobsCriteria()
method to
make the code more 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.
In the countActiveJobs()
method, instead of using doSelect()
and then
count the number of results, we have used the much faster doCount()
method.
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 propel: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
:
# config/schema.yml propel: jobeet_category: id: ~ name: { type: varchar(255), required: true } slug: { type: varchar(255), required: true, index: unique }
Bây giờ slug
đã là một cột thực sự, bạn cần xóa phương thức getSlug()
ở
JobeetCategory
.
Each time the category
name changes, we need to compute and change the
slug
as well. Let's override the setName()
method:
// lib/model/JobeetCategory.php public function setName($name) { parent::setName($name); $this->setSlug(Jobeet::slugify($name)); }
Dùng lệnh propel:build-all-load
để 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 propel:build-all-load
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 Propel object, symfony cung cấp lớp class:
sfPropelPager
.
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 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(); }
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 sfPropelPager
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
The sfPropelPager::setCriteria()
method takes a Criteria object to use when
selecting the items from the database. Again, we do a bit of refactoring in
the Model:
// lib/model/JobeetCategory.php public function getActiveJobsCriteria() { $criteria = new Criteria(); $criteria->add(JobeetJobPeer::CATEGORY_ID, $this->getId()); return JobeetJobPeer::addActiveJobsCriteria($criteria); }
Now that we have the getActiveJobsCriteria()
method, we can refactor other
JobeetCategory
methods to use it:
// 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(); $criteria->add(JobeetJobPeer::CATEGORY_ID, $this->getId()); return JobeetJobPeer::doCount($criteria); }
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 sfPropelPager
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.