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.
The Propel Criteria Object
Đâ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 = JobeetJobPeer::doSelect(new Criteria()); } // ... }
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) { $criteria = new Criteria(); $criteria->add(JobeetJobPeer::CREATED_AT, time() - 86400 * 30, Criteria::GREATER_THAN); $this->jobeet_job_list = JobeetJobPeer::doSelect($criteria); }
The Criteria::add()
method adds a WHERE
clause to the generated SQL. Here,
we restrict the criteria to only select jobs that are no older than 30 days.
This method has a lot of different comparison operators; here are the most
common ones:
Criteria::EQUAL
Criteria::NOT_EQUAL
Criteria::GREATER_THAN
,Criteria::GREATER_EQUAL
Criteria::LESS_THAN
,Criteria::LESS_EQUAL
Criteria::LIKE
,Criteria::NOT_LIKE
Criteria::CUSTOM
Criteria::IN
,Criteria::NOT_IN
Criteria::ISNULL
,Criteria::ISNOTNULL
Criteria::CURRENT_DATE
,Criteria::CURRENT_TIME
,Criteria::CURRENT_TIMESTAMP
Debug cho Propel từ câu SQL được sinh ra
Chúng ta không trực tiếp viết câu lệnh SQL, Propel 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 Propel; 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 6 15:47:12 symfony [debug] {sfPropelLogger} exec: SET NAMES 'utf8' Dec 6 15:47:12 symfony [debug] {sfPropelLogger} prepare: SELECT jobeet_job.ID, jobeet_job.CATEGORY_ID, jobeet_job.TYPE, jobeet_job.COMPANY, jobeet_job.LOGO, jobeet_job.URL, jobeet_job.POSITION, jobeet_job.LOCATION, jobeet_job.DESCRIPTION, jobeet_job.HOW_TO_APPLY, jobeet_job.TOKEN, jobeet_job.IS_PUBLIC, jobeet_job.CREATED_AT, jobeet_job.UPDATED_AT FROM `jobeet_job` WHERE jobeet_job.CREATED_AT>:p1 Dec 6 15:47:12 symfony [debug] {sfPropelLogger} Binding '2008-11-06 15:47:12' at position :p1 w/ PDO type PDO::PARAM_STR
You can see for yourself that Propel has generated a where clause for the
created_at
column (WHERE jobeet_job.CREATED_AT > :p1
).
note
The :p1
string in the query indicates that Propel generates prepared
statements. The actual value of :p1
('2008-11-06 15:47:12' in the example
above) is passed during the execution of the query and properly escaped by
the database engine. The use of prepared statements dramatically reduces your
exposure to SQL injection .
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 Propel được đưa vào database, bạn cần override phương thức save()
:
// lib/model/JobeetJob.php class JobeetJob extends BaseJobeetJob { public function save(PropelPDO $con = null) { if ($this->isNew() && !$this->getExpiresAt()) { $now = $this->getCreatedAt() ? $this->getCreatedAt('U') : time(); $this->setExpiresAt($now + 86400 * 30); } return parent::save($con); } // ... }
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) { $criteria = new Criteria(); $criteria->add(JobeetJobPeer::EXPIRES_AT, time(), Criteria::GREATER_THAN); $this->jobs = JobeetJobPeer::doSelect($criteria); }
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/020_jobs.yml JobeetJob: # other jobs expired_job: category_id: 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 token: job_expired email: job@example.com
Ngay cả khi giá trị cột created_at
được thêm tự động bởi Propel, 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 propel: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(PropelPDO $con = null) { if ($this->isNew() && !$this->getExpiresAt()) { $now = $this->getCreatedAt() ? $this->getCreatedAt('U') : time(); $this->setExpiresAt($now + 86400 * sfConfig::get('app_active_days')); } return parent::save($con); }
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 đề đó?
The Criteria
code does not belong to the action, it belongs to the
Model layer. As the code returns jobs, let's create a method in the
JobeetJobPeer
class:
// lib/model/JobeetJobPeer.php class JobeetJobPeer extends BaseJobeetJobPeer { static public function getActiveJobs() { $criteria = new Criteria(); $criteria->add(self::EXPIRES_AT, time(), Criteria::GREATER_THAN); return self::doSelect($criteria); } }
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 = JobeetJobPeer::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
:
static public function getActiveJobs() { $criteria = new Criteria(); $criteria->add(self::EXPIRES_AT, time(), Criteria::GREATER_THAN); $criteria->addDescendingOrderByColumn(self::EXPIRES_AT); return self::doSelect($criteria); }
The addDescendingOrderByColumn()
method adds an ORDER BY
clause to the
generated SQL (addAscendingOrderByColumn()
also exists).
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.
Open the JobeetCategoryPeer
class and add a getWithJobs()
method:
// lib/model/JobeetCategoryPeer.php class JobeetCategoryPeer extends BaseJobeetCategoryPeer { static public function getWithJobs() { $criteria = new Criteria(); $criteria->addJoin(self::ID, JobeetJobPeer::CATEGORY_ID); $criteria->add(JobeetJobPeer::EXPIRES_AT, time(), Criteria::GREATER_THAN); $criteria->setDistinct(); return self::doSelect($criteria); } }
The Criteria::addJoin()
method adds a JOIN
clause to the generated SQL.
By default, the join condition is added to the WHERE
clause. You can also
change the join operator by adding a third argument (Criteria::LEFT_JOIN
,
Criteria::RIGHT_JOIN
, and Criteria::INNER_JOIN
).
Sửa lại action index
:
// apps/frontend/modules/job/actions/actions.class.php public function executeIndex(sfWebRequest $request) { $this->categories = JobeetCategoryPeer::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.
For this to work, we need to add the getActiveJobs()
method to the
JobeetCategory
class:
// lib/model/JobeetCategory.php public function getActiveJobs() { $criteria = new Criteria(); $criteria->add(JobeetJobPeer::CATEGORY_ID, $this->getId()); return JobeetJobPeer::getActiveJobs($criteria); }
In the add()
call, we have omitted the third argument as Criteria::EQUAL
is the default value.
When calling the JobeetJobPeer::getActiveJobs()
, we need to pass the current
Criteria
object. So, the getActiveJobs()
needs to merge it with its own
criteria. As the Criteria
is an object, this is quite simple:
// lib/model/JobeetJobPeer.php static public function getActiveJobs(Criteria $criteria = null) { if (is_null($criteria)) { $criteria = new Criteria(); } $criteria->add(JobeetJobPeer::EXPIRES_AT, time(), Criteria::GREATER_THAN); $criteria->addDescendingOrderByColumn(self::EXPIRES_AT); return self::doSelect($criteria); }
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/JobeetCategory.php public function getActiveJobs($max = 10) { $criteria = new Criteria(); $criteria->add(JobeetJobPeer::CATEGORY_ID, $this->getId()); $criteria->setLimit($max); return JobeetJobPeer::getActiveJobs($criteria); }
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 ?>: category_id: 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?
By default, the sfPropelRoute
uses the standard doSelectOne()
method to
retrieve the object, but you can change it by providing a
method_for_criteria
option in the route configuration:
# apps/frontend/config/routing.yml job_show_user: url: /job/:company_slug/:location_slug/:id/:position_slug class: sfPropelRoute options: model: JobeetJob type: object method_for_criteria: doSelectActive param: { module: job, action: show } requirements: id: \d+
The doSelectActive()
method will receive the Criteria
object built by the
route:
// lib/model/JobeetJobPeer.php static public function doSelectActive(Criteria $criteria) { $criteria->add(JobeetJobPeer::EXPIRES_AT, time(), Criteria::GREATER_THAN); return self::doSelectOne($criteria); }
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.