Tóm tắt
Hôm qua là một ngày tuyệt vời. Chúng ta đã học cách tạo một URL dễ nhìn và cách dùng symfony framework để tự động làm nhiều việc cho chúng ta.
Hôm nay, chúng ta sẽ chi tiết thêm Jobeet website bằng cách chỉnh sửa code đã có. Trong quá trình đó, bạn sẽ được học thêm về những tính năng chúng tôi đã giới thiệu trong tuần này.
Truy vấn đối tượng Doctrine
Đây là yêu cầu đề ra trong ngày 2:
"Khi người dùng vào trang Jobeet, anh ta sẽ thấy danh sách các công việc mới nhất."
Nhưng hiện tại tất cả các công việc đều được hiển thị:
class jobActions extends sfActions { public function executeIndex(sfWebRequest $request) { $this->jobeet_job_list = Doctrine::getTable('JobeetJob') ->createQuery('a') ->execute(); } // ... }
Một active job là một công việc được đưa lên trong 30 ngày gần nhất. Ở đây, chúng ta đã lấy tất cả các công việc trong cơ sở dữ liệu.
Ta cần sửa lại để chỉ lấy các active job:
public function executeIndex(sfWebRequest $request) { $q = Doctrine_Query::create() ->from('JobeetJob j') ->where('j.created_at > ?', date('Y-m-d h:i:s', time() - 86400 * 30)); $this->jobeet_job_list = $q->execute(); }
Debug cho Doctrine từ câu SQL được sinh ra
Chúng ta không trực tiếp viết câu lệnh SQL, Doctrine sẽ sinh ra câu SQL phù hợp với database engines mà chúng ta đã chọn trong cấu hình. Đôi khi, chúng ta muốn xem câu SQL được tạo ra bởi Doctrine; ví dụ, khi chúng ta muốn debug một câu
truy vấn làm việc không như mong muốn. Trong môi trường dev
, symfony
lưu lại những câu truy vấn này (và nhiều thứ khác) trong thư mục log/
.
Mỗi file log ứng với một application trong một môi trường nào đó.
File chúng ta cần tìm có tên frontend_dev.log
:
# log/frontend_dev.log Dec 04 13:58:33 symfony [info] {sfDoctrineLogger} executeQuery : SELECT j.id AS j__id, j.category_id AS j__category_id, j.type AS j__type, j.company AS j__company, j.logo AS j__logo, j.url AS j__url, j.position AS j__position, j.location AS j__location, j.description AS j__description, j.how_to_apply AS j__how_to_apply, j.token AS j__token, j.is_public AS j__is_public, j.is_activated AS j__is_activated, j.email AS j__email, j.expires_at AS j__expires_at, j.created_at AS j__created_at, j.updated_at AS j__updated_at FROM jobeet_job j WHERE j.created_at > ? (2008-11-08 01:13:35)
Bạn có thể thấy rằng Doctrine đã tạo một mệnh đề where cho cột created_at
(WHERE j.created_at > ?
).
note
Kí tự ?
trong câu truy vấn cho biết rằng Doctrine tạo ra một câu lệnh chuẩn bị.
Giá trị thực sự của ?
('2008-11-08 01:13:35' trong ví dụ trên)
được dùng trong quá trình thực thi câu truy vấn và được escape bởi
database engine. Việc này giúp giảm nguy cơ từ tấn công SQL injection attacks.
Xem từ file log khá tiên lợi, nhưng có một chút bất tiện khi phải chuyển qua lại giữa trình duyệt, IDE, và file log mỗi khi bạn cần kiểm tra sự thay đổi. Nhờ có symfony web debug toolbar, bạn có thể xem tất cả các thông tin cần thiết này từ trình duyệt:
Object Serialization
Mặc dù code trên hoạt đông, nhưng nó vẫn chưa đúng là những gì chúng ta muốn:
"Người dùng có thể kích hoạt lại hoặc tăng thời hạn cho công việc thêm 30 ngày nữa..."
Điều này là không thể thực hiện với code ở trên, do giá trị created_at
chỉ được xác định một lần khi tạo job mới.
Hãy nhớ lại database schema, chúng ta có một cột expires_at
. Hiện tại giá trị
này để trống. Khi một công việc được tạo, nó phải được thiết lập giá trị là
ngày cách ngày hiện tại 30 ngày. Để thực hiện một việc gì đó trước khi một
đối tượng Doctrine được đưa vào database, bạn cần override phương thức save()
:
// lib/model/doctrine/JobeetJob.class.php class JobeetJob extends BaseJobeetJob { public function save(Doctrine_Connection $conn = null) { if ($this->isNew() && !$this->getExpiresAt()) { $now = $this->getCreatedAt() ? strtotime($this->getCreatedAt()) : time(); $this->setExpiresAt(date('Y-m-d h:i:s', $now + 86400 * 30)); } return parent::save($conn); } // ... }
Phương thức isNew()
trả về true
nếu đối tượng chưa được ghi vào database, và false
trong trường hợp ngược lại.
Sửa lại câu truy vấn trong action sử dụng cột expires_at
:
public function executeIndex(sfWebRequest $request) { $q = Doctrine_Query::create() ->from('JobeetJob j') ->where('j.expires_at > ?', date('Y-m-d h:i:s', time())); $this->jobs = $q->execute(); }
Chúng ta đã truy vấn để chỉ lấy các jobs còn thời hạn.
More with Fixtures
Refresh lại trang chủ bạn sẽ thấy không có gì thay đổi vì các job trong database chỉ mới đưa lên vài ngày trước. Hãy thêm một công việc đã hết hạn:
# data/fixtures/jobs.yml JobeetJob: # other jobs expired_job: JobeetCategory: programming company: Sensio Labs position: Web Developer location: Paris, France description: Lorem ipsum dolor sit amet, consectetur adipisicing elit. how_to_apply: Send your resume to lorem.ipsum [at] dolor.sit is_public: true is_activated: true created_at: '2005-12-01 00:00:00' token: job_expired email: job@example.com
Ngay cả khi giá trị cột created_at
được thêm tự động bởi Doctrine, bạn vẫn
có thể override nó. Load lại file fixtures và refresh lại trình duyệt bạn sẽ thấy rằng công việc quá hạn không được hiển thị:
$ php symfony doctrine:data-load
Custom Configuration
Trong phương thức JobeetJob::save()
, chúng ta đã để cố định cho số ngày mà một công việc trở thành quá hạn. Ta nên để số ngày này có thể cấu hình được.
Symfony framework cung cấp sẵn file cho các cấu hình của application:app.yml
. File YAML này có thể chứa bất kì cấu hình nào bạn muốn:
# apps/frontend/config/app.yml all: active_days: 30
Trong application, những thiết lập này có thể được truy cập thông qua global sfConfig
class:
sfConfig::get('app_active_days')
Tên của setting được gán app_
đằng trước vì lớp sfConfig
có thể truy cập đến
tất cả các symfony settings mà chúng ta sẽ thấy ở phần sau.
Hãy sửa lại code với setting vừa tạo:
public function save(Doctrine_Connection $conn = null) { if ($this->isNew() && !$this->getExpiresAt()) { $now = $this->getCreatedAt() ? strtotime($this->getCreatedAt()) : time(); $this->setExpiresAt(date('Y-m-d h:i:s', $now + 86400 * sfConfig::get('app_active_days'))); } return parent::save($conn); }
File app.yml
là nơi lý tưởng để chứa các global settings của application.
Refactoring
Mặc dù code chúng ta đã viết hoạt động tốt, nhưng nó vẫn còn vài vấn đề. Bạn có thể chỉ ra được những vấn đề đó?
Code Doctrine_Query
không thuộc action, nó thuộc tầng Model. Do code trả về danh sách các job, nên chúng ta tạo phương thức này trong lớp JobeetJobTable
:
// lib/model/doctrine/JobeetJobTable.class.php class JobeetJobTable extends Doctrine_Table { public function getActiveJobs() { $q = $this->createQuery('j') ->where('j.expires_at > ?', date('Y-m-d h:i:s', time())); return $q->execute(); } }
Bây giờ, ở action ta gọi phương thức vừa tạo để nhận các active job.
public function executeIndex(sfWebRequest $request) { $this->jobeet_job_list = Doctrine::getTable('JobeetJob')->getActiveJobs(); }
Việc refactoring này có vài lợi ích so với code trước:
- Việc xử lý để nhận các active job bây giờ đã nằm đúng chỗ của nó trong Model
- Code trong controller trở nên dễ đọc hơn
- Phương thức
getActiveJobs()
có thể được dùng lại (ví dụ trong action khác) - Model code bây giờ có thể unit test
Hãy sắp xếp các công việc theo expires_at
:
public function getActiveJobs() { $q = $this->createQuery('j') ->where('j.expires_at > ?', date('Y-m-d h:i:s', time())) ->orderBy('j.expires_at DESC'); return $q->execute(); }
Phương thức orderBy
thêm mệnh đề ORDER BY
vào trong câu SQL (ta cũng có addOrderBy()
).
Categories on the Homepage
Từ yêu cầu ở ngày 2:
"Công việc được hiển thị theo category và xếp theo ngày public (công việc mới ở trên)."
Hiện tại, chúng ta chưa nhóm các công việc theo category. Đầu tiên, chúng ta cần lấy tất cả các category có ít nhất một active job.
Thêm phương thức getWithJobs()
vào lớp JobeetCategoryTable
:
// lib/model/doctrine/JobeetCategoryTable.class.php class JobeetCategoryTable extends Doctrine_Table { public function getWithJobs() { $q = $this->createQuery('c') ->leftJoin('c.JobeetJob j') ->where('j.expires_at > ?', date('Y-m-d h:i:s', time())); return $q->execute(); } }
Sửa lại action index
:
// apps/frontend/modules/job/actions/actions.class.php public function executeIndex(sfWebRequest $request) { $this->categories = Doctrine::getTable('JobeetCategory')->getWithJobs(); }
Trong template, chúng ta cần duyệt qua tất cả các categories và hiển thị các active job:
// apps/frontend/modules/job/indexSuccess.php <?php use_stylesheet('jobs.css') ?> <div id="jobs"> <?php foreach ($categories as $category): ?> <div class="category_<?php echo Jobeet::slugify($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> </div> <?php endforeach; ?> </div>
note
Để hiển thị tên của category trong template, chúng ta viết echo $category
.
Điều này có vẻ bất thường? $category
là một đối tượng, làm thế nào mà echo
lại hiển thị ra được tên của category? Câu trả lời đã có trong ngày 3 khi
chúng ta dùng magic method __toString()
trong tất cả các model classes.
Để code trên chạy được, chúng ta cần thêm phương thức getActiveJobs()
vào lớp
JobeetCategory
:
// lib/model/doctrine/JobeetCategory.class.php public function getActiveJobs() { $q = Doctrine_Query::create() ->from('JobeetJob j') ->where('j.category_id = ?', $this->getId()); return Doctrine::getTable('JobeetJob')->getActiveJobs($q); }
Khi gọi Doctrine::getTable('JobeetJob')->getActiveJobs()
, chúng ta cần
cung cấp đối tượng Doctrine_Query
hiện tại. Vì thế, trong getActiveJobs()
ta cần thêm vào câu truy vấn của chúng ta. Do Doctrine_Query
là object, nên
việc này rất đơn giản:
// lib/model/doctrine/JobeetJobTable.class.php public function getActiveJobs(Doctrine_Query $q = null) { if (is_null($q)) { $q = Doctrine_Query::create() ->from('JobeetJob j'); } $q->andWhere('j.expires_at > ?', date('Y-m-d h:i:s', time())) ->addOrderBy('j.expires_at DESC'); return $q->execute(); }
Limit the Results
Vẫn còn một yêu cầu cần thực hiện trong trang chủ:
"Với mỗi category, ta chỉ list 10 công việc đầu tiên và có một link cho phép xem tất cả các công việc của category."
Điều này có thể thực hiện đơn giản bằng cách thêm tham số vào phương thức
getActiveJobs()
:
// lib/model/doctrine/JobeetCategory.class.php public function getActiveJobs($max = 10) { $q = Doctrine_Query::create() ->from('JobeetJob j') ->where('j.category_id = ?', $this->getId()) ->limit($max); return Doctrine::getTable('JobeetJob')->getActiveJobs($q); }
Giá trị trong mệnh đề LIMIT
có thể cấu hình được. Sửa lại template để lấy số công việc được cấu hình trong app.yml
:
<?php foreach ($category->getActiveJobs(sfConfig::get('app_max_jobs_on_homepage')) as $i => $job): ?>
và thêm setting vào file app.yml
:
all: active_days: 30 max_jobs_on_homepage: 10
Dynamic Fixtures
Trừ khi bạn sửa giá trị của max_jobs_on_homepage
thành 1, nếu không bạn sẽ thấy trang chủ không có gì thay đổi. Chúng ta cần thêm một số job nữa vào file
fixtures. Bạn có thể phải copy hàng chục lần những job đã có ... nhưng có một
cách tốt hơn.Nên tránh việc lặp lại, kể cả trong file fixture!
File YAML trong symfony có thể chứa code PHP code, nó sẽ được dịch ra trước khi parse. Thêm đoạn code sau vào cuối file fixtures:
JobeetJob: # Starts at the beginning of the line (no whitespace before) <?php for ($i = 100; $i <= 130; $i++): ?> job_<?php echo $i ?>: JobeetCategory: programming company: Company <?php echo $i."\n" ?> position: Web Developer location: Paris, France description: Lorem ipsum dolor sit amet, consectetur adipisicing elit. how_to_apply: | Send your resume to lorem.ipsum [at] company_<?php echo $i ?>.sit is_public: true is_activated: true token: job_<?php echo $i."\n" ?> email: job@example.com <?php endfor; ?>
Hãy chú ý các khoảng lùi đầu dòng.Làm theo những chỉ dẫn đơn giản sau khi bạn thêm code PHP vào file YAML:
<?php ?>
phải để ở đầu dòng hoặc để trong một giá trị.- Nếu
<?php ?>
kết thúc một dòng, bạn cần output thêm kí tự xuống dòng ("\n").
Secure the Job Page
Khi một công việc hết hạn, nếu bạn biết URL, bạn vẫn có thể truy cập nó. Hãy thử truy cập vào một công việc hết hạn theo URL (thay id
bằng id
trong database của bạn):
/frontend_dev.php/job/sensio-labs/paris-france/4/web-developer-expired
Thay vì hiển thị công việc, chúng ta cần chuyển người dùng đến trang 404. Làm thế nào chúng ta có thể làm được điều này khi job được nhận tự động bởi route?
# apps/frontend/config/routing.yml job_show_user: url: /job/:company_slug/:location_slug/:id/:position_slug class: sfDoctrineRoute options: model: JobeetJob type: object method_for_query: retrieveActiveJob param: { module: job, action: show } requirements: id: \d+
Phương thức retrieveActiveJob
nhận đối tượng Doctrine_Query
tạo bởi route:
// lib/model/doctrine/JobeetJobTable.class.php public function retrieveActiveJob(Doctrine_Query $q) { $q->andWhere('a.expires_at > ?', date('Y-m-d h:i:s', time())); return $q->fetchOne(); }
Bây giờ, nếu bạn truy cập một công việc hết hạn, bạn sẽ được chuyển sang trang 404.
Link to the Category Page
Bây giờ, hãy thêm một link tới trang category ở trang chủ và tạo trang category.
Nhưng hãy đợi chút. Hôm nay là thứ 7, vì thế chúng ta sẽ không làm quá nhiều. Và bạn cũng đủ kiến thức để làm việc này! Hãy tự làm nó và chúng ta sẽ kiểm tra lại vào ngày mai.
Hẹn gặp lại ngày mai
Hãy tự bổ sung những thứ trên cho Jobeet project của mình. Sử dụng API documentation và documentation khi bạn cần giúp đỡ.
Chúc may mắn!
Bạn có thể checkout mã nguồn ngày hôm nay tại:
http://svn.jobeet.org/tags/release_day_06/
This work is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.