Tóm tắt
Tuần thứ 2 bắt đầu bằng việc giới thiệu về symfony test framework. Hôm nay chúng ta sẽ tiếp tục với form framework.
Form Framework
Bất kì website nào cũng có các form; từ form contact đơn giản đến các form phức tạp với rất nhiều trường. Viết code cho các form là một trong những việc rắc rối và kinh khủng đối với lập trình viên: bạn cần phải viết HTML form, thực hiện việc validation cho mỗi trường, lưu các giá trị vào trong database, hiển thị thông báo lỗi, hiển thị giá trị nhập lỗi, và còn rất nhiều việc nữa...
Tất nhiên, thay vì việc lặp lại tất cả những công việc trên, symfony cung cấp một framework để dễ dàng quản lý form. Form framework gồm 3 thành phần:
- validation: validation sub-framework cung cấp các lớp để kiểm tra dữ liệu nhập vào (integer, string, email address, ...)
- widgets: widget sub-framework cung cấp các lớp để trả về mã HTML (input, textarea, select, ...)
- forms: các lớp form mô tả về các form được tạo từ widget và validator, và nó cũng cung cấp các phương thức để quản lý form. Mỗi form field đã bao gồm validator và widget.
Forms
Một symfony form là một lớp được tạo bởi các field. Mỗi field có tên, validator,
và một widget. Một ContactForm
đơn giản có thể được xác định bởi lớp sau:
class ContactForm extends sfForm { public function configure() { $this->setWidgets(array( 'email' => new sfWidgetFormInput(), 'message' => new sfWidgetFormTextarea(), )); $this->setValidators(array( 'email' => new sfValidatorEmail(), 'message' => new sfValidatorString(array('max_length' => 255)), )); } }
Các form field được cấu hình trong phương thức configure()
, bằng các phương thức setValidators()
và setWidgets()
.
tip
Form framework có sẵn rất nhiều widgets và validators. Bạn có thể tham khảo API để xem tất cả các option, error, và default error message của chúng.
Tên của các lớp widget và validator rất rõ ràng: field email
sẽ được render
thành một HTML <input>
tag (sfWidgetFormInput
) và được validate như một
địa chỉ email (sfValidatorEmail
). Field message
sẽ được render thành một
<textarea>
tag (sfWidgetFormTextarea
), và phải là một chuỗi không có quá
255 kí tự (sfValidatorString
).
Mặc định tất cả các field đều là bắt buộc, tương đương với giá trị của option
required
là true
. Vì thế, validation cho field email
tương đương với
cách viết new sfValidatorEmail(array('required' => true))
.
tip
Bạn có thể nhúng một form vào một form khác bằng phương thức mergeForm()
,
hoặc embedForm()
:
$this->mergeForm(new AnotherForm()); $this->embedForm('name', new AnotherForm());
Propel Forms
Trong đa số các trường hợp, một form thường gồm các trường trong database.
Symfony đã biết mọi thứ về database model, do đó nó có thể tự động
tạo ra các form dựa trên những thông tin này. Khi bạn thực thi lệnh
propel:build-all
trong ngày 3, symfony đã tự động gọi lệnh
propel:build-forms
:
$ php symfony propel:build-forms
Lệnh propel:build-forms
tạo ra các form classe trong thư mục lib/form/
.
Tổ chức các file này tương tự như trong thư mục lib/model
.
Mỗi model class tương ứng với một form class (ví dụ
JobeetJob
ứng với JobeetJobForm
), các lớp này chưa có gì và thừa kế từ base class:
// lib/form/JobeetJobForm.class.php
Customizing the Job Form
Chúng ta sẽ học cách chỉnh sửa form thông qua job form. Chúng ta sẽ thực hiện nó từng bước một.
Đầu tiên, đổi link "Post a Job" ở layout:
<!-- apps/frontend/templates/layout.php -->
<a href="<?php echo url_for('@job_new') ?>">Post a Job</a>
Mặc định, một Propel form hiển thị tất cả các trường trong bảng. Nhưng với job form, một vài trường không được chỉnh sửa bởi người dùng. Bỏ những trường này ra khỏi form bằng cách unset chúng:
// lib/form/JobeetJobForm.class.php class JobeetJobForm extends BaseJobeetJobForm { public function configure() { unset( $this['created_at'], $this['updated_at'], $this['expires_at'], $this['is_activated'] ); } }
Unset một field có nghĩa là cả field widget và validator cũng được bỏ.
Form configuration phải cụ thể hơn những gì được mô tả trong database schema.
Ví dụ, cột email
có kiểu là varchar
trong schema, nhưng chúng ta cần cột
này được validate như một email.
Đổi validate mặc định sfValidatorString
thành sfValidatorEmail
:
// lib/form/JobeetJobForm.class.php public function configure() { // ... $this->validatorSchema['email'] = new sfValidatorEmail(); }
Cột type
cũng có kiểu là varchar
trong schema, nhưng chúng ta muốn giá trị
của nó chỉ là: full time, part time, hoặc freelance.
Đầu tiên, hãy định nghĩa các giá trị trong JobeetJobPeer
:
// lib/model/JobeetJobPeer.php class JobeetJobPeer extends BaseJobeetJobPeer { static public $types = array( 'full-time' => 'Full time', 'part-time' => 'Part time', 'freelance' => 'Freelance', ); // ... }
Sau đó, dùng sfWidgetFormChoice
cho widget type
:
$this->widgetSchema['type'] = new sfWidgetFormChoice(array( 'choices' => JobeetJobPeer::$types, 'expanded' => true, ));
sfWidgetFormChoice
mô tả một choice widget có thể được render thành các
widget khác nhau tùy thuộc vào một vài configuration options (expanded
và
multiple
):
- Dropdown list (
<select>
):array('multiple' => false, 'expanded' => false)
- Dropdown box (
<select multiple="multiple">
):array('multiple' => true, 'expanded' => false)
- List of radio buttons:
array('multiple' => false, 'expanded' => true)
- List of checkboxes:
array('multiple' => true, 'expanded' => true)
note
Nếu bạn muốn một radio button được selecte mặc định (ví dụ full-time
), bạn có thể đổi giá trị mặc định trong database schema.
Mặc dù bạn nghĩ rằng không thể submit một giá trị khác, nhưng một hacker có thể bypass dễ dàng một widget choices bằng cách sử dụng các công cụ như curl hoặc Firefox Web Developer Toolbar. Hãy sửa lại validator để bắt buộc giá trị phải trong các lựa chọn đưa ra:
$this->validatorSchema['type'] = new sfValidatorChoice(array( 'choices' => array_keys(JobeetJobPeer::$types), ));
Cột logo
sẽ chứa tên của file ảnh logo, chúng ta cần đổi widget thành một
file input tag:
$this->widgetSchema['logo'] = new sfWidgetFormInputFile(array( 'label' => 'Company logo', ));
Với mỗi field, symfony tự động tạo ra một label (được dùng khi render <label>
tag). Ta có thể thay đổi nó với label
option.
Bạn cũng có thể thay đổi nhiều label một lúc bằng cách cung cấp một mảng cho
phương thức setLabels()
:
$this->widgetSchema->setLabels(array( 'category_id' => 'Category', 'is_public' => 'Public?', 'how_to_apply' => 'How to apply?', ));
Chúng ta cũng cần thay đổi validator mặc định:
$this->validatorSchema['logo'] = new sfValidatorFile(array( 'required' => false, 'path' => sfConfig::get('sf_upload_dir').'/jobs', 'mime_types' => 'web_images', ));
sfValidatorFile
thực sự thú vị, nó thực hiện một số công việc:
- Validates file upload lên phải là ảnh (
mime_types
) - đổi tên file thành duy nhất
- lưu file vào
path
được chỉ ra - cập nhật cột
logo
với tên file tạo ra
note
Bạn cần tạo thư mục logo (web/uploads/jobs
) và chắc rằng web server có quyền
ghi vào thư mục này.
Validator sẽ lưu tên file vào database, sửa lại logo hiển thị trong template
showSuccess
:
// apps/frontend/modules/job/template/showSuccess.php <img src="/uploads/jobs/<?php echo $job->getLogo() ?>" alt="<?php echo $job->getCompany() ?> logo" />
tip
Nếu một phương thức generateLogoFilename()
đã tồn tại trong form, validator sẽ gọi phương thức này
và override kết quả mặc định.
Phương thức này nhận sfValidatedFile
object làm tham số.
Tương tự như label, bạn cũng có thể tạo một help message. Thêm một help message
cho cột is_public
để mô tả ý nghĩa của cột này:
$this->widgetSchema->setHelp('is_public', 'Whether the job can also be published on affiliate websites or not.');
Lớp JobeetJobForm
sau khi đã chỉnh sửa:
// lib/form/JobeetJobForm.class.php class JobeetJobForm extends BaseJobeetJobForm { public function configure() { unset( $this['created_at'], $this['updated_at'], $this['expires_at'], $this['is_activated'] ); $this->validatorSchema['email'] = new sfValidatorEmail(); $this->widgetSchema['type'] = new sfWidgetFormChoice(array( 'choices' => JobeetJobPeer::$types, 'expanded' => true, )); $this->validatorSchema['type'] = new sfValidatorChoice(array( 'choices' => array_keys(JobeetJobPeer::$types), )); $this->widgetSchema['logo'] = new sfWidgetFormInputFile(array( 'label' => 'Company logo', )); $this->validatorSchema['logo'] = new sfValidatorFile(array( 'required' => false, 'path' => sfConfig::get('sf_upload_dir').'/jobs', 'mime_types' => 'web_images', )); $this->widgetSchema->setHelp('is_public', 'Whether the job can also be published on affiliate websites or not.'); } }
Form Template
Bây giờ form class đã được chỉnh sửa. chúng ta cần hiển thị nó. Template cho
form này là nơi bạn muốn tạo một công việc mới hoặc sửa một công việc đã có.
Cả 2 template newSuccess.php
và editSuccess.php
đều tương tự nhau:
<!-- apps/frontend/modules/job/templates/newSuccess.php --> <?php use_stylesheet('job.css') ?> <h1>Post a Job</h1> <?php include_partial('form', array('form' => $form)) ?>
Form được render trong partial _form
. Thay nội dung được tạo ra trong partial
_form
bằng đoạn code sau:
<!-- apps/frontend/modules/job/templates/_form.php --> <?php include_stylesheets_for_form($form) ?> <?php include_javascripts_for_form($form) ?> <?php echo form_tag_for($form, '@job') ?> <table id="job_form"> <tfoot> <tr> <td colspan="2"> <input type="submit" value="Preview your job" /> </td> </tr> </tfoot> <tbody> <?php echo $form ?> </tbody> </table> </form>
2 helper include_javascripts_for_form()
và include_stylesheets_for_form()
include JavaScript và stylesheet cần thiết cho form widgets.
tip
Mặc dù job form không cần bất kì file JavaScript hay stylesheet nào, nhưng ta vẫn gọi những helper phòng khi cần tới. Ví dụ khi bạn muốn thay đổi một widget yêu cầu một vài JavaScript hay stylesheet.
Helper form_tag_for()
tạo ra một <form>
tag ứng với các tham số form và
route và thay đổi HTTP methods thành POST
hoặc PUT
phụ thuộc vào
object là new hay không. Nó cũng tự thêm multipart
attribute nếu
form có file input tags.
Cuối cùng, <?php echo $form ?>
render các form widget.
Form Action
Chúng ta đã có một form class và một template được render. Bây giờ chúng ta cần thực hiện các công việc xử lý với một vài action.
Job form được quản lý bởi 5 phương thức trong module job
:
- new: hiển thị form để tạo công việc
- edit: hiển thị form để sửa công việc
- create: tạo công việc mới khi user submit
- update: sửa công việc đã có khi user submit
- processForm: gọi bởi
create
vàupdate
, thực thi các công việc: validation, form repopulation, and serialization to the database
Các form có vòng đời như sau:
Chúng ta đã tạo một Propel route collection ở ngày 5 cho module job
,
code của các phương thức quản lý form như sau:
// apps/frontend/modules/job/actions/actions.class.php public function executeNew(sfWebRequest $request) { $this->form = new JobeetJobForm(); } public function executeCreate(sfWebRequest $request) { $this->form = new JobeetJobForm(); $this->processForm($request, $this->form); $this->setTemplate('new'); } public function executeEdit(sfWebRequest $request) { $this->form = new JobeetJobForm($this->getRoute()->getObject()); } public function executeUpdate(sfWebRequest $request) { $this->form = new JobeetJobForm($this->getRoute()->getObject()); $this->processForm($request, $this->form); $this->setTemplate('edit'); } protected function processForm(sfWebRequest $request, sfForm $form) { $form->bind( $request->getParameter($form->getName()), $request->getFiles($form->getName()) ); if ($form->isValid()) { $job = $form->save(); $this->redirect($this->generateUrl('job_show', $job)); } }
KHi bạn truy cập trang /job/new
, một form instance được tạo ra và truyền cho template (new
action).
Khi user submit form (create
action), giá trị user cung cấp được truyền vào (bind()
method) và validation.
Khi form ở trạng thái bound, nó được validate và qua phương thức isValid()
ta biết được form được valid (returns true
) hay không, nếu valid job sẽ được lưu vào database ($form->save()
), và user được chuyển sang trang job
preview; nếu không, template newSuccess.php
sẽ được hiển thi với giá trị user đã submit kèm thông báo lỗi.
tip
Phương thức setTemplate()
thay đổi template dùng cho action. Nếu
submitted form không valid, phương thức create
và update
dùng các
template của new
và edit
action để hiển thị lại
form với thông báo lỗi.
Chỉnh sửa một công việc có sẵn hoàn toàn tương tự. Chỉ có một điểm khác duy nhất
giữa new
và edit
action là job object để sửa phải được cung cấp trong phương thức khởi tạo
của form. Đối tượng này sẽ được sử dụng cho các giá trị mặc định của widget trong template (các giá trị mặc định là một object đối với Propel forms, nhưng là một mảng trong trường hợp form đơn giản).
Bạn cũng có thể tự xác định các giá trị mặc định trong creation form. Cách đầu tiên là khai báo các giá trị mặc định này trong database schema. Cách khác là cung cấp Job
object cho phương thức khởi tạo form:
// apps/frontend/modules/job/actions/actions.class.php public function executeNew(sfWebRequest $request) { $job = new JobeetJob(); $job->setType('full-time'); $this->form = new JobeetJobForm($job); }
note
Khi form ở trạng thai bound, giá trị mặc định được thay thế bởi giá trị user submitted. Giá trị user submit sẽ được dùng để hiển thị lại trong trường hợp có lỗi.
Bảo vệ Job Form với một Token
Mọi thứ hiện đều hoạt động tốt. Nhưng có một vấn đề. Đầu tiên, job token
phải được tạo tự động khi một job mới được tạo, chứ chúng ta không muốn
user cung cấp giá trị này. Update phương thức save()
của JobeetJob
:
// lib/model/JobeetJob.php public function save(PropelPDO $con = null) { // ... if (!$this->getToken()) { $this->setToken(sha1($this->getEmail().rand(11111, 99999))); } return parent::save($con); }
Chúng ta cũng bỏ token
field ra khỏi form:
// lib/form/JobeetJobForm.class.php class JobeetJobForm extends BaseJobeetJobForm { public function configure() { unset( $this['created_at'], $this['updated_at'], $this['expires_at'], $this['is_activated'], $this['token'] ); // ... } // ... }
Nếu bạn nhớ lại kịch bản trong ngày 2, một job có thể được chỉnh sửa chỉ cần user biết được token của nó. Đúng vậy, đó là cách đơn giản để chỉnh sửa hay xoá một công việc dựa trên URL.s
Mặc định, sfPropelRouteCollection
route tạo ra URLs với primary key, nhưng ta có thể đổi thành bất kì unique column nào bằng cách cung cấp column
option:
# apps/frontend/config/routing.yml job: class: sfPropelRouteCollection options: { model: JobeetJob, column: token } requirements: { token: \w+ }
Bây giờ, tất cả các route, ngoại trừ job_show_user
, đều chưas token. Ví dụ, route đề sửa một jobs:
http://jobeet.localhost/job/TOKEN/edit
Bạn cũng cần sửa lại link "Edit" trong template showSuccess
:
<!-- apps/frontend/modules/job/templates/showSuccess.php -->
<a href="<?php echo url_for('job_edit', $job) ?>">Edit</a>
note
Chúng ta cũng thay đổi requirements cho cột token
thay cho
requirements mặc định \d+
đối với primary key.
Trang Preview
Trang preview tương tự như trang hiển thị một job. Nhờ có routing, một user có thể truy cập nếu cung cấp đúng tokens.
Nếu user truy cập dựa trên tokenized URL, chúng ta sẽ thêm một admin bar ở đầu. Ở đầu showSuccess
template, thêm một partial để chứa admin bar và bỏ link edit
ở cuối đi:
<!-- apps/frontend/modules/job/templates/showSuccess.php --> <?php if ($sf_request->getParameter('token') == $job->getToken()): ?> <?php include_partial('job/admin', array('job' => $job)) ?> <?php endif; ?>
Sau đó, tạo _admin
partial:
<!-- apps/frontend/modules/job/templates/_admin.php --> <div id="job_actions"> <h3>Admin</h3> <ul> <?php if (!$job->getIsActivated()): ?> <li><?php echo link_to('Edit', 'job_edit', $job) ?></li> <li><?php echo link_to('Publish', 'job_edit', $job) ?></li> <?php endif; ?> <li><?php echo link_to('Delete', 'job_delete', $job, array('method' => 'delete', 'confirm' => 'Are you sure?')) ?></li> <?php if ($job->getIsActivated()): ?> <li<?php $job->expiresSoon() and print ' class=" expires_soon"' ?>> <?php if ($job->isExpired()): ?> Expired <?php else: ?> Expires in <strong><?php echo $job->getDaysBeforeExpires() ?></strong> days <?php endif; ?> <?php if ($job->expiresSoon()): ?> - <a href="">Extend</a> for another <?php echo sfConfig::get('app_active_days') ?> days <?php endif; ?> </li> <?php else: ?> <li> [Bookmark this <?php echo link_to('URL', 'job_show', $job, true) ?> to manage this job in the future.] </li> <?php endif; ?> </ul> </div>
Có rất nhiều code, nhưng phần lớn đều đơn giản và dễ hiểu. Admin bar thay đối phụ thuộc vào trang thái công việc:
Để template dễ đọc, ta thêm các methods vào JobeetJob
class:
// lib/model/JobeetJob.php public function getTypeName() { return $this->getType() ? JobeetJobPeer::$types[$this->getType()] : ''; } public function isExpired() { return $this->getDaysBeforeExpires() < 0; } public function expiresSoon() { return $this->getDaysBeforeExpires() < 5; } public function getDaysBeforeExpires() { return floor(($this->getExpiresAt('U') - time()) / 86400); }
Job Activation and Publication
Trong mục trước, có một link để publish một công việc. Link cần thay đổi
để trỏ tới publish
action. Thay vì tạo một route mới, chúng ta có thể sửa lại route job
:
# apps/frontend/config/routing.yml job: class: sfPropelRouteCollection options: model: JobeetJob column: token object_actions: { publish: put } requirements: token: \w+
object_actions
nhận một mảng các action thêm vào. Bây giờ, chúng ta sửa lại link "Publish":
<!-- apps/frontend/modules/job/templates/_admin.php --> <li> <?php echo link_to('Publish', 'job_publish', $job, array('method' => 'put')) ?> </li>
Cuối cùng tạo publish
action:
// apps/frontend/modules/job/actions/actions.class.php public function executePublish(sfWebRequest $request) { $request->checkCSRFProtection(); $job = $this->getRoute()->getObject(); $job->publish(); $this->getUser()->setFlash('notice', sprintf('Your job is now online for %s days.', sfConfig::get('app_active_days'))); $this->redirect($this->generateUrl('job_show_user', $job)); }
Chúng ta đã enabled CSRF protection, helper link_to()
chứa một CSRF
token và phương thức checkCSRFProtection()
của request object
kiểm trae validity của giá trị submit.
Phương thức executePublish()
sử dụng publish()
method:
// lib/model/JobeetJob.php public function publish() { $this->setIsActivated(true); $this->save(); }
Bây giờ bạn có thể test tính năng publish trên trình duyệt.
Nhưng chúng ta vẫn có một vài thứ cần sửa. Các công việc non-activated phải không được truy cập, nghĩa là chúng phải không được hiển thị ở trang chủ, và không được truy cập thông qua URL. Chúng ta đã tạo phương thức
addActiveJobsQuery
để lấy các active jobs, chúng ta cần sửa lại nó:
// lib/model/JobeetJobPeer.php static public function addActiveJobsCriteria(Criteria $criteria = null) { // ... $criteria->add(self::IS_ACTIVATED, true); return $criteria; }
Bây giờ, bạn có thể kiểm tra lại trên trình duyệt. Tất cả các công việc non-activated đã biến mất khỏi trang chủ; và ngay cả khi chúng ta biết URLs của chúng, chúng ta cũng không thể truy cập được. Tuy nhiên, chúng ta vẫn có thể truy cập được nếu dựa trên token URL. Khi đó công việc sẽ được hiển thị kèm với admin bar.
Đó là một trong những lợi ích của MVC pattern. Chúng ta chỉ cần một thay đổi nhỏ trong một phuơng thức để thêm một tính năng mới.
note
khi tạo phương thức getWithJobs()
, chúng ta đã không sử dụng
addActiveJobsQuery
. Do đó, chúng ta cần sửa lại phương thức này:
class JobeetCategoryPeer extends BaseJobeetCategoryPeer { static public function getWithJobs() { // ... $criteria->add(JobeetJobPeer::IS_ACTIVATED, true); return $criteria; }
Hẹn gặp lại ngày mai
Hôm nay chúng ta đã học rất nhiều kiến thức mới, hi vọng bạn đã hiểu về symfony's form framework.
Có thể bạn đã nhận thấy rằng chúng ta đã quên một vài thứ hôm nay... Chúng ta đã không viết bất kì test nào cho tính năng mới. Bởi vì viết test là một phần quan trọng trong việc phát triển một ứng dụng, chúng ta sẽ làm nó vào ngày mai.
This work is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.