316
Giới thiệu Framework symfony trở thành dự án mã nguồn mở cách đây 3 năm và đã trở thành một trong những frameworks PHP phổ biến nhất nhờ những tính năng tuyệt vời và tài liệu phong phú. Tháng 12-2005, sau khi symfony ra phiên bản đầu tiên, chúng tôi đã ra mắt "Askeet tutorial ", gồm 24 bài hướng dẫn, được đưa lên từng ngày từ 1/12 đến giáng sinh. Hướng dẫn này là một công cụ vô giá để quảng bá framework đến những người mới bắt đầu sử dụng. Rất nhiều lập trình viên học symfony thích thú với askeet, và nhiều công ty vẫn dùng askeet làm tài liệu đào tạo. Chúng ta vừa chào đón symfony 1.2 và askeet tutorial trở nên lạc hậu. Vì thế chúng ta cần một loạt bài hướng dẫn mới. Chào mừng đến với Jobeet, loạt bài hướng dẫn của năm 2008! Thử thách Đúng vậy, chúng tôi lại thực hiện nó. Mỗi ngày kể cả cuối tuần, một bài hướng dẫn mới sẽ được đưa lên. Mỗi bài hướng dẫn sẽ mất khoảng một giờ để thực hành, và là cơ hội để học symfony bằng cách code một website thực sự, từ đầu đến cuối. 1 giờ/1 bài hướng dẫn x 24 bài hướng dẫn = 1 ngày, đó chính là khoảng thời gian mà chúng tôi nghĩ một lập trình viên cần để học cơ bản về symfony. Mỗi ngày, các tính năng mới sẽ được thêm vào ứng dụng, đồng thời các chức năng mới của symfony cũng được giới thiệu. Với askeet, ngày thứ 21 là "get-a-symfony-guru-for-a-day". Chúng tôi không có kế hoạch cho ngày này, cộng đồng sẽ đề xuất một tính năng để thêm vào askeet. Và cộng đồng quyết định chúng ta cần một search engine cho ứng dụng. Và chúng tôi đã làm nó.

lập trình symfony tiếng việt

Embed Size (px)

DESCRIPTION

hướng dân lạp trình symfony dành cho người mới bắt đầu

Citation preview

Giới thiệu

Framework symfony trở thành dự án mã nguồn mở cách đây 3 năm và đã trở thành một trong những frameworks PHP phổ biến nhất nhờ những tính năng tuyệt vời và tài liệu phong phú.

Tháng 12-2005, sau khi symfony ra phiên bản đầu tiên, chúng tôi đã ra mắt "Askeet tutorial", gồm 24 bài hướng dẫn, được đưa lên từng ngày từ 1/12 đến giáng sinh.

Hướng dẫn này là một công cụ vô giá để quảng bá framework đến những người mới bắt đầu sử dụng. Rất nhiều lập trình viên học symfony thích thú với askeet, và nhiều công ty vẫn dùng askeet làm tài liệu đào tạo.

Chúng ta vừa chào đón symfony 1.2 và askeet tutorial trở nên lạc hậu. Vì thế chúng ta cần một loạt bài hướng dẫn mới.

Chào mừng đến với Jobeet, loạt bài hướng dẫn của năm 2008!

Thử thách

Đúng vậy, chúng tôi lại thực hiện nó. Mỗi ngày kể cả cuối tuần, một bài hướng dẫn mới sẽ được đưa lên. Mỗi bài hướng dẫn sẽ mất khoảng một giờ để thực hành, và là cơ hội để học symfony bằng cách code một website thực sự, từ đầu đến cuối.

1 giờ/1 bài hướng dẫn x 24 bài hướng dẫn = 1 ngày, đó chính là khoảng thời gian mà chúng tôi nghĩ một lập trình viên cần để học cơ bản về symfony. Mỗi ngày, các tính năng mới sẽ được thêm vào ứng dụng, đồng thời các chức năng mới của symfony cũng được giới thiệu.

Với askeet, ngày thứ 21 là "get-a-symfony-guru-for-a-day". Chúng tôi không có kế hoạch cho ngày này, cộng đồng sẽ đề xuất một tính năng để thêm vào askeet. Và cộng đồng quyết định chúng ta cần một search engine cho ứng dụng. Và chúng tôi đã làm nó.

Với Jobeet, chúng tôi dự định ngày thứ 21 sẽ là "design day". Sau ngày thứ 4, bạn sẽ có tất cả thông tin cần thiết về HTML và CSS để bắt đầu design cho website Jobeet. Vì vậy, nếu bạn là designer, hoặc công ty bạn có bộ phận về design, bạn có thể đóng góp về design. Vào ngày thứ 21, chúng tôi sẽ tổ chức bầu chọn và cộng đồng sẽ chọn design chính thức cho Jobeet. Tất nhiên, nếu được chọn bạn sẽ nhận được thẻ thanh toán và cả sự nổi tiếng!

Sự khác biệt của hướng dẫn này

Hãy nhớ lại những ngày còn PHP4. Ah, la Belle Epoque! PHP là 1 trong những ngôn ngữ đầu tiên có mục đích chính là web và là một trong những ngôn ngữ dễ học nhất.

Nhưng công nghệ web phát triển rất nhanh, người lập trình web cần luôn cập nhật những công cụ và công nghệ mới. Cách tốt nhất để học là đọc blogs, tutorials, và sách. Chúng tôi đã đọc rất nhiều, với nhiều ngôn ngữ khác nhau PHP, Python, Java, Ruby, Perl...

Chắc bạn thường gặp những cảnh báo như:

"Với một ứng dụng thực sự, đừng quên kiểm tra sự hợp lệ (validation) và điều khiển lỗi (proper error handling)."

hoặc

"Security is left as an exercise to the reader."

hoặc

"Bạn cần phải viết tests."

Những thứ đó cũng quan trọng như là code vậy. Không có nó, mã nguồn có thể chạy không đúng như dự định. Thật là tệ! Tại sao vậy? Bởi vì security, validation, error handling, và tests, giúp code của bạn trở nên đúng đắn.

Trong hướng dẫn này, bạn sẽ không bao giờ phải thấy những câu như chúng ta phải viết tests, kiểm soát lỗi, validation code, mà vẫn đảm bảo rằng ứng dụng của chúng ta hoàn toàn bảo mật. Đó là bởi vì symfony không chỉ để viết code, mà còn là môi trường tốt nhất để phát triển các ứng dụng chuyên nghiệp. Chúng ta có thể làm bởi vì symfony cung cấp tất cả các công cụ cần thiết để thực hiện những việc này mà không cần phải viết nhiều code để làm điều đó.

Validation, error handling, security, và tests là những ưu tiên hàng đầu trong symfony. Đó là một trong những lý do chúng ta sử dụng framework trong một dự án thực tế.

Toàn bộ mã nguồn trong hướng dẫn này bạn có thể sử dụng trong dự án thực tế. Bạn có thể thoải mái sử dụng một phần hoặc toàn bộ mã nguồn.

Dự án

Ứng dụng được thiết kế trên symfony. Mục đích để chứng tỏ rằng symfony có thể sử dụng để phát triển một ứng dụng chuyên nghiệp một cách dễ dàng và ít tốn công sức.

Hôm nay, nội dung của dự án sẽ được bí mật. Chúng ta chỉ biết tên của dự án là: Jobeet.

Công việc hôm nay?

24 giờ là đủ để phát triển 1 ứng dụng với symfony, hôm nay chúng ta sẽ không viết dòng code PHP nào. Tuy không viết code, nhưng bạn sẽ bắt đầu hiểu lợi ích của việc sử dụng một framework như symfony, bằng cách khởi tạo một project mới.

Mục tiêu của ngày hôm nay là cài đặt môi trường phát triển và hiển thị một trang của ứng dụng trên trình duyệt web. Công việc bao gồm: cài đặt , khởi tạo một ứng dụng, và cấu hình web server.

Yêu cầu

Trước tiên, bạn phải có một web server (Apache chẳng hạn), một hệ quản trị cơ sở dữ liệu (MySQL, PostgreSQL, hoặc SQLite), và PHP 5.2.4 trở lên.

Chúng ta sẽ sử dụng dòng lệnh rất nhiều, tốt nhất là sử dụng hệ điều hành họ Unix, nhưng nếu bạn dùng Windows, bạn cần gõ các lệnh từ cửa sổ cmd.

Có thể sử dụng các lệnh của Unix trên môi trường Windows. Nếu bạn muốn sử dụng các công cụ như tar, gzip, hay grep trên Windows bạn có thể cài đặt Cygwin. Tài liệu hướng dẫn không nhiều, hướng dẫn cài đặt bạn có thể xem ở đây. Bạn cũng có thể thử khám phá Windows Services for Unix.

Hướng dẫn này chủ yếu đề cập đến symfony framework, chúng tôi giả định rằng bạn đã có hiểu biết về PHP 5 và lập trình hướng đối tượng.

Cài đặt Symfony

Đầu tiên, tạo một thư mục để chứa các file của Jobeet project:

$ mkdir -p /home/sfprojects/jobeet$ cd /home/sfprojects/jobeet

Ở Windows:

c:\> mkdir c:\development\sfprojects\jobeetc:\> cd c:\development\sfprojects\jobeet

Người dùng Windows nên chạy symfony và khởi tạo project ở thư mục không chứa dấu cách. Tránh sử dụng thư mục Documents and Settings , hay My Documents.

Tạo một thư mục để chứa thư viện symfony framework:

$ mkdir -p lib/vendor

Để cài đặt symfony, download archive package trên trang web symfony. Hướng dẫn này được viết trên symfony 1.2, hãy download phiên bản mới nhất của symfony 1.2.

Ở mục "Source Download", bạn sẽ tìm thấy file nén dạng .tgz hoặc .zip. Download file này và copy vào thư mục vừa tạo lib/vendor sau đó giải nén:

$ cd lib/vendor$ tar zxpf symfony-1.2-latest.tgz$ mv symfony-1.2.0 symfony

Ở Windows, việc giải nén file zip có thể làm từ menu chuột phải. Sau khi đổi tên thư mục thành symfony, chúng ta có thư mục như sau c:\development\sfprojects\jobeet\lib\vendor\symfony.

Do PHP configurations có thể khác nhau, chúng ta cần kiểm tra PHP configuration để chắc chắn đáp ứng các yêu cầu tối thiểu để chạy symfony. Chạy đoạn script kiểm tra từ dòng lệnh:

$ cd ../..$ php lib/vendor/symfony/data/bin/check_configuration.php

Nếu có vấn đề, màn hình sẽ đưa ra gợi ý và cách sửa. Bạn có chạy file kiểm tra PHP configuration từ trình duyệt. Copy file vào thư mục web server root và truy cập nó từ trình duyệt. Đừng quên xóa file đi sau khi đã kiểm tra xong.

Nếu đoạn script không hiện thông báo lỗi, hãy kiểm tra để chắc rằng symfony được cài thành công bằng cách sử dụng lệnh của symfony để xem phiên bản (chữ cái V viết hoa):

$ php lib/vendor/symfony/data/bin/symfony -V

Ở Windows:

c:\> cd ..\..c:\> php lib\vendor\symfony\data\bin\symfony -V

Nếu bạn muốn xem tất cả các lệnh của symfony, gõ symfony để xem danh sách các lệnh:

$ php lib/vendor/symfony/data/bin/symfony

Ở Windows:

c:\> php lib\vendor\symfony\data\bin\symfony

Các lệnh của symfony rất hữu dụng. Nó cung cấp rất nhiều công cụ giúp cho việc phát triển sản phẩm của bạn tiện lợi như xoá cache, tự động sinh code, ...

Cài đặt Project

Trong symfony, các applications có chung một cơ sở dữ liệu (data model) được nhóm lại thành projects. Với Jobeet project, chúng ta có 2 applications: frontend và backend.

Tạo Project

Từ thư mục jobeet, chạy lệnh generate:project để tạo 1 symfony project:

$ php lib/vendor/symfony/data/bin/symfony generate:project jobeet

Ở Windows:

c:\> php lib\vendor\symfony\data\bin\symfony generate:project jobeet

Lệnh generate:project tạo ra cấu trúc file và thư mục mặc định cần cho một symfony project:

Thư mục Mô tảapps/ chứa toàn bộ các applications của projectscache/ chứa các files cache tạo bởi frameworkconfig/ chứa các file cấu hìnhlib/ chứa các lớp và thư việnlog/ các file log của frameworkplugins/ chứa các plugins được cài đặt

Thư mục Mô tảtest/ chứa các file unit và functional testweb/ thư mục web root (xem bên dưới)

Tại sao symfony tạo ra quá nhiều file vậy? Một trong những lợi ích của việc sử dụng full-stack framework là chuẩn hoá sự phát triển của bạn. Nhờ cấu trúc file và thư mục thống nhất của symfony, bất kì lập trình viên nào có hiểu biết về symfony cũng có thể thực hiện công việc bảo trì cho bất kì dự án symfony nào. Sau một thời gian ngắn, anh ta đã có thể bắt đầu code, sửa lỗi, và thêm tính năng mới.

Lệnh generate:project cũng tạo một symfony shortcut ở thư mục ngoài cùng của Jobeet project để giảm số kí tự phải gõ trong lệnh.

Vì thế, từ bây giờ, thay vì gõ đầy đủ đường dẫn, chúng ta chỉ cần gõ symfony.

Tạo Application

Bây giờ, tạo application frontend bằng lệnh generate:app task:

$ php symfony generate:app --escaping-strategy=on --csrf-secret=Unique$ecret frontend

Khi chạy file symfony, người dùng Unix có thể thay thế toàn bộ đoạn 'php symfony' bằng './symfony' from now on.

Ở Windows bạn có thể copy file 'symfony.bat' vào project của bạn và sử dụng 'symfony' thay vì 'php symfony':

c:\> copy lib\vendor\symfony\data\bin\symfony.bat .

Một lần nữa, lệnh generate:app tạo cấu trúc thư mục mặc định cần thiết cho một application nằm trong thư mục apps/frontend:

Thư mục Mô tảconfig/ chứa các file cấu hình cho applicationlib/ các lớp và thư viện của applicationmodules/ chứa mã nguồn của ứng dụng (MVC)templates/ chứa các file template toàn cục

Tất cả các lệnh của symfony phải chạy dưới thư mục gốc của project trừ khi có chỉ dẫn khác.

Khi gọi lệnh generate:app , chúng ta đã cung cấp hai lựa chọn liên quan đến bảo mật:

--escaping-strategy: cho phép output escaping để chống tấn công XSS --csrf-secret: cho phép session tokens in forms để chống tấn công CSRF

Nhờ 2 tham số này, chúng ta đã bảo vệ được ứng dụng của mình khỏi 2 lỗ hổng bảo mật phổ biến trên web.

Nếu bạn không biết về XSS và CSRF, hãy dành vài phút để tìm hiểu về những lỗ hổng bảo mật này.

Đường dẫn symfony

Bạn có thể xem phiên bản symfony sử dụng trong dự án của bạn bằng cách gõ:

$ php symfony -V

Lệnh này cũng hiển thị đường dẫn đến thư mục cài đặt symfony, được sử dụng trong file config/ProjectConfiguration.class.php:

// config/ProjectConfiguration.class.phprequire_once '/Users/fabien/work/symfony/dev/1.2/lib/autoload/sfCoreAutoload.class.php';

để cho thuận tiện, thay đường dẫn tuyệt đối bằng đường dẫn tương đối:

// config/ProjectConfiguration.class.phprequire_once dirname(__FILE__).'/../lib/vendor/symfony/lib/autoload/sfCoreAutoload.class.php';

Bây giờ, bạn có thể copy Jobeet project đến bất kì đâu, nó vẫn chạy được.

Môi trường

Trong thư mục web/ , bạn có thể thấy 2 file PHP: index.php và frontend_dev.php. Những file này được gọi là front controllers: mọi yêu cầu đến ứng dụng đều thông qua chúng. Nhưng tại sao chúng ta có 2 file front controllers trong khi chúng ta chỉ có 1 ứng dụng?

Cả hai file đều gọi cùng một ứng dụng nhưng trong những môi trường khác nhau. Khi bạn phát triển một ứng dụng, trừ khi bạn phát triển trực tiếp sản phẩm trên server, bạn cần vài môi trường:

development environment: môi trường sử dụng bởi web developers để thêm các tính năng, sửa lỗi, ...

test environment: môi trường sử dụng cho các ứng dụng test tự động. staging environment: môi trường sử dụng bởi customer để test ứng dụng và

thông báo lỗi và các tính năng thiếu. production environment: môi trường tương tác với end user.

Trong môi trường development, ứng dụng cần log tất cả các request để dễ dàng tìm lỗi, nó phải hiển thị exception trên trình duyệt, và hệ thống cache phải được tắt để có thể thấy thay đổi khi thay đổi code. Vì thế, môi trường development phải được cấu hình cho phù hợp với lập trình viên:

Ở môi trường production, ứng dụng phải hiển thị một thông báo lỗi thay vì hiển thị lỗi cụ thể của hệ thống, và tất nhiêu, cache phải được bật. Vì thế, môi trường production phải được cấu hình cho phù hợp.

Mỗi môi trường trong symfony có một tập các cấu hình riêng và symfony có sẵn 3 môi trường: dev, test, và prod.

Nếu bạn mở các file front controller, bạn sẽ thấy chúng chỉ khác nhau về cấu hình của môi trường:

// web/index.php<?php require_once(dirname(__FILE__).'/../config/ProjectConfiguration.class.php'); $configuration = ProjectConfiguration::getApplicationConfiguration('frontend', 'prod', false);sfContext::createInstance($configuration)->dispatch();

Tạo một môi trường mới trong symfony đơn giản là tạo một file front controller mới. Chúng ta sẽ xem cách thay đổi các cấu hình cho một môi trường ở các phần sau.

Cài đặt Web Server: cách tối

Trong phần trước, chúng ta đã tạo một thư mục để chứa Jobeet project. Nếu bạn tạo thư mục này dưới thư mục web root của web server, bạn đã có thể truy cập vào project trên trình duyệt

Thật là dễ dàng và không cần phải cấu hình gì cả! Tuy nhiên, hãy thử truy cập file config/databases.yml từ trình duyệt để hiểu tác hại của sự lười biếng này.

Không bao giờ được sử dụng cách cài đặt này trên server cho một sản phẩm thực sự và hãy đọc phần tiếp theo để biết cách cấu hình web server đúng đắn.

Cài đặt Web Server: cách bảo mật

Tốt nhất là chỉ đặt trong thư mục web root các file mà trình duyệt cần truy cập trực tiếp như stylesheets, JavaScripts, hoặc file ảnh. Mặc định, chúng tôi khuyên bạn để những file này trong thư mục web của symfony project.

Nếu bạn xem trong thư mục này, bạn sẽ thấy vài thư mục con như css, images, js và 2 file front controller. File front controllers là file php duy nhất cần đặt trong thư mục web root. Tất cả các file PHP khác có thể ẩn đi đối với trình duyệt, để bảo mật cho ứng dụng.

Cấu hình Web Server

Bây giờ cần thay đổi cấu hình Apache để có thể truy cập project.

Mở file cấu hình httpd.conf và thêm vào cuối file:

# Be sure to only have this line once in your configurationNameVirtualHost 127.0.0.1:8080

# This is the configuration for JobeetListen 127.0.0.1:8080

<VirtualHost 127.0.0.1:8080> DocumentRoot "/home/sfprojects/jobeet/web" DirectoryIndex index.php <Directory "/home/sfprojects/jobeet/web"> AllowOverride All Allow from All </Directory>

Alias /sf /home/sfprojects/jobeet/lib/vendor/symfony/data/web/sf <Directory "/home/sfprojects/jobeet/lib/vendor/symfony/data/web/sf"> AllowOverride All Allow from All </Directory></VirtualHost>

The /sf cho phép bạn truy cập các file ảnh và javascript cần để hiển thị các trang mặc định của symfony và thanh web debug toolbar.

Ở Windows, bạn cần sửa dòng Alias thành:

Alias /sf "c:\development\sfprojects\jobeet\lib\vendor\symfony\data\web\sf"

/home/sfprojects/jobeet/web sẽ được thay thế bởi:

c:\development\sfprojects\jobeet\web

Cấu hình này giúp Apache lắng nghe cổng 8080 trên máy bạn, vì thế Jobeet website có thể được truy cập được theo URL:

http://localhost:8080/

Bạn có thể đổi 8080 thành bất kì số nào nhưng nên lớn hơn 1024 để chắc rằng cổng này chưa được sử dụng.

Cấu hình để tạo một tên miền cho Jobeet

Nếu bạn có quyền administrator trên máy, tốt nhất là tạo một virtual hosts thay vì thêm một cổng mới mỗi khi bạn tạo một project mới. Thay vì tạo một cổng và Listen , hãy chọn một tên miền và thêm ServerName:

# This is the configuration for Jobeet<VirtualHost 127.0.0.1:80> ServerName jobeet.localhost <!-- same configuration as before --></VirtualHost>

Tên miền jobeet.localhost phải được khai báo. Nếu bạn dùng Linux, bạn cần thêm vào trong file /etc/hosts . Nếu bạn dùng Windows XP, file này nằm trong thư mục C:\WINDOWS\system32\drivers\etc\.

Và thêm dòng sau:

127.0.0.1 jobeet.localhost

Kiểm tra cấu hình

Khởi động lại Apache, và kiểm tra xem bạn có thể truy cập ứng dụng từ địa chỉ http://localhost:8080/index.php/, hoặc http://jobeet.localhost/index.php/ tùy vào cấu hình bạn chọn ở mục trước.

Nếu bạn có cài đặt module Apache mod_rewrite , bạn có thể bỏ đoạn /index.php/ trên URLs.

Bạn có thể truy cập ứng dụng trên môi trường development:

http://jobeet.localhost/frontend_dev.php/

Thanh web debug toolbar được hiển thị bên góc phải, bao gồm các biểu tượng nhỏ được cung cấp từ sf/ nếu bạn cấu hình đúng.

Cách cài đặt có khác chút nếu bạn chạy symfony trên IIS server ở Windows. Cách cấu hình có thể tìm ở hướng dẫn này.

Subversion

Tốt nhất là sử dụng một công cụ để quản lý phiên bản của mã nguồn khi phát triển một ứng dụng web. Nó cho phép chúng ta:

làm việc với sự tin cậy chuyển lại phiên bản trước nếu sự thay đổi gây ra lỗi cho phép nhiều người cùng làm việc trên một project có thể truy cập tất cả các phiên bản của ứng dụng

Trong mục này, chúng tôi sẽ mô tả cách sử dụng Subversion với symfony. Nếu bạn sử dụng công cụ quản lý mã nguồn khác, nó cũng tương tự như những gì chúng tôi hướng dẫn với Subversion.

Chúng tôi giả sử rằng bạn có quyền truy cập Subversion server.

Nếu bạn không có một Subversion server, bạn có thể tạo miễn phí trên Google Code hoặc gõ "free subversion repository" trên Google để có nhiều lựa chọn.

Đầu tiên, tạo một kho chứa mới cho jobeet project:

$ svnadmin create http://svn.example.com/jobeet$ svn mkdir -m "created default directory structure" http://svn.example.com/jobeet/trunk http://svn.example.com/jobeet/tags http://svn.example.com/jobeet/branches

Sau đó, xóa các file trong thư mục cache/ và log/ vì chúng ta không muốn đưa nó lên kho chứa.

$ cd /home/sfprojects/jobeet$ rm -rf cache/*$ rm -rf log/*

Bây giờ, chúng ta chmod để chắc chắc rằng chúng ta có quyền ghi vào thư mục cache và logs để web server của bạn có thể ghi vào đó:

$ chmod 777 cache$ chmod 777 log

Tiếp theo, đưa mã nguồn lên kho chứa:

$ svn import -m "made the initial import" . http://svn.example.com/jobeet/trunk

Chúng ta không bao giờ muốn commit những file trong thư mục cache/ và /log , nên ta đưa nó vào danh sách cần bỏ qua:

$ svn propedit svn:ignore cache

Text editor mặc định để cấu hình SVN được mở. Subversion phải bỏ qua tất cả nội dung trong thư mục:

*

Lưu lại vào thoát.

Làm tương tự với thư mục log/:

$ svn propedit svn:ignore log

Và gõ:

*

Cuối cùng, commit những thay đổi lên kho chứa:

$ svn commit -m "added cache/ and log/ content in the ignore list"

người dùng Windows có thể sử dụng TortoiseSVN client, một công cụ tuyệt vời để quản lý subversion.

Hẹn gặp lại ngày mai

Thời gian của hôm nay đã hết! Mặc dù chúng ta chưa thực sự bắt đầu với symfony, nhưng chúng ta đã cài đặt môi trường phát triển, chúng ta đã nói về web development best practices, và chúng ta đã sẵn sàng coding.

Ngày mai, chúng ta sẽ khám phá những yêu cầu của ứng dụng mà chúng ta sẽ thực hiện trong suốt hướng dẫn này.

Nếu bạn muốn xem mã nguồn của ngày hôm nay, và những ngày khác, bạn có thể lấy về từ kho chứa Jobeet SVN (http://svn.jobeet.org/propel/).

Ví dụ, bạn có thể lấy mã nguồn ngày hôm nay, bạn có thể checking out tag release_day_01:

$ svn co http://svn.jobeet.org/propel/tags/release_day_01/ jobeet/

Tóm tắt ngày trước

Chúng ta đã không viết một dòng code PHP nào, nhưng ngày hôm qua, chúng ta đã cài đặt môi trường, khởi tạo một symfony project với một vài bảo mật ban đầu. Nếu bạn làm theo đầy đủ các hướng dẫn, bạn đã thấy trang mặc định của symfony cho một ứng dụng mới.

Nhưng bạn còn muốn nhiều hơn thế. Bạn muốn học tất cả mọi chi tiết để phát triển một ứng dụng với symfony.

Hôm nay, chúng ta sẽ dành thời gian để mô tả yêu cầu của dự án Jobeet.

Mục đích của dự án

Gần đây, người ta nói nhiều về khủng hoảng. Thất nghiệp đang tăng trở lại.

Tôi biết, những lập trình viên symfony không bị ảnh hưởng nhiều và đó là lý do bạn muốn học symfony! . Nhưng thật khó để tìm một lập trình viên symfony giỏi.

Bạn có thể tìm một lập trình viên symfony ở đâu? Nơi nào để cho bạn giới thiệu kĩ năng về symfony của mình?

Bạn cần tìm một job board tốt. Thật rộng lớn? Hãy nghĩ lại. Bạn cần một nơi đăng tuyển dụng tập trung. Nơi mà bạn có thể tìm thấy chuyên gia tốt nhất. Nơi mà bạn có thể dễ dàng và nhanh chóng tìm kiếm một công việc hoặc đăng một tuyển dụng.

Không phải tìm ở đâu xa. Jobeet chính là một nơi như thế. Jobeet là một Open-Source job board software chỉ làm một thứ, nhưng làm tốt. Nó dễ dàng để sử dụng, chỉnh sửa, mở rộng, và nhúng vào website khác. Nó hỗ trợ đa ngôn ngữ, và tất nhiên sử dụng những công nghệ Web 2.0 mới nhất. Nó cũng cung cấp feeds và API để tương tác với chương trình khác.

Một ứng dụng như vậy đã có chưa? Là người sử dụng, bạn sẽ tìm thấy rất nhiều nơi đăng tuyển dụng như Jobeet trên Internet. Nhưng hãy thử tìm một ứng dụng mã nguồn mở, và đầy đủ tính năng như chúng ta đã mô tả.

Và cần ít hơn 24 để phát triển với symfony? Vậy chúng ta phải bắt đầu ngay!

Kịch bản của dự án

Trước khi bắt đầu code, chúng ta hãy mô tả một chút về dự án. Phần này sẽ mô tả các tính năng mà chúng tôi muốn có trong phiên bản đầu tiên của project.

Website Jobeet có 4 loại người dùng:

admin: chủ sở hữu website và có tất cả mọi quyền user: người vào website để tìm kiếm 1 công việc hoặc đăng tuyển dụng poster: người đăng tuyển dụng affiliate: người gắn tuyển dụng của Jobeet vào website của anh ta

Project có 2 application: frontend (stories F1 đến F7, bên dưới), nơi người dùng tương tác với website, và backend (stories B1 đến B3), nơi admins quản lý website.

backend được bảo mật và yêu cầu quyền truy cập.

Story F1: Trang chủ, hiển thị danh sách các công việc mới nhất

Khi một người dùng vào website Jobeet, cô ta sẽ thấy danh sách các công việc mới nhất. Các công việc được sắp xếp theo category và ngày đưa lên (các công việc mới được đưa lên trên). Với mỗi công việc, chỉ có địa điểm (location), vị trí công việc (position), và tên công ty (company) được hiển thị.

Trong mỗi category, chỉ hiển thị danh sách 10 công việc mới nhất và một link cho phép xem danh sách tất cả các công việc của category đó(Story F2).

Ở trang chủ, người dùng có thể lọc danh sách công việc (Story F3), hoặc đăng tuyển dụng (Story F5).

Story F2: Người dùng xem danh sách các công việc trong 1 category

Khi người dùng clicks vào tên của category hoặc vào link "more jobs" ở trang chủ, anh ta sẽ thấy được tất cả các công việc của category đó được sắp xếp theo ngày tháng.

Danh sách công việc sẽ được phân trang: 20 công việc một trang.

Story F3: Người dùng lọc danh sách công việc bằng từ khóa

Người dùng có thể nhập một vài từ khóa để tìm kiếm. Từ khóa có thể được tìm theo địa điểm, vị trí công việc, theo category, hoặc tên công ty.

Story F4: Người dùng xem chi tiết một công việc

Người dùng có thể chọn một công việc trong danh sách để xem chi tiết.

Story F5: Người dùng đăng một tuyển dụng

Người dùng có thể đăng một tuyển dụng. Một công việc cần chứa các thông tin:

Tên công ty Loại hình công việc (full-time, part-time, hay freelance) Logo (không bắt buộc) URL (không bắt buộc) Vị trí công việc Địa điểm Category (người dùng chọn trong danh sách các category có sẵn) Mô tả công việc (URLs và emails sẽ được tự động link)

Cách ứng tuyển (URLs và emails sẽ được tự động link) Public (công việc có thể đưa lên websites của affiliate) Email (emailcủa người gửi)

Người dùng không cần tạo tài khoản cũng có thể đăng tuyển dụng.

Quá trình này được tiến hành qua 2 bước: đầu tiên, người dùng điền vào form các thông tin cần thiết về công việc, sau đó anh ta kiểm tra lại các thông tin bằng cách preview trên trang tuyển dụng.

Mặc dù người dùng không có tài khoản, sau này công việc vẫn có thể được chỉnh sửa nhờ một URL đặc biệt (được bảo vệ bởi một token cung cấp cho người dùng khi công việc được tạo).

Mỗi công việc sẽ xuất hiện trong 30 ngày (admin có thể sửa giá trị này - xem Story B2). Người dùng có thể kích hoạt lại hoặc tăng thời hạn công việc thêm 30 ngày nữa nhưng chỉ khi công việc đã hết hạn ít nhất 5 ngày.

Story F6: Người dùng đăng kí làm affiliate

Người dùng cần đăng kí để trở thành affiliate và cần xác thực để có thể sử dụng Jobeet API. Để đăng kí, anh ta phải cung cấp các thông tin:

Tên Email Website URL

Tài khoản affiliate phải được kích hoạt bởi admin (Story B3). Mỗi lần kích hoạt, affiliate sẽ nhận một token qua email để sử dụng API.

Khi được chấp nhận, affiliate có thể chọn các công việc trong các categories.

Story F7: Affiliate nhận danh sách các công việc hiện tại

Một affiliate có thể nhận danh sách các công việc hiện tại thông qua API với token của anh ta. Danh sách công việc có thể trả về dưới dạng XML, JSON hoặc YAML.

Danh sách chứa các thông tin public về công việc.

Affiliate cũng có thể giới hạn số công việc trả về, và lọc theo category.

Story B1: Admin cấu hình website

Admin có thể chỉnh sửa các categories trên website.

Anh ta cũng có thể thay đổi:

Số công việc hiển thị ở trang chủ Ngôn ngữ của website Thời hạn của công việc

Story B2: Admin quản lý công việc

Admin có thể sửa, xóa bất kì công việc nào.

Story B3: Admin quản lý affiliates

Admin có thể tạo và sửa affiliates. Anh ta chịu trách nhiệm kích hoạt tài khoản affiliate hoặc khóa tài khoản đó.

Khi admin kích hoạt một tài khoản affiliate mới, hệ thống sẽ tạo một token duy nhất để affiliate sử dụng.

Hẹn gặp lại ngày mai

Khi phát triển ứng dụng web, chúng ta không bao giờ code ngay từ đầu. Trước tiên, bạn cần nắm rõ yêu cầu và làm việc với một mô hình thiết kế. Đó là những gì chúng ta làm ngày hôm nay.

Tóm tắt

Những ai nóng lòng muốn mở text editor và viết vài đoạn code PHP chắc sẽ rất vui khi được biết ngày hôm nay chúng ta sẽ làm điều đó. Chúng ta sẽ xác định Jobeet data model, sử dụng ORM để tương tác với cơ sở dữ liệu, và xây dựng module đầu tiên của

ứng dụng. Nhưng symfony sẽ làm nhiều việc thay chúng ta, chúng ta sẽ có một module với đầy đủ các chức năng mà không cần phải viết nhiều code PHP.

Relational Model

Như đã đề cập hôm trước, ứng dụng của chúng ta có các đối tượng chính: jobs, affiliates, và categories. Đây là lược đồ quan hệ giữa chúng:

Ngoài các cột như đã mô tả, chúng ta có thêm trường created_at trong một số bảng. Symfony ghi nhận những trường này và tự động gán giá trị thời gian hiện tại mỗi khi một bản ghi được tạo. Tương tự với trường updated_at, trường này sẽ được tự động cập nhật mỗi khi cập nhật một bản ghi.

Schema

Để chứa jobs, affiliates, và categories, chúng ta cần một cơ sở dữ liệu quan hệ.

Nhưng symfony là một framework hướng đối tượng, chúng ta muốn thao tác với đối tượng bất cứ khi nào có thể. Ví dụ, thay vì viết câu lệnh SQL để nhận một bản ghi từ cơ sở dữ liệu, ta muốn sử dụng objects.

Thông tin về relational database phải được chuyển thành một object model. Điều đó có thể thực hiện với một ORM tool, symfony cung cấp sẵn 2 công cụ: Propel và Doctrine. Trong hướng dẫn này, chúng ta sẽ sử dụng Propel.

ORM cần thông tin mô tả các bảng và quan hệ giữa chúng để tạo class tương ứng. Có hai cách để tạo một schema mô tả: từ một cơ sở dữ liệu có sẵn hoặc tự tạo nó.

Một vài công cụ cho phép bạn tạo database ở chế độ đồ họa (ví dụ Fabforce's Dbdesigner) và sinh ra trực tiếp file schema.xml (với DB Designer 4 TO Propel Schema Converter, bạn có thể chuyển thành propel schema).

Cơ sở dữ liệu chưa tồn tại và chúng ta muốn cơ sở dữ liệu Jobeet là agnostic, do đó chúng ta tự tạo file schema config/schema.yml:

# config/schema.ymlpropel: jobeet_category: id: ~ name: { type: varchar(255), required: true }  jobeet_job: id: ~ category_id: { type: integer, foreignTable: jobeet_category, foreignReference: id, required: true } type: { type: varchar(255) } company: { type: varchar(255), required: true } logo: { type: varchar(255) } url: { type: varchar(255) } position: { type: varchar(255), required: true } location: { type: varchar(255), required: true } description: { type: longvarchar, required: true } how_to_apply: { type: longvarchar, required: true } token: { type: varchar(255), required: true, index: unique } is_public: { type: boolean, required: true, default: 1 } is_activated: { type: boolean, required: true, default: 0 } email: { type: varchar(255), required: true } expires_at: { type: timestamp, required: true } created_at: ~ updated_at: ~  jobeet_affiliate: id: ~ url: { type: varchar(255), required: true } email: { type: varchar(255), required: true, index: unique } token: { type: varchar(255), required: true } is_active: { type: boolean, required: true, default: 0 } created_at: ~  jobeet_category_affiliate: category_id: { type: integer, foreignTable: jobeet_category, foreignReference: id, required: true, primaryKey: true, onDelete: cascade } affiliate_id: { type: integer, foreignTable: jobeet_affiliate, foreignReference: id, required: true, primaryKey: true, onDelete: cascade }

Nếu bạn tạo bảng bằng cách viết lệnh SQL, bạn có thể tạo ra file schema.yml tương ứng bằng lệnh propel:build-schema.Lệnh này sẽ chuyển đổi trực tiếp từ lược đồ quan hệ sang YAML format.

YAML Format

theo website YAML, YAML là "là tập các dữ liệu chuẩn đối với mọi ngôn ngữ lập trình, dễ hiểu đối với con người"

Nói cách khác, YAML là một ngôn ngữ đơn giản đề mô tả dữ liệu (strings, integers, dates, arrays, và hashes).

Trong YAML, cấu trúc được xác định thông qua dấu lùi dòng, cặp key/value được cách nhau bởi dấu hai chấm (:). YAML cũng có các kí hiệu để mô tả cấu trúc với ít dòng hơn, như arrays được xác định trong cặp [] và hashes với {}.

Nếu bạn chưa quen với cấu trúc của YAML, bạn sẽ quen dần khi sử dụng symfony framework bởi nó được dùng trong các file cấu hình.

File schema.yml chứa thông tin về tất cả các bảng và cột tương ứng. Mỗi cột được mô tả bao gồm:

type: kiểu dữ liệu (boolean, tinyint, smallint, integer, bigint, double, float, real, decimal, char, varchar(size), longvarchar, date, time, timestamp, blob, và clob)

required: true nếu cột đó là bắt buộc index: true nếu bạn muốn index cho cột unique nếu bạn muốn unique index cho

cột.

Với những cột được set ~ (id, created_at, và updated_at), symfony sẽ tự cấu hình phù hợp (khóa chính với id và timestamp với created_at và updated_at).

Thuộc tính onDelete xác định ứng xử khi ON DELETE của khóa ngoài, và Propel hỗ trợ CASCADE, SETNULL, và RESTRICT. Ví dụ, khi một job record bị xóa, tất cả các jobeet_category_affiliate record liên quan sẽ tự động được xóa theo bởi database hoặc bởi Propel nếu underlying engine không hỗ trợ chức năng này.

Database

Framework symfony hỗ trợ tất cả các sơ sở dữ liệu hỗ trợ PDO (MySQL, PostgreSQL, SQLite, Oracle, MSSQL, ...). PDO là database abstraction layer đi kèm trong PHP.

Chúng ta sử dụng MySQL trong tutorial này:

$ mysqladmin -uroot -pmYsEcret create jobeet

Bạn có thể thoải mái chọn bất kì database engine nào bạn muốn. Chúng ta sử dụng ORM nên việc sử dụng các database engine khác nhau không gây ra khó khăn!

Chúng ta cần khai báo với symfony cơ sở dữ liệu ta sử dụng cho Jobeet project:

$ php symfony configure:database "mysql:host=localhost;dbname=jobeet" root mYsEcret

Lệnh configure:database có 3 tham số: PDO DSN, tên, và mật khẩu truy cập cơ sở dữ liệu. Nếu password là rỗng, hãy bỏ qua tham số này

Lệnh configure:database chứa cấu hình của cơ sở dữ liệu vào file config/databases.yml. Bạn có thể sửa trực tiếp file này thay vì sử dụng lệnh.

ORM

Nhờ những mô tả về cơ sở dữ liệu trong file schema.yml , chúng ta có thể sử dụng của Propel để sinh các câu SQL cần thiết để tạo các bảng trong cơ sở dữ liệu:

$ php symfony propel:build-sql

Lệnh propel:build-sql sinh ra các câu lệnh SQL nằm trong thư mục data/sql:

# snippet from data/sql/lib.model.schema.sqlCREATE TABLE `jobeet_category`( `id` INTEGER NOT NULL AUTO_INCREMENT, `name` VARCHAR(255) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `jobeet_category_U_1` (`name`))Type=InnoDB;

Để tạo các bảng trong cơ sở dữ liệu, chúng ta chạy lệnh propel:insert-sql:

$ php symfony propel:insert-sql

Do lệnh này sẽ xóa các bảng hiện tại trước khi tạo bảng mới, nên bạn được yêu cầu xác nhận hành động này. Bạn có thể thêm option --no-confirmation để bỏ qua việc xác nhận, việc này sẽ hữu ích khi bạn cần chạy lệnh một cách tự động:

$ php symfony propel:insert-sql --no-confirmation

Như bất kì công cụ dòng lệnh nào, các lệnh symfony cũng có một vài tham số và lựa chọn. Có thể dùng help để xem chi tiết các lệnh:

$ php symfony help propel:insert-sql

help sẽ hiện danh sách các tham số và lựa chọn, các giá trị mặc định, và một vài ví dụ sử dụng.

ORM tạo các PHP classes tương ứng từ các table records sang objects:

$ php symfony propel:build-model

Lệnh propel:build-model tạo các file PHP trong thư mục lib/model dùng để tương tác với database.

Khi vào thư mục này, bạn có thể thấy rằng Propel tạo 4 class cho một table. Ví dụ với bảng jobeet_job:

JobeetJob: mỗi object của class này tương đương với một record của bảng jobeet_job. Ban đầu, class chưa có gì.

BaseJobeetJob: Lớp cha của JobeetJob. Mỗi khi bạn chạy lệnh propel:build-model, lớp này sẽ bị thay đổi, vì thế các thao tác bạn phải để trong lớp JobeetJob.

JobeetJobPeer: Lớp này chứa các static methods trả về tập các JobeetJob object. Ban đầu, class chưa có gì.

BaseJobeetJobPeer: Lớp cha của JobeetJobPeer. Mỗi khi bạn chạy lệnh propel:build-model, lớp này sẽ bị thay thế, vì thế mọi chỉnh sửa cần để trong lớp JobeetJobPeer.

Giá trị của các cột có thể được truy cập thông qua các phương thức get*() và set*():

$job = new JobeetJob();$job->setPosition('Web developer');$job->save(); echo $job->getPosition(); $job->delete();

Bạn có thể trực tiếp xác định khóa ngoài bằng cách link đến objects đó:

$category = new JobeetCategory();$category->setName('Programming'); $job = new JobeetJob();$job->setCategory($category);

Lệnh propel:build-all bao gồm các thao tác mà chúng ta đã làm. Ngoài ra, nó còn tạo ra các forms và validators cho Jobeet model classes:

$ php symfony propel:build-all

Validators in action được đề cập ở cuối ngày hôm nay và forms được miêu tả chi tiết trong ngày 10.

Lệnh propel:build-all-load bao gồm lệnh propel:build-all và propel:data-load task.

Như bạn sẽ thấy ở các phần sau, symfony tự động gọi các PHP classes, do đó bạn sẽ không bao giờ phải sử dụng require trong mã nguồn. Đó là một trong nhiều thứ mà symfony tự động làm cho lập trình viên, nhưng nó cũng có bất tiện: khi bạn thêm một class mới, bạn cần xóa symfony cache. Lệnh propel:build-model đã tạo ra rất nhiều classes mới, do đó chúng ta cần xóa cache:

$ php symfony cache:clear

Lệnh symfony bao gồm 1 namespace và tên thao tác. Các lệnh có thể viết tắt nếu không trùng với lệnh khác. Lệnh sau tương đương với cache:clear:

$ php symfony cc

Khởi tạo dữ liệu

Chúng ta đã tạo ra các bảng trong database nhưng chưa có dữ liệu. Bất kì một ứng dụng web nào đều có 3 kiểu dữ liệu:

Initial data: các dữ liệu cần thiết để ứng dụng làm việc. Ví dụ, Jobeet cần có một vài categories. Nếu không, người dùng sẽ không thể đăng tuyển dụng. Chúng ta cũng cần tạo một tài khoản admin để đăng nhập vào backend

Test data: dữ liệu test là cần thiết để test ứng dụng. Là developer, bạn cần viết test để chắc rằng ứng dụng hoạt động đúng như mô tả và cách tốt nhất là viết test tự động. Vì thế mỗi khi bạn chạy test, bạn cần một cơ sở dữ liệu với một vài dữ liệu để test.

User data: dữ liệu tạo bởi người dùng trong quá trình sử dụng ứng dụng.

Mỗi khi symfony tạo 1 bảng trong database, rất nhiều dữ liệu bị mất. Để tạo database với một vài dữ liệu khởi tạo, chúng ta có thể dùng PHP script, hoặc thực thi câu lệnh SQL. Nhưng có một cách tốt hơn trong symfony: tạo một file YAML trong thư mục data/fixtures/ và dùng lệnh 'propel:data-load` để load chúng vào database:

# data/fixtures/010_categories.ymlJobeetCategory: design: { name: Design } programming: { name: Programming } manager: { name: Manager } administrator: { name: Administrator } # data/fixtures/020_jobs.ymlJobeetJob: job_sensio_labs: category_id: programming type: full-time company: Sensio Labs logo: sensio_labs.png url: http://www.sensiolabs.com/ position: Web Developer

location: Paris, France description: | You've already developed websites with symfony and you want to work with Open-Source technologies. You have a minimum of 3 years experience in web development with PHP or Java and you wish to participate to development of Web 2.0 sites using the best frameworks available. how_to_apply: | Send your resume to fabien.potencier [at] sensio.com is_public: true is_activated: true token: job_sensio_labs email: [email protected] expires_at: 2010-10-10  job_extreme_sensio: category_id: design type: part-time company: Extreme Sensio logo: extreme_sensio.png url: http://www.extreme-sensio.com/ position: Web Designer location: Paris, France description: | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in.  Voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. how_to_apply: | Send your resume to fabien.potencier [at] sensio.com is_public: true is_activated: true token: job_extreme_sensio email: [email protected] expires_at: 2010-10-10

job fixture file sử dụng 2 ảnh. Bạn có thể download chúng (http://www.symfony-project.org/get/jobeet/sensio-labs.gif, http://www.symfony-project.org/get/jobeet/extreme-sensio.gif) và đặt trong thư mục uploads/jobs/.

Một file fixtures được viết bằng YAML, với mỗi model object được gán một label duy nhất. Label được sử dụng để link đến object liên quan mà không cần dùng khóa chính (khóa này tự động tăng và ta không gán giá trị cho nó). Ví dụ, công việc job_sensio_labs có category là programming, là label của category 'Programming'.

Một file fixture có thể chứa object của 1 hoặc vài model.

Chú ý đến số đằng trước ở tên file. Đó là một cách đơn giản để điều khiển thứ tự nạp dữ liệu. Sau này, nếu bạn cần thêm một vài dữ liệu mới, bạn sẽ dễ dàng thêm một số mới vào giữa những số đã có.

Trong file fixture, bạn không cần cung cấp giá trị tất cả các cột, symfony sẽ sử dụng các giá trị mặc định trong database schema. Giá trị các cột 'created_atvàupdated_at` sẽ được tự động thêm vào.

Load các dữ liệu khởi tạo vào database bằng lệnh propel:data-load:

$ php symfony propel:data-load

Xem một Action trên trình duyệt

Chúng ta đã sử dụng dòng lệnh rất nhiều nhưng nó không thực sự hứng thú, đặc biệt là với một dự án web. Bây giờ chúng ta đã có mọi thứ để tạo một trang Web tương tác với database.

Hãy xem cách hiển thị các công việc, chỉnh sửa một công việc, và xóa một công việc. Như đã nói trong ngày 1, một symfony project được tạo bởi các application. Mỗi application bao gồm nhiều modules. Một module là tập các mã nguồn PHP mô tả các tính năng của ứng dụng (như module API chẳng hạn), hay các thao tác của người dùng với một model object (như module job).

Symfony có thể tự động tạo module cho một model với các tính năng cơ bản:

$ php symfony propel:generate-module --with-show --non-verbose-templates frontend job JobeetJob

Lệnh propel:generate-module tạo module job ở application frontend ứng với model JobeetJob. Như phần lớn các lệnh symfony khác, một vài file và thư mục được tạo trong thư mục apps/frontend/modules/job:

Thư mục Mô tảactions/ chứa các action của moduletemplates/ chứa các template của module

file actions/actions.class.php chứa các action của module job:

Tên Action Mô tảindex hiển thị các records của tableshow hiển thị các fields của 1 recordnew hiển thị form để tạo một record mớicreate tạo một record mớiedit hiển thị form để sửa một record

Tên Action Mô tảupdate cập nhật một record thông qua các giá trị người dùng chỉnh sửadelete xóa một record khỏi table

Bây giờ bạn có thể test module job trên trình duyệt:

http://jobeet.localhost/frontend_dev.php/job

Nếu bạn thử sửa một công việc, bạn sẽ nhận được một exception vì symfony cần một đoạn text để mô tả mỗi category. Việc mô tả một PHP object có thể được xác định thông qua magic method __toString(). Đoạn text mô tả một category record được xác định trong JobeetCategory model class:

// lib/model/JobeetCategory.phpclass JobeetCategory extends BaseJobeetCategory{ public function __toString() { return $this->getName(); }}

Bây giờ, mỗi khi symfony cần một đoạn text để mô tả một category, nó có thể gọi phương thức __toString() trả về tên của category. Do chúng ta sẽ cần một đoạn text để mô tả tất cả các model class, nên hãy viết các phương thức __toString() cho các model class còn lại:

// lib/model/JobeetJob.phpclass JobeetJob extends BaseJobeetJob{ public function __toString() { return sprintf('%s at %s (%s)', $this->getPosition(), $this->getCompany(), $this->getLocation()); }} // lib/model/JobeetAffiliate.phpclass JobeetAffiliate extends BaseJobeetAffiliate{ public function __toString() { return $this->getUrl(); }}

Bây giờ, bạn có thể tạo và sửa một công việc. Thử để trống các trường bắt buộc, hoặc điền một giá trị không hợp lệ. Symfony đã tạo sẵn các validation rules cơ bản dựa vào database schema.

Hẹn gặp lại ngày mai

Đó là tất cả những công việc của hôm nay. Như đã nói trong phần giới thiệu, chúng ta không phải viết PHP code nhưng chúng ta đã có một web module ứng với job model, sẵn sàng cho chúng ta chỉnh sửa.

Nếu bạn vẫn còn hứng thú, hãy đọc mã nguồn đã được tạo ra tự động và cố gắng hiểu cách làm việc của nó. Nếu không, bạn có thể đi ngủ, và ngày mai, chúng ta sẽ nói về một trong những mô hình phổ biến nhất của web framework MVC design pattern.

Giống như hôm qua, hôm nay mã nguồn được public lên kho chứa của Jobeet SVN . Checkout tag release_day_03:

$ svn co http://svn.jobeet.org/propel/tags/release_day_03/ jobeet/

Tóm tắt

Hôm qua, chúng ta đã khám phá cách quản lý database của symfony: trừu tượng sự khác nhau giữa các database engines, chuyển đổi cơ sở dữ liệu thành các lớp hướng đối tượng. Chúng ta cũng đã sử dụng Propel để tạo database schema, tạo các bảng, và tạo sẵn một vài dữ liệu mẫu.

Hôm nay, chúng ta sẽ bắt đầu chỉnh sửa module job đã tạo hôm qua. Module job đã có tất cả các mã nguồn cần thiết:

Một trang list các công việc Một trang tạo công việc mới

Một trang cập nhật công việc đã có Một trang xóa công việc

Mặc dù mã nguồn đã có thể sử dụng, nhưng chúng ta cần sửa lại templates cho phù hợp.

Kiến trúc MVC

Nếu bạn đã từng phát triển một website bằng PHP mà không dùng framework, thường với mỗi trang HTML bạn sẽ dùng một file PHP. File PHP này sẽ chứa nhiều kiểu cấu trúc: các cấu hình khởi tạo và toàn cục, business logic liên quan đến yêu cầu của trang, lấy các dữ liệu từ database, và cuối cùng tạo mã HTML để hiển thị.

Bạn có thể sử dụng một templating engine để tách phần logic và HTML. Tất nhiên, bạn cũng có thể sử dụng một database abstraction layer để tách phần thao tác với model ra khỏi business logic. Nhưng thường bạn sẽ tạo ra rất nhiều code mà việc maintain trở thành cơn ác mộng. Có thể bạn sẽ xây dựng ứng dụng rất nhanh, nhưng thật khó để thay đổi, nâng cấp, đặc biệt khi không có ai ngoại trừ bạn hiểu được cách nó làm việc.

Có một giải pháp tuyệt vời để giải quyết những vấn đề trên. Đối với việc phát triển web , giải pháp thường dùng là tổ chức code theo MVC design pattern. Pattern này chia code thành ba tầng:

Model bao gồm business logic (database nằm ở tầng này). Bạn đã thấy rằng symfony chứa tất cả các class và file liên quan đến Model trong thư mục lib/model.

View là những gì tương tác với người dùng (template engine là một phần của tầng này). Trong symfony, tầng View được tạo bởi PHP templates. Các file này nằm trong các thư mục templates khác nhau mà chúng ta sẽ thấy ở các phần sau trong ngày hôm nay.

Controller thực hiện việc lấy dữ liệu từ Model và chuyển cho View để hiển thị ở client. Khi chúng ta cài symfony trong ngày đầu tiên, chúng ta đã thấy rằng mọi yêu cầu được điều khiển bởi file front controllers (index.php và frontend_dev.php). Những file front controllers này sẽ tìm actions tương ứng để thực hiện yêu cầu đó. Như chúng ta thấy hôm qua, các action được nhóm lại trong module.

Hôm nay, chúng ta sẽ dựa vào những nội dung trong ngày 2 để chỉnh sửa lại mã nguồn đã có sẵn của trang chủ và trang chi tiết công việc. Đồng thời, chúng ta cũng chỉnh sửa rất nhiều file liên quan để làm rõ cấu trúc thư mục của symfony và cách phân chia code giữa các tầng.

Layout

Nếu để ý, bạn sẽ thấy rằng các trang có nhiều phần giống nhau.Bạn cũng hiểu rằng việc lặp lại code thật tệ, bất kể đó là code HTML hay PHP, do đó chúng ta cần tìm cách để giảm sự lặp lại này.

Một cách giải quyết là tách các header và footer thành các file riêng và include chúng vào mỗi template:

Nhưng ở đây, file header và footer không chứa valid HTML. Cần có cách tốt hơn. Thay vì reinventing the wheel, chúng ta dùng một design pattern khác để giải quyết vấn đề này: decorator design pattern. Decorator design pattern giải quyết vấn đề theo cách: sau khi nội dung chính được tạo, ta sẽ dùng một global template để thêm các phần còn lại, global template trong symfony gọi là một layout:

Layout mặc định của một application là file layout.php nằm trong thư mục apps/frontend/templates/. Thư mục này chứa tất cả các global templates cho một application.

Thay layout mặc định của symfony bằng đoạn code sau:

<!-- apps/frontend/templates/layout.php --><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <title>Jobeet - Your best job board</title> <link rel="shortcut icon" href="/favicon.ico" /> <?php include_javascripts() ?> <?php include_stylesheets() ?> </head> <body> <div id="container"> <div id="header"> <div class="content"> <h1><a href="/job"> <img src="/images/jobeet.gif" alt="Jobeet Job Board" /> </a></h1>  <div id="sub_header"> <div class="post"> <h2>Ask for people</h2> <div> <a href="/job/new">Post a Job</a> </div> </div>  <div class="search"> <h2>Ask for a job</h2> <form action="" method="get">

<input type="text" name="keywords" id="search_keywords" /> <input type="submit" value="search" /> <div class="help"> Enter some keywords (city, country, position, ...) </div> </form> </div> </div> </div> </div>  <div id="content"> <?php if ($sf_user->hasFlash('notice')): ?> <div class="flash_notice"><?php echo $sf_user->getFlash('notice') ?></div> <?php endif; ?>  <?php if ($sf_user->hasFlash('error')): ?> <div class="flash_error"><?php echo $sf_user->getFlash('error') ?></div> <?php endif; ?>  <div class="content"> <?php echo $sf_content ?> </div> </div>  <div id="footer"> <div class="content"> <span class="symfony"> <img src="/images/jobeet-mini.png" /> powered by <a href="http://www.symfony-project.org/"> <img src="/images/symfony.gif" alt="symfony framework" /></a> </span> <ul> <li><a href="">About Jobeet</a></li> <li class="feed"><a href="">Full feed</a></li> <li><a href="">Jobeet API</a></li> <li class="last"><a href="">Affiliates</a></li> </ul> </div> </div> </div> </body></html>

Một template trong symfony là một file PHP. Trong layout template, bạn sẽ thấy các PHP functions được gọi và tham chiếu đến các biến PHP. $sf_content là một biến thú vị: nó được tạo bởi framework và chứa code HTML tạo bởi một action.

Nếu bạn truy cập module job (http://jobeet.localhost/frontend_dev.php/job), bạn sẽ thấy các actions bây giờ đều có layout.

Trong layout, chúng ta có một favicon. Bạn có thể download the Jobeet one và đặt nó vào thư mục web/.

Stylesheets, Images và JavaScripts

Chúng ta sẽ tổ chức chọn "best design" vào ngày thứ 21, trong khi chờ đợi chúng ta sẽ dùng tạm một design đơn giản: download các file ảnh và giải nén vào thư mục web/images/; download các file stylesheet và giải nén vào thư mục web/css/.

Lệnh generate:project tạo 3 thư mục mặc định: web/images/ để chứa ảnh, web/css/ để chứa các file css, và web/js/ chứa các file JavaScripts. Tất nhiên, bạn cũng có thể để ở các thư mục khác trong thư mục web/

Bạn đọc tinh ý có thể sẽ thấy rằng, mặc dù file main.css không được nhắc đến trong layout, nhưng nó vẫn được gọi khi tạo HTML. Sao lại có thể như vậy?

File stylesheet được include bởi hàm include_stylesheets() ở <head> trong layout. Hàm include_stylesheets() chính là một helper. Một helper là một function, tạo bởi symfony, nhận tham số và trả về mã HTML. Các helper giúp giảm thời gian code, chúng đóng gói các đoạn mã thường dùng trong template. Helper include_stylesheets() tạo thẻ <link> cho stylesheets.

Nhưng làm thế nào để helper biết cần include file stylesheets nào?

Tầng View có thể cấu hình bằng cách chỉnh sửa file view.yml của application. Đây là nội dung mặc định được tạo ra sau khi dùng lệnh generate:app:

# apps/frontend/config/view.ymldefault: http_metas: content-type: text/html  metas: #title: symfony project #description: symfony project #keywords: symfony, project #language: en #robots: index, follow  stylesheets: [main.css]  javascripts: []  has_layout: on layout: layout

File view.yml chứa cấu hình chung cho tất cả các templates của application. Ví dụ, phần stylesheets được xác định bởi một mảng các file stylesheet được include trong mọi trang của application (việc include được thực hiện bởi helper include_stylesheets() trong layout).

Trong file view.yml, ta viết main.css, chứ không dùng /css/main.css. Symfony sẽ tự động tìm file trong thư mục /css/.

Nếu có nhiều file, symfony sẽ include chúng theo thứ tự như viết trong cấu hình:

stylesheets: [main.css, jobs.css, job.css]

Bạn cũng có thể thay đổi attribute media và bỏ qua đuôi .css:

stylesheets: [main.css, jobs.css, job.css, print: { media: print }]

Cấu hình này sẽ được render thành:

<link rel="stylesheet" type="text/css" media="screen" href="/css/main.css" /><link rel="stylesheet" type="text/css" media="screen" href="/css/jobs.css" /><link rel="stylesheet" type="text/css" media="screen" href="/css/job.css" /><link rel="stylesheet" type="text/css" media="print" href="/css/print.css" />

File view.yml cũng xác định layout sử dụng cho application. Mặc định, tên của nó là layout, tức là file layout.php. Bạn cũng có thể không dùng layout bằng cách chuyển giá trị của has_layout thành false.

Ta thấy rằng file jobs.css chỉ cần ở trang chủ và file job.css chỉ cần ở trang xem chi tiết công việc. Cấu hình của file view.yml có thể thay đổi lại trong từng module cụ thể. Do đó, file view.yml của application chỉ chứa file main.css:

# apps/frontend/config/view.ymlstylesheets: [main.css]

Để cấu hình riêng cho module job, tạo file view.yml trong thư mục apps/frontend/modules/job/config/:

# apps/frontend/modules/job/config/view.ymlindexSuccess: stylesheets: [jobs.css] showSuccess: stylesheets: [job.css]

Dưới mục indexSuccess và showSuccess (chúng là các template ứng với action index và show , sẽ được đề cập đến ở phần sau), bạn có thể chỉnh sửa lại các mục đã có trong phần default ở file view.yml của application.Các cấu hình này sẽ thay thế các cấu hình ở application có cùng nội dung. Bạn cũng có thể tạo một vài cấu hình cho toàn bộ action của module dưới mục all.

Nguyên tắc cấu hình trong symfony

Có nhiều file cấu hình, các cấu hình giống nhau sẽ được xác định bởi các level khác nhau:

Cấu hình mặc định có giá trị trong toàn bộ framework Cấu hình toàn cục cho project (trong thư mục config/) Cấu hình cục bộ cho một application (trong thư mục apps/APP/config/) Cấu hình cục bộ cho một module (trong thư mục

apps/APP/modules/MODULE/config/)

Khi chạy, hệ thống sẽ xác định cấu hình từ tất cả các file này và cache lại để đảm bảo hiệu năng.

Khi một thứ có thể cấu hình dựa trên file cấu hình, nó cũng có thể cấu hình bằng code PHP. Thay vì tạo file view.yml cho module job, bạn có thể sử dụng helper use_stylesheet() để include file stylesheet trong một template:

<?php use_stylesheet('main.css') ?>

Bạn cũng có thể sử dụng helper này trong layout để include một stylesheet chung cho application.

Chọn cách làm nào là tùy sở thích của mỗi người. File view.yml cung cấp cách cấu hình cố định cho các template. Còn khi sử dụng helper use_stylesheet() mọi thứ trở nên mềm dẻo hơn. Với Jobeet, chúng tôi sẽ sử dụng helper use_stylesheet(), vì thế bạn có thể xóa file view.yml và thêm lời gọi use_stylesheet() vào trong template job.

Tương tự, cấu hình JavaScript nằm trong mục javascripts của file view.yml và có thể dùng helper use_javascript() để gọi file JavaScript trong template.

Trang chủ

Trang chủ chính là action index của module job. Action index là phần Controller của trang và liên kết với template, indexSuccess.php, là phần View:

apps/ frontend/ modules/ job/ actions/ actions.class.php templates/ indexSuccess.php

Action

Mỗi action được tạo bởi một phương thức của một lớp. Với trang chủ, đó là lớp jobActions (tên module + Actions) và phương thức executeIndex() (execute + tên action). Nó thực hiện việc lấy tất cả các job từ database:

// apps/frontend/modules/job/actions/actions.class.phpclass jobActions extends sfActions{ public function executeIndex(sfWebRequest $request) { $this->jobeet_job_list = JobeetJobPeer::doSelect(new Criteria()); }  // ...

}

Let's have a closer look at the code: the executeIndex() method (the Controller) calls the Model JobeetJobPeer to retrieve all the jobs (new Criteria()). It returns an array of JobeetJob objects that are assigned to the jobeet_job_list object property.

Tất cả các đối tượng này được tự động chuyển cho template (View). Để chuyển dữ liệu từ Controller cho View, hãy sử dụng $this-> :

public function executeIndex(sfWebRequest $request){ $this->foo = 'bar'; $this->bar = array('bar', 'baz');}

Bây giờ, trong template ta có thể sử dụng các biến $foo và $bar.

Template

Mặc định, template được đặt tên trùng với tên action kèm cụm Success, nhờ đó symfony có thể xác định action tương ứng.

Template indexSuccess.php được sinh tự động gồm mã HTML theo cấu trúc table:

<!-- apps/frontend/modules/job/templates/indexSuccess.php --><h1>Job List</h1> <table> <thead> <tr> <th>Id</th> <th>Category</th> <th>Type</th><!-- more columns here --> <th>Created at</th> <th>Updated at</th> </tr> </thead> <tbody> <?php foreach ($jobeet_job_list as $jobeet_job): ?> <tr> <td> <a href="<?php echo url_for('job/show?id='.$jobeet_job->getId()) ?>"> <?php echo $jobeet_job->getId() ?> </a> </td> <td><?php echo $jobeet_job->getCategoryId() ?></td> <td><?php echo $jobeet_job->getType() ?></td><!-- more columns here --> <td><?php echo $jobeet_job->getCreatedAt() ?></td> <td><?php echo $jobeet_job->getUpdatedAt() ?></td>

</tr> <?php endforeach; ?> </tbody></table> <a href="<?php echo url_for('job/new') ?>">New</a>

Trong template, foreach duyệt qua danh sách các Job objects ($jobeet_job_list), và với mỗi job, giá trị của từng cột được hiển thị. Ở đây, việc truy cập các giá trị của cột đơn giản là gọi một phương thức accessor có tên bắt đầu bằng get kèm theo tên cột viết hoa chữ cái đầu (ví dụ phương thức getCreatedAt() với cột created_at). (đơn giản hơn có thể dùng: $jobeet_job->id, $jobeet_job->type, ... - người dịch :D)

Ta chỉ cần hiển thị một vài cột:

<!-- apps/frontend/modules/job/templates/indexSuccess.php --><?php use_stylesheet('jobs.css') ?> <div id="jobs"> <table class="jobs"> <?php foreach ($jobeet_job_list as $i => $job): ?> <tr class="<?php echo fmod($i, 2) ? 'even' : 'odd' ?>"> <td><?php echo $job->getLocation() ?></td> <td> <a href="<?php echo url_for('job/show?id='.$job->getId()) ?>"> <?php echo $job->getPosition() ?> </a> </td> <td><?php echo $job->getCompany() ?></td> </tr> <?php endforeach; ?> </table></div>

Hàm url_for() là một symfony helper sẽ được đề cập vào ngày mai.

Job Page Template

Bây giờ hãy chỉnh sửa giao diện của trang chi tiết công việc. Mở file showSuccess.php và thay toàn bộ nội dung bằng đoạn code sau:

<?php use_stylesheet('job.css') ?><?php use_helper('Text') ?> <div id="job"> <h1><?php echo $job->getCompany() ?></h1> <h2><?php echo $job->getLocation() ?></h2> <h3> <?php echo $job->getPosition() ?> <small> - <?php echo $job->getType() ?></small>

</h3>  <?php if ($job->getLogo()): ?> <div class="logo"> <a href="<?php echo $job->getUrl() ?>"> <img src="<?php echo $job->getLogo() ?>" alt="<?php echo $job->getCompany() ?> logo" /> </a> </div> <?php endif; ?>  <div class="description"> <?php echo simple_format_text($job->getDescription()) ?> </div>  <h4>How to apply?</h4>  <p class="how_to_apply"><?php echo $job->getHowToApply() ?></p>  <div class="meta"> <small>posted on <?php echo $job->getCreatedAt('m/d/Y') ?></small> </div>  <div style="padding: 20px 0"> <a href="<?php echo url_for('job/edit?id='.$job->getId()) ?>">Edit</a> </div></div>

Template sử dụng biến $job lấy từ action để hiển thị thông tin công việc. Do đó, chúng ta cần đổi tên biến từ $jobeet_job thành $job trong action show (có 2 chỗ cần sửa):

// apps/frontend/modules/job/actions/actions.class.phppublic function executeShow(sfWebRequest $request){ $this->job = JobeetJobPeer::retrieveByPk($request->getParameter('id')); $this->forward404Unless($this->job);}

Notice that some Propel accessors take arguments. As we have defined the created_at column as a timestamp, the getCreatedAt() accessor takes a date formatting pattern as its first argument:

$job->getCreatedAt('m/d/Y');

Phần mô tả công việc sửa dụng helper simple_format_text() để format nội dung, bằng cách thay thế kí tự xuống dòng thành mã html <br />. Helper này nằm trong nhóm helper Text, mặc định không được tự động load, do đó chúng ta cần dùng helper use_helper() để load.

Slots

Hiện tại, tiêu đề của tất cả các trang được xác định trong thẻ <title> ở layout:

<title>Jobeet - Your best job board</title>

Nhưng với trang chi tiết công việc, chúng ta cần cung cấp nhiều thông tin hữu ích hơn, như là tên công ty và vị trí tuyển dụng.

Trong symfony, khi một vùng của layout phụ thuộc vào template, ta sử dụng slot:

Thêm một slot vào layout cho phép title có thể tự động thay đổi:

// apps/frontend/templates/layout.php<title><?php include_slot('title') ?></title>

Mỗi slot được xác định bởi một tên (title) và được hiển thị qua helper include_slot(). Bây giờ, ở đầu template showSuccess.php, dùng helper slot() để xác định nội dung của slot:

// apps/frontend/modules/job/templates/showSuccess.php<?php slot('title', sprintf('%s is looking for a %s', $job->getCompany(), $job->getPosition())) ?>

Nếu tiêu đề phức tạp, ta có thể đặt trong block:

// apps/frontend/modules/job/templates/showSuccess.php<?php slot('title') ?> <?php echo sprintf('%s is looking for a %s', $job->getCompany(), $job->getPosition()) ?><?php end_slot(); ?>

Với một vài trang, như trang chủ, chúng ta cần một tiêu đề mặc định. Do đó, ta có thể sử dụng tiêu đề mặc định trong layout nếu không có slot:

// apps/frontend/templates/layout.php<title> <?php if (!include_slot('title')): ?> Jobeet - Your best job board <?php endif; ?></title>

Helper include_slot() trả về true nếu slot được xác định. Khi đó nội dung của slot trong template sẽ được sử dụng;ngược lại, giá trị mặc định sẽ được dùng

Chúng ta đã có một vài helper bắt đầu bằng include_. Chúng ta có thể thay bằng get_ để xem kết quả trả về của những helper này:

<?php include_slot('title') ?><?php echo get_slot('title') ?> <?php include_stylesheets() ?><?php echo get_stylesheets() ?>

Job Page Action

Trang chi tiết công việc được tạo ra bởi action show, xác định trong phương thức executeShow() của module job:

class jobActions extends sfActions{ public function executeShow(sfWebRequest $request) { $this->job = JobeetJobPeer::retrieveByPk($request->getParameter('id')); $this->forward404Unless($this->job); }  // ...}

As in the index action, the JobeetJobPeer class is used to retrieve a job, this time by using the retrieveByPk() method. The parameter of this method is the unique identifier of a job, its primary key. The next section will explain why the $request->getParameter('id') statement returns the job primary key.

The generated model classes contain a lot of useful methods to interact with the project objects. Take some time to browse the code located in the lib/om/ directory and discover all the power embedded in these classes.

Nếu công việc không tồn tại trong database, chúng ta muốn chuyển người dùng tới trang 404, điều đó được thực hiện nhờ phương thức forward404Unless().

Trang này hiển thị khác nhau trong môi trường prod và dev:

Trước khi chúng ta đưa Jobeet website lên chạy trên server, bạn sẽ học cách chỉnh sửa trang 404 mặc định.

Các phương thức "forward"

Phương thức forward404Unless tương đương với:

$this->forward404If(!$this->job);

và:

if (!$this->job){ $this->forward404();}

Phương thức forward404() là cách viết gọn của:

$this->forward('default', '404');

Phương thức forward() chuyển đến một action khác trong cùng application; ở ví dụ này, là action 404 của module default. Module default là module có sẵn trong symfony chứa các action mặc định để render các trang 404, secure, và login.

Request và Response

Khi bạn truy cập trang /job hoặc /job/show/id/1 từ trình duyệt: trình duyệt gửi một request và server trả về một response.

Chúng ta đã thấy rằng symfony đóng gói request trong đối tượng sfWebRequest (xem tham số truyền vào phương thức executeShow()). Và symfony là một framework hướng đối tượng, do đó response cũng là một đối tượng, của lớp sfWebResponse. Bạn có thể truy cập đối tượng response trong action bằng cách gọi $this->getResponse().

Những đối tượng này cung cấp nhiều phương thức tiện lợi để truy cập các thông tin từ PHP functions và PHP global variables.

Tại sao symfony lại phải đóng gói các PHP function có sẵn? Đầu tiên, là bởi vì các phương thức của symfony mạnh hơn các function PHP tương ứng. Ngoài ra, khi bạn test một ứng dụng, việc giả lập các đối tượng request và response đơn giản hơn hẳn việc dùng các global variables hay làm việc với các PHP functions như header().

Request

Lớp sfWebRequest gồm $_SERVER, $_COOKIE, $_GET, $_POST, và $_FILES PHP global arrays:

Method name PHP equivalentgetMethod() $_SERVER['REQUEST_METHOD']getUri() $_SERVER['REQUEST_URI']getReferer() $_SERVER['HTTP_REFERER']getHost() $_SERVER['HTTP_HOST']getLanguages() $_SERVER['HTTP_ACCEPT_LANGUAGE']getCharsets() $_SERVER['HTTP_ACCEPT_CHARSET']isXmlHttpRequest()$_SERVER['X_REQUESTED_WITH'] == 'XMLHttpRequest'getHttpHeader() $_SERVERgetCookie() $_COOKIEisSecure() $_SERVER['HTTPS']getFiles() $_FILESgetGetParameter() $_GETgetPostParameter()$_POSTgetUrlParameter() $_SERVER['PATH_INFO']getRemoteAddress()$_SERVER['REMOTE_ADDR']

Chúng ta có thể truy cập tham số qua phương thức getParameter(). Nó trả về giá trị của $_GET hoặc $_POST global variable, hoặc từ PATH_INFO variable.

Nếu bạn muốn biết rõ request parameter là từ biến nào trong các biến trên, bạn có thể sử dụng getGetParameter(), getPostParameter(), getUrlParameter()`

Khi bạn muốn hạn chế một action với một method xác định, ví dụ bạn muốn form được submitt qua POST, bạn có thể dùng phương thức isMethod(): $this->forwardUnless($request->isMethod('POST'));.

Response

Lớp sfWebResponse chứa header() và setrawcookie() PHP methods:

Method name PHP equivalentsetCookie() setrawcookie()setStatusCode() header()setHttpHeader() header()setContentType() header()addVaryHttpHeader() header()addCacheControlHttpHeader()header()

Tất nhiên, lớp sfWebResponse cũng cung cấp phương thức để tạo nội dung của response (setContent()) và gửi response tới trình duyệt (send()).

Ở trên, chúng ta đã biết cách quản lý stylesheets và JavaScripts từ file view.yml và templates. Ở đây, ta có thể dùng phương thức addStylesheet() và addJavascript() của đối tượng response.

Lớp sfAction, sfRequest, và sfResponse cung cấp rất nhiều phương thức hữu ích. Bạn có thể vào API documentation để tra cứu tất cả các lớp của symfony.

Hẹn gặp lại ngày mai

Hôm nay, chúng ta đã nói về một vài design patterns được sử dụng trong symfony. Cấu trúc thư mục của project đã trở nên dễ hiểu. Chúng ta đã có thể làm việc với templates bằng cách chỉnh sửa layout và file template. Chúng ta cũng đã dùng slots và actions.

Nếu bạn muốn gửi một design cho design day contest (sẽ được bầu chọn vào ngày thứ 21), bạn có thể bắt đầu với template chúng tôi đã cung cấp hôm nay.

Ngày mai, chúng ta sẽ tìm hiểu về helper url_for() chúng ta đã dùng hôm nay, và kết hợp với sub-framework routing.

Bạn có thể truy cập mã nguồn của ngày hôm nay (tag release_day_04) tại:

http://svn.jobeet.org/tags/release_day_04/

Trước khi bắt đầu

Hôm qua, chúng ta đã nói về design cho Jobeet. Nếu bạn muốn tham gia, chúng tôi đã chuẩn bị một file nén với các trang chính chúng tôi sẽ phát triển trong tutorial (file nén chứa các file HTML tĩnh, các file stylesheets, và các file ảnh). Vào ngày thứ 21 chúng tôi sẽ tổ chức bầu chọn, bạn có thể gửi cho tôi (fabien.potencier [.at.] symfony-project.com) bản thiết kế của bạn trước ngày này. Chúc may mắn!

Tóm tắt

Nếu bạn đã hoàn thành ngày 4, bạn đã quen với MVC pattern và cảm thấy rằng đó là một cách code thật tự nhiên. Hãy dành một chút thời gian để tìm hiểu nó và chúng ta không còn phải bận tâm về nó nữa. Ngày hôm qua, chúng ta đã chỉnh sửa vài trang Jobeet, đồng thời tìm hiểu một vài khái niệm trong symfony như layout, helpers, và slots.

Hôm nay chúng ta sẽ tìm hiểu về một phần thú vị của symfony: routing.

URLs

Nếu bạn click xem một công việc từ trang chủ, URL sẽ kiểu như: /job/show/id/1. Nếu bạn đã từng phát triển một website bằng PHP, có lẽ bạn sẽ quen với URLs kiểu /job.php?id=1. Symfony xử lý URLs như thế nào? Làm thế nào để symfony biết được action nào được gọi dựa trên URL? Tại sao $request->getParameter('id') trả về id của job ? Hôm nay, chúng ta sẽ trả lời những câu hỏi này.

Nhưng trước tiên, chúng ta hãy nói về URLs. Trong một web context, mỗi URL xác định duy nhất một web resource. Khi bạn truy cập một URL, bạn yêu cầu trình duyệt lấy resource xác định bởi URL đó. Do URL là giao diện tương tác giữa website và người dùng, nên nó cần chứa một vài thông tin hữu ích về resource mà nó tham chiếu đến. Nhưng URLs "truyền thống" không thực sự mô tả resource, nó phơi bày ra cấu trúc bên trong của ứng dụng. Người dùng không quan tâm đến việc website được phát triển bằng ngôn ngữ nào và dữ liệu được chứa trong cơ sở dữ liệu ra sao.Việc lộ rõ cấu trúc bên trong còn tạo ra các vấn đề về bảo mật: người dùng có thể đoán được URL của các resources mà anh ta không được phép truy cập? Tất nhiên, lập trình viên phải bảo mật cho những khu vực này, nhưng tốt nhất là hãy ẩn đi các thông tin nhạy cảm.

URLs trong symfony quan trọng đến mức chúng ta có một framework riêng để quản lý chúng: routing framework. Routing quản lý internal URIs và external URLs. Khi có một request đến, routing phân tích URL và chuyển thành internal URI.

Bạn đã thấy internal URI cho trang job ở template showSuccess.php:

'job/show?id='.$job->getId()

Helper url_for() chuyển internal URI này thành URL:

/job/show/id/1

Một internal URI được tạo thành từ các phần: job là module, show là action và query string thêm tham số để gửi cho action. Cấu trúc thông dụng của một internal URIs như sau:

MODULE/ACTION?key=value&key_1=value_1&...

Trong symfony, routing được tách riêng ra, bạn có thể thay đổi URLs mà không ảnh hưởng đến việc xử lý bên trong. Đó là một trong những lợi ích của front-controller design pattern.

Cấu hình Routing

Sự tương ứng giữa internal URIs và external URLs được ghi trong file routing.yml:

# apps/frontend/config/routing.ymlhomepage: url: / param: { module: default, action: index } default_index: url: /:module param: { action: index } default: url: /:module/:action/*

File routing.yml chứa thông tin về các route. Một route bao gồm tên (homepage), pattern (/:module/:action/*), và một vài tham số (nằm sau từ khóa param).

Khi có một request, routing sẽ so sánh URL với các pattern đã có. Route tìm thấy đầu tiên sẽ được sử dụng, do đó thứ tự trong routing.yml rất quan trọng. Chúng ta sẽ xem xét vài ví dụ để hiểu rõ hơn cách hoạt động của nó.

Khi bạn request trang chủ, có URL là /job , route đầu tiên phù hợp là default_index. Trong một pattern, chuỗi nằm sau dấu hai chấm (:) là một biến, vì thế pattern /:module có nghĩa là: / theo sau là một chuỗi nào đó. Trong ví dụ của chúng ta, biến module có giá trị là job. Giá trị này có thể nhận bằng cách $request->getParameter('module'). Route này cũng xác định sẵn một giá trị mặc định cho biến action. Vì thế, với mọi URLs tương ứng với route này, tham số action luôn có giá trị là index.

Nếu bạn request trang /job/show/id/1, symfony sẽ match với pattern cuối cùng: /:module/:action/*. Ở pattern này, dấu (*) tương ứng với tập các cặp biến/giá trị cách nhau bởi dấu (/):

Request parameter Valuemodule jobaction showid 1

module và action là các biến đặc biệt, symfony dùng để xác định action được thực thị.

URL /job/show/id/1 có thể tạo từ template bằng helper url_for():

url_for('job/show?id='.$job->getId())

Bạn cũng có thể dùng tên của route với kí tự @ đằng trước:

url_for('@default?id='.$job->getId())

Hai cách gọi là tương đương nhưng cách gọi sau là nhanh hơn do routing không phải phân tích mọi route để tìm ra route thích hợp nhất, và nó cũng dễ khi sử dụng (không cần dùng tên module và action trong internal URI).

Route Customizations

Hiện tại, khi bạn truy cập URL / từ trình duyệt, bạn sẽ thấy trang chào mừng mặc định của symfony. Đó là vì URL này matches với route homepage. Ta cần thay đổi route này thành trang chủ của Jobeet: sửa giá trị biến module thành job:

# apps/frontend/config/routing.ymlhomepage: url: / param: { module: job, action: index }

Bây giờ, chúng ta có thể sửa lại link ở Jobeet logo trong layout dùng route homepage:

<h1> <a href="<?php echo url_for('@homepage') ?>"> <img src="/images/jobeet.gif" alt="Jobeet Job Board" /> </a></h1>

Thật đơn giản! Phức tạp hơn một chút, chúng ta cần đổi URL của trang job thành:

/job/sensio-labs/paris-france/1/web-developer

Không cần xem chi tiết nội dung, chỉ cần nhìn URL bạn cũng có thể biết được rằng Sensio Labs đang tìm một Web developer làm việc ở Paris, France.

Một URLs đẹp rất quan trọng bởi vì nó truyền tải thông tin đến người dùng. Nó cũng hữu dụng khi bạn copy và paste URL vào email hay optimize website cho các search engines.

Pattern dưới đây match với URL trên:

/job/:company/:location/:id/:position

Sửa file routing.yml và thêm route job vào đầu file:

job: url: /job/:company/:location/:id/:position param: { module: job, action: show }

Refresh lại trang chủ, link tới các job vẫn không thay đổi. Đó là vì để tạo route, bạn cần cung cấp tất cả các biến cần thiết. Vì thế, url_for() trong indexSuccess.php cần sửa lại thành:

url_for('job/show?id='.$job->getId().'&company='.$job->getCompany(). '&location='.$job->getLocation().'&position='.$job->getPosition())

Một internal URI cũng có thể viết ở dạng array:

url_for(array( 'module' => 'job', 'action' => 'show', 'id' => $job->getId(), 'company' => $job->getCompany(), 'location' => $job->getLocation(), 'position' => $job->getPosition(),))

Requirements

Trong ngày đầu tiên, chúng ta đã nói về validation và error handling. Hệ thống routing có sẵn tính năng validation. Mỗi biến trong pattern được validate bởi một regular expression xác định trong mục requirements của route:

job: url: /job/:company/:location/:id/:position param: { module: job, action: show } requirements: id: \d+

Mục requirements ở trên bắt buộc id phải là số. Nếu không, route sẽ không match.

Route Class

Mỗi route xác định trong file routing.yml được chuyển đổi thành một object của lớp sfRoute. Có thể đổi lớp này bằng lớp khác xác định trong mục class của route. Ta đã biết rằng giao thức HTTP có vài "methods": GET, POST, HEAD, DELETE, và PUT. 3 method đầu được hỗ trợ bởi tất cả các trình duyệt, trong khi 2 method sau thì không.

Để route chỉ match với một loại request methods nào đó, bạn có thể đổi route class thành sfRequestRoute và thêm một giá trị cho biến sf_method trong requirements:

job: url: /job/:company/:location/:id/:position class: sfRequestRoute param: { module: job, action: show } requirements: id: \d+ sf_method: [GET]

bắt buộc route chỉ match với một HTTP methods nào đó tương đương với việc sử dụng sfWebRequest::isMethod() trong action.

Object Route Class

Mỗi internal URI cho một job thật là dài và bất tiện khi viết, nhưng như chúng ta đã biết ở phần trước, có thể thay đổi route class. Với job route, ta nên dùng lớp sfPropelRoute để mô tả các Propel objects và collections of Propel objects:

job_show_user: url: /job/:company/:location/:id/:position class: sfPropelRoute options: { model: JobeetJob, type: object } param: { module: job, action: show } requirements: id: \d+ sf_method: [GET]

Ở đây, option model xác định lớp Propel model (JobeetJob) liên quan đến route, và option type cho biết route này liên quan đến 1 object (bạn có thể dùng list nếu route mô tả tập các objects).

Route job_show_user bây giờ liên quan đến JobeetJob vì thế url_for() có thể viết đơn giản như sau:

url_for(array('sf_route' => 'job_show_user', 'sf_subject' => $job))

hoặc:

url_for('job_show_user', $job)

Điều này trở nên tiện lợi khi bạn cần cung cấp nhiêu tham số.

Nó hoạt động được bởi vì mỗi biến trong route đều tương ứng với một phương thức accessor trong lớp JobeetJob (ví dụ, biến company được thay bằng giá trị trả về của phương thức getCompany()).

Nếu bạn nhìn URLs đã được tạo ra, bạn sẽ thấy rằng đó chưa thực sự là những gì chúng ta muốn:

http://jobeet.localhost/frontend_dev.php/job/Sensio+Labs/Paris%2C+France/1/Web+Developer

Chúng ta cần "slugify" giá trị các cột bằng cách thay thế các kí tự non ASCII bằng kí tự -. Mở file JobeetJob và thêm các phương thức sau vào trong lớp:

// lib/model/JobeetJob.phppublic function getCompanySlug(){ return Jobeet::slugify($this->getCompany());} public function getPositionSlug(){ return Jobeet::slugify($this->getPosition());} public function getLocationSlug(){ return Jobeet::slugify($this->getLocation());}

Sau đó, tạo file lib/Jobeet.class.php và thêm phương thức slugify vào:

// lib/Jobeet.class.phpclass Jobeet{ static public function slugify($text) { // replace all non letters or digits by - $text = preg_replace('/\W+/', '-', $text);  // trim and lowercase $text = strtolower(trim($text, '-'));  return $text; }}

Chúng ta đã tạo ra 3 "virtual" accessors mới: getCompanySlug(), getPositionSlug(), và getLocationSlug(). Bây giờ, bạn có thể thay thế tên của các cột bằng các tên mới này trong route job_show_user:

job_show_user: url: /job/:company_slug/:location_slug/:id/:position_slug class: sfPropelRoute options: { model: JobeetJob, type: object } param: { module: job, action: show } requirements: id: \d+

sf_method: [GET]

Trước khi refresh trang chủ, bạn cần xóa cache do đã tạo một lớp mới (Jobeet):

$ php symfony cc

Bây giờ URLs đã hoàn hảo như mong đợi:

http://jobeet.localhost/frontend_dev.php/job/sensio-labs/paris-france/4/web-developer

Nhưng đó chỉ là một phần của câu chuyện. Route có thể tạo ra URL dựa trên một object, ngược lại ta cũng có thể xác định một object dựa vào URL liên quan đến nó.Object này có thể nhận bằng phương thức getObject() của đối tượng route. Khi phân tích một request đến, routing lưu lại object, vì thế bạn có thể dùng trong actions. Bạn có thể sửa lại phương thức executeShow() :

class jobActions extends sfActions{ public function executeShow(sfWebRequest $request) { $this->job = $this->getRoute()->getObject(); $this->forward404Unless($this->getRoute()->getObject()); }  // ...}

Nếu bạn thử xem một job có id không đúng, bạn sẽ được chuyển sang trang 404 error nhưng thông báo lỗi đã thay đổi:

Đó là bởi vì phương thức getRoute() đã tự động bắt lỗi 404 giúp bạn. Do đó, phương thức executeShow có thể sửa lại thành:

class jobActions extends sfActions

{ public function executeShow(sfWebRequest $request) { $this->job = $this->getRoute()->getObject(); }  // ...}

Nếu bạn không muốn route tạo một lỗi 404, bạn có thể để lựa chọn allow_empty routing là true.

Routing trong Actions và Templates

Trong temlate, helper url_for() chuyển đổi internal URI thành external URL. Một vài symfony helpers khác nhận internal URI làm tham số, như helper link_to() sẽ tạo một thẻ <a>:

<?php echo link_to($job->getPosition(), 'job_show_user', $job) ?>

Mã HTML được tạo ra:

<a href="/job/sensio-labs/paris-france/1/web-developer">Web Developer</a>

Cả url_for() và link_to() đều tạo ra đường dẫn tuyệt đối:

url_for('job_show_user', $job, true); link_to($job->getPosition(), 'job_show_user', $job, true);

Nếu bạn muốn tạo ra một URL trong action, bạn có thể dùng phương thức generateUrl():

$this->redirect($this->generateUrl('job_show_user', $job));

Các phương thức "redirect"

Hôm qua, chúng ta đã nói về các phương thức "forward". Những phương thức đó chuyển yêu cầu đến action khác mà không thay đổi URL

Các phương thức "redirect" chuyển người dùng tới URL khác. Giống như forward, ta có các phương thức redirect(), redirectIf() và redirectUnless().

Collection Route Class

Với module job, chúng ta đã chỉnh sửa route cho action show, còn URLs cho các phương thức khác (index, new, edit, create, update, và delete) vẫn được quản lý bởi route default:

default: url: /:module/:action/*

The default route is a great way to start coding without defining too many routes. But as the route acts as a "catch-all", it cannot be configured for specific needs.

Tất các các action trong module job đều liên quan đến JobeetJob model class, do đó chúng ta có thể dễ dàng dùng class sfPropelRoute route cho từng action như đã làm với action show. Tuy nhiên chúng ta có thể dùng lớp sfPropelRouteCollection cho cả 7 action này:

// apps/frontend/config/routing.yml # put this definition just before the job_show_user onejob: class: sfPropelRouteCollection options: { model: JobeetJob }

Route job tương đương với 7 sfPropelRoute routes sau:

job: url: /job.:sf_format class: sfPropelRoute options: { model: JobeetJob, type: list } param: { module: job, action: index, sf_format: html } requirements: { sf_method: GET } job_new: url: /job/new.:sf_format class: sfPropelRoute options: { model: JobeetJob, type: object } param: { module: job, action: new, sf_format: html } requirements: { sf_method: GET } job_create: url: /job.:sf_format class: sfPropelRoute options: { model: JobeetJob, type: object } param: { module: job, action: create, sf_format: html } requirements: { sf_method: POST } job_edit: url: /job/:id/edit.:sf_format class: sfPropelRoute options: { model: JobeetJob, type: object } param: { module: job, action: edit, sf_format: html } requirements: { sf_method: GET } job_update:

url: /job/:id.:sf_format class: sfPropelRoute options: { model: JobeetJob, type: object } param: { module: job, action: update, sf_format: html } requirements: { sf_method: PUT } job_delete: url: /job/:id.:sf_format class: sfPropelRoute options: { model: JobeetJob, type: object } param: { module: job, action: delete, sf_format: html } requirements: { sf_method: DELETE } job_show: url: /job/:id.:sf_format class: sfPropelRoute options: { model: JobeetJob, type: object } param: { module: job, action: show, sf_format: html } requirements: { sf_method: GET }

Một vài route tạo ra bởi sfPropelRouteCollection có URL giống nhau. Routing vẫn có thể dùng chúng bởi vì chúng yêu cầu các HTTP method khác nhau.

Route job_delete và job_update yêu cầu các HTTP methods không được hỗ trợ bởi trình duyệt (DELETE và PUT). Nó vẫn hoạt động bởi vì symfony đã mô phỏng chúng. Mở template _form.php để xem một ví dụ:

// apps/frontend/modules/job/templates/_form.php<form action="..." ...><?php if (!$form->getObject()->isNew()): ?> <input type="hidden" name="sf_method" value="PUT" /><?php endif; ?> <?php echo link_to( 'Delete', 'job/delete?id='.$form->getObject()->getId(), array('method' => 'delete', 'confirm' => 'Are you sure?')) ?>

Các symfony helpers có thể nhận bất kì HTTP method nào thông qua tham số sf_method.

symfony có nhiều tham số tương tự sf_method, tất cả đều bắt đầu bởi sf_. Ở routes bên trên, bạn có thể thấy tham số:sf_format, sẽ được làm rõ trong một vài ngày tới.

Route Debugging

Khi bạn dùng collection routes, có thể bạn cần list danh sách các route được tạo ra. Lệnh app:routes liệt kê tất cả các routes của một application:

$ php symfony app:routes frontend

Bạn cũng có thể có nhiều thông tin hơn để debug cho route bằng cách cung cấp thêm tham số:

$ php symfony app:routes frontend job_edit

Routes mặc định

Tốt nhất là tạo routes cho tất cả các URL. Nếu bạn làm được điều đó, hãy xóa bỏ hoặc comment các routes mặc định trong file routing.yml:

// apps/frontend/config/routing.yml#default_index:# url: /:module# param: { action: index }##default:# url: /:module/:action/*

Hẹn gặp lại ngày mai

Hôm nay, chúng ta đã biết thêm rất nhiều kiến thức mới. Chúng ta đã học cách sử dụng routing framework của symfony và cách tách URLs ra khỏi hệ thống xử lý.

Chúng ta sẽ học tutorial mới vào ngày mai, mặc dù mai là thứ 7. Chúng tôi sẽ không giới thiệu một nội dung mới nào, thay vào đó chúng ta sẽ đi sâu vào tìm hiểu những gì đã biết.

Mã nguồn của ngày hôm nay đã được đưa lên kho chứa SVN:

http://svn.jobeet.org/tags/release_day_05/

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.logDec 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>:p1Dec 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).

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.phpclass 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.ymlJobeetJob: # 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 expires_at: 2005-12-01 token: job_expired email: [email protected]

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.ymlall: 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.phpclass 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.phpclass 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.phppublic 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>

Để 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.phppublic 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.phpstatic 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: [email protected] <?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.ymljob_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.phpstatic 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/

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.ymlcategory: url: /category/:slug class: sfPropelRoute param: { module: category, action: show } options: { model: JobeetCategory, type: object }

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.phppublic 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.phppublic 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.phpclass 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.

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.

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.ymlpropel: 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.phppublic 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.phpclass 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.

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.phppublic 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();}

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.ymlall: 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.phppublic 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.phppublic 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="/images/first.png" alt="First page" /> </a>

  <a href="<?php echo url_for('category', $category) ?>?page=<?php echo $pager->getPreviousPage() ?>"> <img src="/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="/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="/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ại getNbResults(): trả về tổng số kết quả haveToPaginate(): trả về true nếu có nhiều hơn một trang getLinks(): trả về danh sách link đến các trang getPage(): trả về số của trang hiện tại getPreviousPage(): trả về số của trang trước getNextPage(): trả về số của trang tiếp getLastPage(): 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/

Tóm tắt

Trong những ngày cuối tuần vừa qua, chúng ta đã chỉnh sửa lại những tính năng đã có và thêm một tính năng mới. Trong quá trình đó, chúng ta cũng học được nhiều kiến thức về symfony.

Hôm nay, chúng ta sẽ nói về một chủ đề hoàn toàn khác: test tự động. Vì vấn đề này rất rộng nên chúng ta sẽ dành trọn 2 ngày để nói về nó.

Test trong symfony

Có hai loại automated test trong symfony: unit tests và functional tests.

Unit test để đảm bảo rằng mỗi method và function đều hoạt động đúng. Các test phải độc lập với nhau.

Còn functional test là để đảm bảo rằng kết quả trả về của ứng dụng đúng như mong đợi.

Tất cả test của symfony đều nằm trong thư mục test/ của project. Nó chứa 2 thư mục con, một cho unit tests (test/unit/) và một cho functional tests (test/functional/).

Unit test sẽ được đề cập đến trong ngày hôm nay, ngày mai chúng ta sẽ nói về functional test.

Unit Tests

Viết unit tests có lẽ là một trong những cần thiết nhất đề có thể phát triển một ứng dụng tốt. Đối với một web developer chưa từng sử dụng test trong công việc, sẽ xuất hiện rất nhiều câu hỏi: Tôi phải viết test trước khi thực hiện một chức năng? Tôi cần test để làm gì? Test của tôi cần bao quát mọi trường hợp có thể? Làm thế nào để chắc rằng mọi thứ được test đúng? Nhưng thường có một câu hỏi cơ bản hơn: Bắt đầu như thế nào?

Mặc dù chúng tôi tán thành việc test, nhưng symfony cũng khá thực tế: có một vài test tốt hơn là không có test nào. Bạn đã viết rất nhiều code mà không có bất kì test nào? Không sao cả. Bạn không cần thiết phải có tất cả bộ test để hiểu lợi ích của việc test. Hãy bắt đầu thêm tests khi bạn tìm thấy một bug trong code của bạn. Sau đó, code của bạn sẽ trở nên tốt hơn. Bắt đầu theo cách thực dụng như vậy, bạn sẽ cảm thấy quen với việc tests hơn. Sau đó, chúng ta có thể viết test cho một tính năng mới trước khi thực hiện nó. Không lâu sau, bạn sẽ trở thành một con nghiện test :)

Một vấn đề với phần lớn các thư viện test là sự phức tạp của nó. Vì thế symfony cung cấp một thư viện test đơn giản, lime, khiến cho việc viết test trở nên dễ dàng hơn.

Mặc dù hướng dẫn này nói về thư viện có sẵn lime , nhưng bạn hoàn toàn có thể sử dụng bất kì thư viện test nào khác, như PHPUnit chẳng hạn.

lime Testing Framework

Tất cả unit test viết trong lime framework đều bắt đầu như sau:

require_once dirname(__FILE__).'/../bootstrap/unit.php'; $t = new lime_test(1, new lime_output_color());

Đầu tiên, file bootstrap unit.php được include để khởi tạo một vài thứ. Sau đó, một đối tượng lime_test mới được tạo ra kèm theo số lượng test dự định thực hiện.

Việc khai báo số test dự định cho phép lime trả về thông báo lỗi trong trường hợp có quá nhiều test được chạy (ví dụ khi test được tạo bởi một PHP fatal error).

Việc test được thực hiện bằng cách gọi một method hay một function với đầu vào xác định và so sánh kết quả trả về với kết quả mong đợi. Sự so sánh này quyết định một test là passes hay fails.

Để tiện cho việc so sánh, đối tượng lime_test cung cấp một số phương thức:

Phương thức Mô tảok($test) Test một mệnh đề và passes nếu nó đúngis($value1, $value2) So sánh 2 giá trị và passes nếu chúng bằng nhau

(==)isnt($value1, $value2) So sánh 2 giá trị và passes nếu chúng

không bằng nhaulike($string, $regexp) So sánh một chuỗi với một biểu thức chính quyunlike($string, $regexp) Kiểm tra xem chuỗi có không match với một

regular expressionis_deeply($array1, $array2)Kiểm tra xem 2 array có cùng giá trị

Bạn có thể thắc mắc tại sao lime lại tạo ra nhiều method test đến vậy, trong khi tất cả chúng đều có thể được viết từ method ok(). Lợi ích nằm ở chỗ sẽ có các thông báo lỗi khác nhau cho từng trường hợp failed , giúp dễ hiểu hơn.

Đối tượng lime_test cũng cung cấp các phương thức test tiện lợi khác:

Phương thức Mô tảfail() luôn trả về fails--thường dùng cho test exceptionspass() luôn trả về passes--thường dùng cho test exceptionsskip($msg, $nb_tests)Counts as $nb_tests tests--useful for conditional

teststodo() Counts as a test--useful for tests yet to be

written

Cuối cùng, phương thức comment($msg) trả về một comment và không chạy test nào.

Running Unit Tests

Tất cả unit tests được chứa trong thư mục test/unit/. Thông thường, tên file test là tên của lớp mà nó cần test kèm theo cụm Test. Mặc dù bạn có thể tổ chức file trong thư mục test/unit/ như thế nào cũng được, nhưng chúng tôi khuyên bạn sử dụng cấu trúc của thư mục lib/.

Tạo file test/unit/JobeetTest.php và copy đoạn code sau:

// test/unit/JobeetTest.phprequire_once dirname(__FILE__).'/../bootstrap/unit.php'; $t = new lime_test(1, new lime_output_color());$t->pass('This test always passes.');

Để chạy file test, bạn có thể gọi trực tiếp:

$ php test/unit/JobeetTest.php

Hoặc dùng lệnh test:unit:

$ php symfony test:unit Jobeet

Dòng lệnh trong Windows không thể highlight kết quả test với màu đỏ hoặc xanh.

Test phương thức slugify

Hãy bắt đầu khám phá thế giới unit test bằng cách viết tests cho phương thức Jobeet::slugify().

Chúng ta tạo phương thức slugify() ở ngày 5 thực hiện việc format lại chuỗi để an toàn hơn khi đưa lên URL. Nó chuyển tất cả các kí tự non-ASCII thành kí tự (-) và chuyển kí tự viết hoa thành viết thường:

Input OutputSensio Labs sensio-labsParis, France paris-france

Thay nội dung của file test bằng đoạn code sau:

// test/unit/JobeetTest.phprequire_once dirname(__FILE__).'/../bootstrap/unit.php'; $t = new lime_test(6, new lime_output_color()); $t->is(Jobeet::slugify('Sensio'), 'sensio');$t->is(Jobeet::slugify('sensio labs'), 'sensio-labs');$t->is(Jobeet::slugify('sensio labs'), 'sensio-labs');$t->is(Jobeet::slugify('paris,france'), 'paris-france');$t->is(Jobeet::slugify(' sensio'), 'sensio');$t->is(Jobeet::slugify('sensio '), 'sensio');

Nếu bạn để ý đoạn code trên bạn sẽ thấy rằng mỗi dòng chỉ test một thứ. Đó là điều bạn cần nhớ khi viết unit tests. Chỉ test một thứ ở một thời điểm.

Bây giờ, bạn có thể chạy file test. Nếu tất cả đều pass, như chúng ta mong đợi, bạn sẽ thấy một "green bar". Nếu không, "red bar" sẽ cảnh báo bạn rằng có một số test không pass và bạn cần fix chúng.

Nếu test fail, màn hình sẽ chỉ rõ là test nào failed; nhưng nếu bạn có hàng trăm test trong một file, thật khó để xác định test fails là test về cái gì.

Tất cả các phương thức test đều có tham số cuối là một chuỗi mô tả nội dung test. Hãy thêm một vài thông báo vào file test slugify:

require_once dirname(__FILE__).'/../bootstrap/unit.php'; $t = new lime_test(6, new lime_output_color()); $t->comment('::slugify()');$t->is(Jobeet::slugify('Sensio'), 'sensio', '::slugify() converts all characters to lower case');$t->is(Jobeet::slugify('sensio labs'), 'sensio-labs', '::slugify() replaces a white space by a -');$t->is(Jobeet::slugify('sensio labs'), 'sensio-labs', '::slugify() replaces several white spaces by a single -');$t->is(Jobeet::slugify(' sensio'), 'sensio', '::slugify() removes - at the beginning of a string');$t->is(Jobeet::slugify('sensio '), 'sensio', '::slugify() removes - at the end of a string');$t->is(Jobeet::slugify('paris,france'), 'paris-france', '::slugify() replaces non-ASCII characters by a -');

Code Coverage

When you write tests, it is easy to forget a portion of the code.

To help you check that all your code is well tested, symfony provides the test:coverage task. Pass this task a test file or directory and a lib file or directory as arguments and it will tell you the code coverage of your code:

$ php symfony test:coverage test/unit/JobeetTest.php lib/Jobeet.class.php

If you want to know which lines are not covered by your tests, pass the --detailed option:

$ php symfony test:coverage --detailed test/unit/JobeetTest.php lib/Jobeet.class.php

Keep in mind that when the task indicates that your code is fully unit tested, it just means that each line has been executed, not that all the edge cases have been tested.

As the test:coverage relies on XDebug to collect its information, you need to install it and enable it first.

Thêm Tests cho một tính năng mới

Hàm slug đối với một chuỗi rỗng là một chuỗi rỗng. Bạn có thể test điều này, và hàm này thực hiện đúng như vậy. Nhưng một chuỗi rỗng không được tốt khi đưa lên URL. Hãy sửa phương thức slugify() để nó trả về "n-a" trong trường hợp chuỗi rỗng.

Bạn có thể viết test trước, sau đó sửa lại phương thức, hoặc ngược lại. Chọn cách nào là tùy bạn, tuy nhiên viết test trước đem lại cho bạn cảm giác tin cậy rằng code của bạn thực thi đúng những gì bạn dự định:

$t->is(Jobeet::slugify(''), 'n-a', '::slugify() converts the empty string by n-a');

Nếu bạn chạy test bây giờ, bạn sẽ thấy red bar. Nếu không, tức là tính năng này đã được thực thi hoặc chương trình test bị sai!

Bây giờ, sửa lại lớp Jobeet và thêm đoạn điều kiện sau ở đầu:

// lib/Jobeet.class.phpstatic public function slugify($text){ if (empty($text)) { return 'n-a'; }  // ...}

Test bây giờ sẽ pass như mong đợi, nhưng bạn phải chắc rằng đã sửa lại số lượng test dự kiến. Nếu không, bạn sẽ nhận được thông báo rằng bạn dự định thực hiện 6 test và bạn đã thêm một test ngoài. Dự định trước số lượt test là quan trọng, bạn có thể kiểm soát được nếu test script die sớm.

Thêm Tests khi có một Bug

Khi có một người dùng thông báo một lỗi kì lạ: một vài link đến job chuyển sang trang lỗi 404. Sau khi tìm hiểu, bạn nhận ra rằng vì lý do nào đó, những jobs này có company, position, hay location slug là rỗng. Sao lại có thể như vậy được? Bạn kiểm tra lại những giá trị này trong database và thấy rằng chúng không rỗng. Bạn đau đầu vì nó, và thật mau mắn, cuối cùng bạn cũng tìm ra lý do. Khi một chuỗi chỉ chứa kí tự non-ASCII, phương

thức slugify() sẽ biến nó thành một chuỗi rỗng. Vui sướng vì tìm ra lý do, bạn mở lớp Jobeet và sửa vấn đề ngay lập tức. Đó không phải là cách làm tốt. Đầu tiên, bạn hãy thêm một test:

$t->is(Jobeet::slugify(' - '), 'n-a', '::slugify() converts a string that only contains non-ASCII characters by n-a');

Sau khi kiểm tra rằng test này không pass, sửa lại lớp Jobeet và chuyển đoạn kiểm tra chuỗi rỗng xuống cuối phương thức:

static public function slugify($text){ // ...  if (empty($text)) { return 'n-a'; }  return $text;}

Bây giờ test vừa thêm vào đã passes. Phương thức slugify() bây giờ đã được sửa hết các lỗi.

Bạn không thể nghĩ hết các trường hợp khi viết tests, và điều đó không có vấn đề gì cả.Bất cứ khi nào bạn nghĩ ra thêm một trường hợp, bạn cần thêm vào file test và test nó trước khi sửa lỗi trong code. Code của bạn sẽ ngày càng trở nên tốt hơn.

Cải tiến phương thức slugify

Hãy thử thêm một test với tiếng Việt có dấu:

$t->is(Jobeet::slugify('Lập trình Web'), 'lap-trinh-web', '::slugify() removes accents');

Test này sẽ fail. Thay vì thay thế ậ bởi a, ì bởi i, phương thức slugify() lại thay chúng bởi kí tự (-). Đó là một vấn đề liên quan đến ngôn ngữ. Nếu bạn có cài "iconv" , nó sẽ thực hiện việc chuyển đổi này giúp chúng ta:

// code derived from http://php.vrana.cz/vytvoreni-pratelskeho-url.phpstatic public function slugify($text){ // replace non letter or digits by - $text = preg_replace('~[^\\pL\d]+~u', '-', $text);  // trim $text = trim($text, '-');  // transliterate $text = iconv('utf-8', 'us-ascii//TRANSLIT', $text);  // lowercase $text = strtolower($text);  // remove unwanted characters $text = preg_replace('~[^-\w]+~', '', $text);  if (empty($text)) { return 'n-a'; }  return $text;}

Hãy luôn nhớ lưu tất cả các file PHP với encode UTF-8

Sửa lại file test để chạy test chỉ khi có "iconv":

if (function_exists('iconv')){ $t->is(Jobeet::slugify('Développeur Web'), 'developpeur-web', '::slugify() removes accents');}else{ $t->skip('::slugify() removes accents - iconv not installed');}

Propel Unit Tests

Cấu hình Database

Unit testing cho một lớp Propel model thì phức tạp hơn một chút, nó yêu cầu kết nối tới database. Bạn đã có một kết nối dùng cho development, nhưng tốt hơn là nên tạo một database riêng dành cho việt tests.

Trong ngày 1, chúng ta đã giới thiệu về các môi trường khác nhau của ứng dụng. Mặc định, tất cả các symfony tests đều chạy trong môi trường test, vì thế cần cấu hình một database khác cho môi trường test:

$ php symfony configure:database --env=test "mysql:host=localhost;dbname=jobeet_test" root mYsEcret

Lựa chọn env chỉ rõ cấu hình database là dành cho môi trường nào. Khi chúng ta thực hiện lệnh này trong ngày 3, chúng ta không cung cấp lựa chọn env, do đó cấu hình của chúng ta dành cho mọi môi trường.

Nếu bạn tò mò, bạn có thể mở file config/databases.yml để xem cách symfony thay đổi cấu hình cho từng môi trường.

Bây giờ, chúng ta đã có cấu hình tới database, chúng ta có thể bắt đầu tạo cơ sở dữ liệu:

$ mysqladmin -uroot -pmYsEcret create jobeet_test$ php symfony propel:insert-sql --env=test

Nguyên tắc cấu hình trong symfony

Trong ngày 4, chúng ta đã biết rằng cấu hình trong các file cấu hình khác nhau được xác định ở những mức độ khác nhau.

Những thiết lập đó cũng phụ thuộc môi trường. Điều đó là đúng với phần lớn những file cấu hình chúng ta đã dùng cho đến nay: databases.yml, app.yml, view.yml, và settings.yml. Trong tất cả những file này, key là tên môi trường, key all chỉ ra rằng cấu hình này là cho mọi môi trường:

# config/databases.ymldev: propel: class: sfPropelDatabase param: classname: DebugPDO test: propel: class: sfPropelDatabase param: classname: DebugPDO dsn: 'mysql:host=localhost;dbname=jobeet_test' all: propel: class: sfPropelDatabase

param: dsn: 'mysql:host=localhost;dbname=jobeet' username: root password: null

Dữ liệu Test

Bây giờ chúng ta đã có một cơ sở dữ liệu riêng cho việc test, chúng ta cần load một vài dữ liệu test. Trong ngày 3, bạn đã học cách dùng lệnh propel:data-load, nhưng để test, chúng ta cần nạp lại dữ liệu mỗi khi chúng ta chạy. Lệnh propel:data-load sử dụng lớp sfPropelData để load dữ liệu:

$loader = new sfPropelData();$loader->loadData(sfConfig::get('sf_test_dir').'/fixtures');

Đối tượng sfConfig có thể dùng để lấy đường dẫn đầy đủ của một thư mục con của project. Có thể sử dụng nó để sửa lại cấu trúc thư mục mặc định.

Phương thức loadData() nhận tham số là một thư mục hoặc một file. Nó cũng có thể nhận một mảng các thư mục hoặc files.

Chúng ta đã tạo một vài dữ liệu trong thư mục data/fixtures/. Để test, chúng ta sẽ đặt các fixture trong thư mục test/fixtures/. Những fixture này sẽ được sử dụng cho Propel unit và functional tests.

Bây giờ, copy các file từ thư mục data/fixtures/ vào thư mục test/fixtures/.

Testing JobeetJob

Ta tạo một vài unit tests cho lớp JobeetJob model.

Tất cả các Propel unit tests đều bắt đầu bằng đoạn code giống nhau, do đó chúng ta tạo file Propel.php trong bootstrap/ của thư mục test:

// test/bootstrap/Propel.phpinclude(dirname(__FILE__).'/unit.php'); $configuration = ProjectConfiguration::getApplicationConfiguration('frontend', 'test', true); new sfDatabaseManager($configuration); $loader = new sfPropelData();$loader->loadData(sfConfig::get('sf_test_dir').'/fixtures');

Ý nghĩa của đoạn code này như sau:

Như đối với front controllers, chúng ta khởi tạo một đối tượng cấu hình cho môi trường test:

$configuration = ProjectConfiguration::getApplicationConfiguration('frontend', 'test', true);

Chúng ta tạo một database manager. Nó khởi tạo Propel connection bằng cách load file cấu hình databases.yml.

new sfDatabaseManager($configuration);

Chúng ta dùng sfPropelData để load dữ liệu test: $loader = new sfPropelData();

$loader->loadData(sfConfig::get('sf_test_dir').'/fixtures');

Propel chỉ kết nối với cơ sở dữ liệu chỉ khi một câu lệnh SQL được thực thi.

Bây giờ chúng ta đã có thể bắt đầu test cho lớp JobeetJob.

Đầu tiên, chúng ta cần tạo file JobeetJobTest.php trong thư mục test/unit/model:

// test/unit/model/JobeetJobTest.phpinclude(dirname(__FILE__).'/../../bootstrap/Propel.php'); $t = new lime_test(1, new lime_output_color());

Sau đó, thêm một test cho phương thức getCompanySlug():

$t->comment('->getCompanySlug()');$job = JobeetJobPeer::doSelectOne(new Criteria());$t->is($job->getCompanySlug(), Jobeet::slugify($job->getCompany()), '->getCompanySlug() return the slug for the company');

Viết test cho phương thức save() thì phức tạp hơn một chút:

$t->comment('->save()');$job = create_job();$job->save();$expiresAt = date('Y-m-d', time() + 86400 * sfConfig::get('app_active_days'));$t->is($job->getExpiresAt('Y-m-d'), $expiresAt, '->save() updates expires_at if not set'); $job = create_job(array('expires_at' => '2008-08-08'));$job->save();$t->is($job->getExpiresAt('Y-m-d'), '2008-08-08', '->save() does not update expires_at if set'); function create_job($defaults = array()){

static $category = null;  if (is_null($category)) { $category = JobeetCategoryPeer::doSelectOne(new Criteria()); }  $job = new JobeetJob(); $job->fromArray(array_merge(array( 'category_id' => $category->getId(), 'company' => 'Sensio Labs', 'position' => 'Senior Tester', 'location' => 'Paris, France', 'description' => 'Testing is fun', 'how_to_apply' => 'Send e-Mail', 'email' => '[email protected]', 'token' => rand(1111, 9999), 'is_activated' => true, ), $defaults), BasePeer::TYPE_FIELDNAME);  return $job;}

Mỗi khi bạn thêm một test, đừng quên update số test bạn dự định trong phương thức khởi tạo lime_test.

Test các lớp Propel khác

Bây giờ bạn có thể thêm test cho tất cả các lớp Propel khác. Bạn đã quen dần với cách viết unit tests, nó đã trở nên thật dễ dàng. Kiểm tra trong kho chứa ngày hôm nay để xem file fixture chúng tôi đã tạo, và các unit test liên quan (ở tag release_day_08).

Unit Tests Harness

Lệnh test:unit cũng có thể được dùng để chạy tất cả các unit test cho một project:

$ php symfony test:unit

Lệnh này sẽ hiển thị xem mỗi file test là passes hay fails:

Hẹn gặp lại ngày mai

Mặc dù test cho một ứng dụng là rất quan trọng, tôi biết rằng một vài bạn có thể tạm thời bỏ qua hướng dẫn ngày hôm nay. Thật mừng vì bạn không làm như vậy.

Sure, embracing symfony is about learning all the great features the framework provides, but it's also about its philosophy of development and the best practices it advocates. And testing is one of them. Sooner or later, unit tests will save the day for you. They give you a solid confidence about your code and the freedom to refactor it without fear. Unit tests are a safe guard that will alert you if you break something. The symfony framework itself has more than 9000 tests.

Tomorrow we will write some functional tests for the job and category modules. Until then, take some time to write more unit tests for the Jobeet model classes.

Tóm tắt

Hôm qua, chúng ta đã dùng thư viện lime có sẵn trong symfony để viết unit test cho các lớp của Jobeet.

Hôm nay, chúng ta sẽ viết functional tests cho các tính năng chúng ta đã xây dựng trong module job và category.

Functional Tests

Functional tests là công cụ tốt để test toàn bộ ứng dụng của bạn: từ request của trình duyệt đến response trả về bởi server. Nó test mọi tầng của một ứng dụng: routing, model, actions, và templates. Việc này cũng tương tự như những gì bạn thường làm: mỗi khi bạn tạo mới hay chỉnh sửa một action, bạn cần chạy ở trình duyệt và kiểm tra xem mọi thứ có hoạt động đúng không bằng cách click vào từng link và kiểm tra kết quả trả về. Nói cách khác, bạn thực thi một kịch bản tương ứng với use case bạn đã thực hiện.

Công việc này thật là nhàm chán và sẽ không kiểm soát được hết các lỗi. Mỗi khi bạn thay đổi một vài thứ trong mã nguồn, bạn phải thực thi qua tất cả các bước của kịch bản để chắc rằng chương trình hoạt động đúng. Điên mất :)). Functional tests trong symfony cung cấp một cách đơn giản để tự động mô phỏng lại các bước trong kịch bản. Giống unit tests, nó giúp code của bạn trở nên đúng đắn hơn.

Lớp sfBrowser

Trong symfony, functional tests được chạy thông qua một trình duyệt đặc biệt, implemented từ lớp sfBrowser. Nó hoạt động như một trình duyệt thực hiện ứng dụng của bạn và kết nối trực tiếp đến nó, không cần một web server. Nó cho phép bạn truy cập tất cả các symfony objects trước và sau mỗi request, giúp bạn xem xét và kiểm tra nó.

sfBrowser cung cấp các phương thức giúp nó hoạt động tương tự như một trình duyệt đơn giản:

Method Mô tảget() Gets a URLpost() Posts to a URL

call() Calls a URL (used for PUT and DELETE methods)

back() Goes back one page in the historyforward() Goes forward one page in the historyreload() Reloads the current pageclick() Clicks on a link or a buttonselect() selects a radiobutton or checkboxdeselect()deselects a radiobutton or checkboxrestart() Restarts the browser

Dưới đây là một vài ví dụ về các phương thức của sfBrowser:

$browser = new sfBrowser(); $browser-> get('/')-> click('Design')-> get('/category/programming?page=2')-> get('/category/programming', array('page' => 2))-> post('search', array('keywords' => 'php'));

sfBrowser cũng có một vài phương thức để cấu hình cho browser behavior:

Method DescriptionsetHttpHeader() Sets an HTTP headersetAuth() Sets the basic authentication credentialssetCookie() Set a cookieremoveCookie() Removes a cookieclearCookie() Clears all current cookiesfollowRedirect()Follows a redirect

Lớp sfTestFunctional

Chúng ta đã có một trình duyệt, nhưng chúng ta cần có cách để có thể xem xét được các symfony objects thể có thể thực hiện đưọc viêc test. Điều này có thể thực hiện đưọc với lime và một vài phuơng thức của sfBrowser như getResponse() và getRequest(), nhưng symfony cung cấp một cách tốt hơn.

Các phuơng thức test đưọc cung cấp bởi một lớp khác, sfTestFunctional nó dùng một sfBrowser instance trong phương thức khởi tạo. Lớp sfTestFunctional chuyển việc test cho các đối tưọng tester. Có nhiều tester đưọc xây dựng sẵn trong symfony, và bạn cũng có thể tự tạo ra chúng.

Như chúng ta đã thấy hôm qua, functional tests được chứa trong thư mục test/functional. Với Jobeet, tests nằm trong thư mục con test/functional/frontend ứng với application tương ứng. Thư mục này đã có 2 files: categoryActionsTest.php, và jobActionsTest.php đưọc tạo tự động khi chúng ta tạo các module tuơng ứng:

// test/functional/frontend/categoryActionsTest.phpinclude(dirname(__FILE__).'/../../bootstrap/functional.php'); $browser = new sfTestFunctional(new sfBrowser()); $browser-> get('/category/index')->  with('request')->begin()-> isParameter('module', 'category')-> isParameter('action', 'index')-> end()->  with('response')->begin()-> isStatusCode(200)-> checkElement('body', '!/This is a temporary page/')-> end();

Khi mới nhìn, bạn có thể không quen với cách viết này. Đó là bởi vì các phưong thức của sfBrowser và sfTestFunctional luôn trả về $this để enable một fluent interface. Nó cho phép bạn xâu chuỗi các phuơng thức cần gọi để dễ đọc hơn.

Tests đưọc chạy trong một khối tester. Một tester block context bắt đầu bởi with('TESTER NAME')->begin() và kết thúc bằng end():

$browser-> with('request')->begin()-> isParameter('module', 'category')-> isParameter('action', 'index')-> end();

Code tests yêu cầu tham số module là category và action là index.

Khi chỉ cần gọi một test method ở một tester, bạn không cần tạo một block: with('request')->isParameter('module', 'category').

Request Tester

Request tester cung cấp các phương thức tester để introspect và test đối tượng sfWebRequest:

Method Mô tảisParameter()Checks a request parameter valueisFormat() Checks the format of a requestisMethod() Checks the methodhasCookie() Checks whether the request has a cookie with the

given nameisCookie() Checks the value of a cookie

Response Tester

Lớp response tester cung cấp các phuơng thức tester đối với các đối tưọng sfWebResponse :

Method Mô tảcheckElement()Checks if a response CSS selector match some criteriaisHeader() Checks the value of a headerisStatusCode()Checks the response status codeisRedirected()Checks if the current response is a redirect

Chúng ta sẽ đề cập đển các lớp testers khác trong những ngày tiếp theo (cho forms, user, cache, ...).

Chạy Functional Tests

Như unit tests, chúng ta có thể thực thi functional tests bằng cách chạy trực tiếp file:

$ php test/functional/frontend/categoryActionsTest.php

Hay qua lệnh test:functional:

$ php symfony test:functional frontend categoryActions

Dữ liệu Test

Tương tự như Propel unit tests, chúng ta cần phải load dữ liệu test mõi khi chúng ta chạy functional test. Chúng ta có thể dùng lại mã nguồn đã viết hôm qua:

include(dirname(__FILE__).'/../../bootstrap/functional.php'); $browser = new JobeetTestFunctional(new sfBrowser());$loader = new sfPropelData();$loader->loadData(sfConfig::get('sf_test_dir').'/fixtures');

Load dữ liệu trong một functional test đơn giản hơn unit tests một chút do database đã được khởi tạo bởi bootstrapping script.

Như với unit tests, chúng ta không muốn copy&paste lại đoạn code này với mỗi file test, chúng ta tạo một functional trong lớp thừa kế từ lớp sfTestFunctional:

// lib/test/JobeetTestFunctional.class.phpclass JobeetTestFunctional extends sfTestFunctional{ public function loadData() { $loader = new sfPropelData(); $loader->loadData(sfConfig::get('sf_test_dir').'/fixtures'); return $this; }}

Viết Functional Tests

Viết functional tests chính là thực hiện các bước trong kịch bản ở một trình duyệt. Chúng ta đã có tất cả các kịch bản cần thiết đưọc mô tả trong ngày 2.

Đầu tiên, chúng ta test trang chủ bằng cách sửa lại file test jobActionsTest.php. Thay code bởi đoạn sau:

Không hiển thị các công việc hết hạn

// test/functional/frontend/jobActionsTest.phpinclude(dirname(__FILE__).'/../../bootstrap/functional.php'); $browser = new JobeetTestFunctional(new sfBrowser());$browser->loadData(); $browser->info('1 - The homepage')-> get('/')-> with('request')->begin()-> isParameter('module', 'job')-> isParameter('action', 'index')-> end()-> with('response')->begin()-> info(' 1.1 - Expired jobs are not listed')-> checkElement('.jobs td.position:contains("expired")', false)-> end();

Tưong tự như lime, một thông điệp có thể được chèn vào bằng cách gọi phuơng thức info() để giúp kết quả hiển thị dễ đọc hơn. Để kiểm tra các công việc hết hạn ở trang chủ có đưọc hiển thị không, chúng ta kiểm tra xem CSS selector .jobs td.position:contains("expired") có match trong nội dung HTML trả về không (nhớ rằng trong file fixture, chỉ các công việc hết hạn mới chứa cụm "expired" trong truờng position).

Phuơng thức checkElement() có thể hiểu đưọc hầu hết các valid CSS3 selectors.

Chỉ hiển thị n công việc cho mỗi category

Thêm đoạn code sau vào cuối file test:

// test/functional/frontend/jobActionsTest.php$max = sfConfig::get('app_max_jobs_on_homepage'); $browser->info('1 - The homepage')-> get('/')-> info(sprintf(' 1.2 - Only %s jobs are listed for a category', $max))-> with('response')-> checkElement('.category_programming tr', $max);

Phương thức checkElement() cũng có thể kiểm tra xem một CSS selector có đưọc match n lần.

Một category có link đến trang category chỉ khi nó có nhiều công việc

$browser->info('1 - The homepage')-> get('/')-> info(' 1.3 - A category has a link to the category page only if too many jobs')-> with('response')->begin()-> checkElement('.category_design .more_jobs', false)-> checkElement('.category_programming .more_jobs')-> end();

Ở đây, chúng ta kiểm tra rằng không có link "more jobs" ở category design (.category_design .more_jobs không tồn tại), và có một link "more jobs" ở category programming (.category_programming .more_jobs tồn tại).

Jobs đưọc sắp xếp theo ngày tháng

// most recent job in the programming category$criteria = new Criteria();$criteria->add(JobeetCategoryPeer::SLUG, 'programming');$category = JobeetCategoryPeer::doSelectOne($criteria); $criteria = new Criteria();$criteria->add(JobeetJobPeer::EXPIRES_AT, time(), Criteria::GREATER_THAN);$criteria->add(JobeetJobPeer::CATEGORY_ID, $category->getId());$criteria->addDescendingOrderByColumn(JobeetJobPeer::CREATED_AT); $job = JobeetJobPeer::doSelectOne($criteria); $browser->info('1 - The homepage')-> get('/')-> info(' 1.4 - Jobs are sorted by date')-> with('response')->begin()-> checkElement('.category_programming tr:last:contains("102")')-> checkElement(sprintf('.category_programming tr:first a[href*="/%d/"]', $job->getId()))-> end();

Để kiểm tra xem các công việc có đưọc sắp xếp theo ngày tháng hay không, chúng ta kiểm tra công việc cuối trong danh sách ở trang chủ có chứa cụm 102 ở trưòng company không. Test công việc đầu tiên trong danh sách programming đòi hỏi một chút khéo léo, do cả 2 công việc đầu tiên đều có thông tin hiển thị như nhau: position, company, và location. Vì thế chúng ta cần kiểm tra xem URL có chứa primary key mà chúng ta mong đợi không. Do primary key có thể thay đổi mỗi lần chạy, nên chúng ta cần lấy Propel object đầu tiên trong database.

Mặc dù test hoạt động đúng, nhưng chúng ta cần refactor lại một chút, giúp cho công việc đầu tiên của category có thể dùng lại đưọc trong test của chúng ta. Chúng ta sẽ không chuyển code tới tầng Model để code test được rành mạch. Thay vào đó, chúng ta sẽ chuyển code tới lớp JobeetTestFunctional chúng ta sẽ tạo ngay sau đây. Lớp này thực hiện như một Domain Specific functional tester class cho Jobeet:

// lib/test/JobeetTestFunctional.class.phpclass JobeetTestFunctional extends sfTestFunctional{ public function getMostRecentProgrammingJob() { // most recent job in the programming category $criteria = new Criteria(); $criteria->add(JobeetCategoryPeer::SLUG, 'programming'); $category = JobeetCategoryPeer::doSelectOne($criteria);  $criteria = new Criteria(); $criteria->add(JobeetJobPeer::EXPIRES_AT, time(), Criteria::GREATER_THAN); $criteria->addDescendingOrderByColumn(JobeetJobPeer::CREATED_AT);  return JobeetJobPeer::doSelectOne($criteria); }  // ...}

Mỗi công việc ở trang chủ đều có thể click được

$browser->info('2 - The job page')-> get('/')->  info(' 2.1 - Each job on the homepage is clickable')-> click('Web Developer', array('position' => 1))-> with('request')->begin()-> isParameter('module', 'job')-> isParameter('action', 'show')-> isParameter('company_slug', 'sensio-labs')-> isParameter('location_slug', 'paris-france')-> isParameter('position_slug', 'web-developer')-> isParameter('id', $browser->getMostRecentProgrammingJob()->getId())-> end();

Để test các job link ở trang chủ, chúng tôi giả lập một click vào đoạn text "Web Developer". Có thể có nhiều đoạn này trên trang web, chúng ta click vào cái đầu tiên (array('position' => 1)).

Mỗi request parameter sau đó đưọc test để chắc rằng routing đã lấy đúng công việc.

Học qua ví dụ

Trong mục này, chúng tôi sẽ cung cấp tất cả code cần thiết để test trang job và category. Hãy đọc kĩ những đoạn code này và bạn sẽ học được nhiều thủ thuật mới:

// lib/test/JobeetTestFunctional.class.phpclass JobeetTestFunctional extends sfTestFunctional

{ public function loadData() { $loader = new sfPropelData(); $loader->loadData(sfConfig::get('sf_test_dir').'/fixtures');  return $this; }  public function getMostRecentProgrammingJob() { // most recent job in the programming category $criteria = new Criteria(); $criteria->add(JobeetCategoryPeer::SLUG, 'programming'); $category = JobeetCategoryPeer::doSelectOne($criteria);  $criteria = new Criteria(); $criteria->add(JobeetJobPeer::EXPIRES_AT, time(), Criteria::GREATER_THAN); $criteria->addDescendingOrderByColumn(JobeetJobPeer::CREATED_AT);  return JobeetJobPeer::doSelectOne($criteria); }  public function getExpiredJob() { // expired job $criteria = new Criteria(); $criteria->add(JobeetJobPeer::EXPIRES_AT, time(), Criteria::LESS_THAN);  return JobeetJobPeer::doSelectOne($criteria); }} // test/functional/frontend/jobActionsTest.phpinclude(dirname(__FILE__).'/../../bootstrap/functional.php'); $browser = new JobeetTestFunctional(new sfBrowser());$browser->loadData(); $browser->info('1 - The homepage')-> get('/')-> with('request')->begin()-> isParameter('module', 'job')-> isParameter('action', 'index')-> end()-> with('response')->begin()-> info(' 1.1 - Expired jobs are not listed')-> checkElement('.jobs td.position:contains("expired")', false)->  info(sprintf(' 1.2 - Only %s jobs are listed for a category', sfConfig::get('app_max_jobs_on_homepage')))-> checkElement('.category_programming tr', sfConfig::get('app_max_jobs_on_homepage'))-> 

info(' 1.3 - A category has a link to the category page only if too many jobs')-> checkElement('.category_design .more_jobs', false)-> checkElement('.category_programming .more_jobs')->  info(' 1.4 - Jobs are sorted by date')-> checkElement(sprintf('.category_programming tr:first a[href*="/%d/"]', $browser->getMostRecentProgrammingJob()->getId()))-> checkElement('.category_programming tr:last:contains("102")')-> end(); $browser->info('2 - The job page')-> info(' 2.1 - Each job on the homepage is clickable and give detailed information')-> click('Web Developer', array('position' => 1))-> with('request')->begin()-> isParameter('module', 'job')-> isParameter('action', 'show')-> isParameter('company_slug', 'sensio-labs')-> isParameter('location_slug', 'paris-france')-> isParameter('position_slug', 'web-developer')-> isParameter('id', $browser->getMostRecentProgrammingJob()->getId())-> end()->  info(' 2.2 - A non-existent job forwards the user to a 404')-> get('/job/foo-inc/milano-italy/0/painter')-> with('response')->isStatusCode(404)->  info(' 2.3 - An expired job page forwards the user to a 404')-> get(sprintf('/job/sensio-labs/paris-france/%d/web-developer', $browser->getExpiredJob()->getId()))-> with('response')->isStatusCode(404); // test/functional/frontend/categoryActionsTest.phpinclude(dirname(__FILE__).'/../../bootstrap/functional.php'); $browser = new JobeetTestFunctional(new sfBrowser());$browser->loadData(); $browser->info('1 - The category page')-> info(' 1.1 - Categories on homepage are clickable')-> get('/')-> click('Programming')-> with('request')->begin()-> isParameter('module', 'category')-> isParameter('action', 'show')-> isParameter('slug', 'programming')-> end()->  info(sprintf(' 1.2 - Categories with more than %s jobs also have a "more" link', sfConfig::get('app_max_jobs_on_homepage')))-> get('/')-> click('22')-> with('request')->begin()->

isParameter('module', 'category')-> isParameter('action', 'show')-> isParameter('slug', 'programming')-> end()->  info(sprintf(' 1.3 - Only %s jobs are listed', sfConfig::get('app_max_jobs_on_category')))-> with('response')->checkElement('.jobs tr', sfConfig::get('app_max_jobs_on_category'))->  info(' 1.4 - The job listed is paginated')-> with('response')->begin()-> checkElement('.pagination_desc', '/32 jobs/')-> checkElement('.pagination_desc', '#page 1/2#')-> end()->  click('2')-> with('request')->begin()-> isParameter('page', 2)-> end()-> with('response')->checkElement('.pagination_desc', '#page 2/2#');

Debugging Functional Tests

Đôi khi một functional test fails. Do symfony giả lập một trình duyệt không có giao diện đồ họa, nên thật khó để tìm ra nguyên nhân. May mắn thay, symfony cung cấp phuơng thức debug() để hiển thị response header và content:

$browser->with('response')->debug();

Có thể thêm phuơng thức debug() vào bất kì đâu trong một response tester block và tạm dừng thực thi script.

Functional Tests Harness

Lệnh test:functional có thể dùng để chạy tất cả các functional tests cho một application:

$ php symfony test:functional frontend

Lệnh này hiển thị kết quả của mỗi test trên một dòng:

Tests Harness

Như bạn mong đợi, cũng có lệnh để thực thi tất cả các test cho một project (unit và functional):

$ php symfony test:all

Hẹn gặp lại ngày mai

Chúng ta đã tìm hiểu về các công cụ test trong symfony. Bạn không còn lý do gì để không test cho ứng dụng của bạn! Với lime framework và functional test framework, symfony cung cấp những công cụ mạnh mẽ để giúp bạn viết test ít tốn công sức nhất.

Chúng ta đã hiểu về functional tests. Từ nay, mỗi khi chúng ta tạo một tính năng mới, chúng ta cần viết tests để học nhiều hơn về test framework.

Functional test framework không thể thay thế đưọc những công cụ như "Selenium". Selenium chạy trực tiếp trong trình duyệt và tự động test trên nhiều platform và browser khác nhau, và nó cũng có thể test JavaScript trong ứng dụng của bạn.

Hãy quay lại vào ngày mai, và chúng ta sẽ nói về một tính năng thú vị khác trong symfony: form framework.

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().

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)).

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.phpclass 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.phppublic 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.phpclass 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)

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

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" />

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.phpclass 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.

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.

Customizing the Look and Feel of a Form

Mặc định, <?php echo $form ?> render các form widget theo dạng table.

Trong nhiều trường hợp, bạn cần thay đổi giao diện của forms. Form object cung cấp nhiều phương thức hữu ích cho việc này:

Method Mô tảrender() Renders the form (equivalent to the output of

echo $form)renderHiddenFields()Renders the hidden fieldshasErrors() Returns true if the form has some errorshasGlobalErrors() Returns true if the form has global errorsgetGlobalErrors() Returns an array of global errorsrenderGlobalErrors()Renders the global errors

Form cũng giống như một mảng các field. Bạn có thể truy cập field company với $form['company']. Đối tượng trả về cung cấp các phương thức đế render từng thành phần của field:

Method Mô tảrenderRow() Renders the field rowrender() Renders the field widgetrenderLabel()Renders the field labelrenderError()Renders the field error messages if anyrenderHelp() Renders the field help message

Câu lệnh echo $form tương đương với:

<?php foreach ($form as $widget): ?> <?php echo $widget->renderRow() ?><?php endforeach(); ?>

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.phppublic 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.

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.phppublic function executeNew(sfWebRequest $request){ $job = new JobeetJob(); $job->setType('full-time');  $this->form = new JobeetJobForm($job);}

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.phppublic 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.phpclass 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.ymljob: 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>

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.phppublic 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.ymljob: 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.phppublic 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.phppublic 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.phpstatic 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.

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.

Tóm tắt

Hôm qua chúng ta đã tạo các form với symfony. Bây giờ, người dùng đã có thể gửi một công việc mới lên Jobeet, nhưng chúng ta đã không đủ thời gian để thực hiện test.

Chúng ta sẽ thực hiện việc này vào hôm nay. Trong quá trình thực hiện, chúng ta sẽ học nhiều hơn về form framework.

Sử dụng Form Framework không cần symfony

symfony framework được ghép từ các thành phần khác nhau. Điều đó có nghĩa là bạn có thể sử dụng từng thành phần riêng biệt mà không cần sử dụng toàn bộ framework. Bạn có thể dùng form framework độc lập với symfony. Bạn có thể dùng nó trong bất kì ứng dụng PHP nào bằng cách copy các thư mục lib/form/, lib/widgets/, và lib/validators/.

Một thành phần khác có thể dùng lại là routing framework. Copy thư mục lib/routing/ vào project bất kì, và bạn có thể dùng nó để tạo ra các URL theo ý muốn.

Các thành phần trong symfony platform:

Submit một Form

Mở file jobActionsTest và thêm functional tests cho quá trình tạo công việc và validation.

Thêm đoạn code sau vào cuối file:

// test/functional/frontend/jobActionsTest.php$browser->info('3 - Post a Job page')-> info(' 3.1 - Submit a Job')->  get('/job/new')-> with('request')->begin()-> isParameter('module', 'job')-> isParameter('action', 'new')-> end();

Chúng ta đã dùng phương thức click() để giả lập việc click vào một link. Phương thức click() cũng có thể dùng để submit một form. Với một form, bạn có thể cung cấp các

giá trị cho mỗi field ở tham số thứ 2 của phương thức. Giống như một trình duyệt thật, trình duyệt sẽ bổ sung các giá trị mặc định cho form khi cần thiết.

Nhưng để cung cấp giá trị cho các field, chúng ta cần biết tên của chúng. Nếu bạn mở source code hay sử dụng Firefox Web Developer Toolbar "Forms > Display Form Details", bạn sẽ thấy rằng tên của field company là jobeet_job[company].

Khi PHP gặp một input field có tên là jobeet_job[company], nó sẽ tự động chuyển thành một mảng jobeet_job.

Để dễ nhìn, chúng ta sẽ chuyển thành job[%s] bằng cách thêm đoạn code sau vào cuối phương thức configure() của JobeetJobForm:

// lib/form/JobeetJobForm.class.php$this->widgetSchema->setNameFormat('job[%s]');

Sau khi thay đổi, tên của field company sẽ là job[company]. Bây giờ chúng ta thực hiện việc click vào nút "Preview your job" và cung cấp giá trị cho form:

// test/functional/frontend/jobActionsTest.php$browser->info('3 - Post a Job page')-> info(' 3.1 - Submit a Job')->  get('/job/new')-> with('request')->begin()-> isParameter('module', 'job')-> isParameter('action', 'new')-> end()->  click('Preview your job', array('job' => array( 'company' => 'Sensio Labs', 'url' => 'http://www.sensio.com/', 'logo' => sfConfig::get('sf_upload_dir').'/jobs/sensio-labs.gif', 'position' => 'Developer', 'location' => 'Atlanta, USA', 'description' => 'You will work with symfony to develop websites for our customers.', 'how_to_apply' => 'Send me an email', 'email' => '[email protected]', 'is_public' => false, )));

Form phải được submit tới action create:

with('request')->begin()-> isParameter('module', 'job')-> isParameter('action', 'create')->end()->

Trình duyệt cũng giả lập việc upload file bằng cách cung cấp đường dẫn tuyệt đối tới file cần upload.

Form Tester

Form chúng ta đã submit cần được valid. Chúng ta có thể kiểm tra điều này bằng cách sử dụng form tester:

with('form')->begin()-> hasErrors(false)->end()->

Form tester có vài phương thức để kiểm tra trạng thái hiện tại của form, như errors...

Nếu bạn có lỗi trong test, và test không pass, bạn có thể sử dụng lệnh with('response')->debug() đã học trong ngày 9 để debug. Nhưng bạn sẽ phải mò trong đống HTML sinh ra để kiểm tra thông báo lỗi. Điều đó không được tiện lợi cho lắm. Vì thế, form tester cũng cung cấp phương thức debug() trả về trạng thái của form và tất cả thông báo lỗi liên quan đến nó:

with('form')->debug()

Redirection Test

Khi form được valid, công việc được tạo và user chuyển sang trang show:

isRedirected()->followRedirect()-> with('request')->begin()-> isParameter('module', 'job')-> isParameter('action', 'show')->end()->

isRedirected() kiểm tra xem trang đã được redirect hay chưa và phương thức followRedirect() chuyển đến trang redirect.

Propel Tester

Cuối cùng, chúng ta muốn test rằng công việc đã được tạo trong database và kiểm tra rằng cột is_public có giá trị là false khi user chưa published nó.

Điều này có thể thực hiện dễ dàng nhờ một tester khác, Propel tester. Mặc định, Propel tester không được đăng kí, chúng ta cần thêm nó:

$browser->setTester('propel', 'sfTesterPropel');

Propel tester cung cấp phương thức check() để kiểm tra xem một hay nhiều object trong database có match với các tham số được cung cấp.

with('propel')->begin()-> check('JobeetJob', array( 'location' => 'Atlanta, USA', 'is_activated' => false, 'is_public' => false, ))->end()

The criteria can be an array of values like above, or a Criteria instance for more complex queries. You can test the existence of objects matching the criteria with a Boolean as the third argument (the default is true), or the number of matching objects by passing an integer.

Test các lỗi

Job form đã tạo công việc đúng như mong đợi khi chúng ta submit các giá trị hợp lệ. Hãy thêm một test để kiểm tra khi chúng ta cung cấp dữ liệu không hợp lệ:

$browser-> info(' 3.2 - Submit a Job with invalid values')->  get('/job/new')-> click('Preview your job', array('job' => array( 'company' => 'Sensio Labs', 'position' => 'Developer', 'location' => 'Atlanta, USA', 'email' => 'not.an.email', )))->  with('form')->begin()-> hasErrors(3)-> isError('description', 'required')-> isError('how_to_apply', 'required')-> isError('email', 'invalid')-> end();

Phương thức hasErrors() có thể kiểm tra số lỗi xảy ra. Phương thức isError() kiểm tra error code với từng field.

Trong khi test chúng ta viết các giá trị không hợp lệ, chứ không test lại toàn bộ form. Chúng ta chỉ thêm các test cho các trường cụ thể.

Bạn cũng có thể test mã HTML sinh ra để kiểm tra rằng nó chứa thông báo lỗi, nhưng việc này là không cần thiết do chúng ta không chỉnh sửa form layout.

Bây giờ, chúng ta cần test thanh admin bar ở trang job preview. Khi một công việc chưa được activated, bạn có thể edit, delete, hoặc publish công việc. Để test 3 link này, trước tiên chúng ta cần tạo một công việc. Chúng ta có thể copy & paste code đã có. Để tránh việc lặp lại này, hãy thêm một phương thức tạo công việc trong lớp JobeetTestFunctional:

// lib/test/JobeetTestFunctional.class.phpclass JobeetTestFunctional extends sfTestFunctional{ public function createJob($values = array()) { return $this-> get('/job/new')-> click('Preview your job', array('job' => array_merge(array( 'company' => 'Sensio Labs', 'url' => 'http://www.sensio.com/', 'position' => 'Developer', 'location' => 'Atlanta, USA', 'description' => 'You will work with symfony to develop websites for our customers.', 'how_to_apply' => 'Send me an email', 'email' => '[email protected]', 'is_public' => false, 'type' => 'full-time', ), $values)))-> followRedirect() ; }  // ...}

Bắt buộc HTTP Method với một link

Test link "Publish" trở nên đơn giản:

$browser->info(' 3.3 - On the preview page, you can publish the job')-> createJob(array('position' => 'FOO1'))-> click('Publish', array(), array('method' => 'put', '_with_csrf' => true))->  with('propel')->begin()-> check('JobeetJob', array( 'position' => 'FOO1', 'is_activated' => true, ))-> end();

Nếu bạn nhớ lại trong ngày 10, link "Publish" đã được cấu hình để gọi với HTTP PUT method. Do trình duyệt không hiểu PUT requests, helper link_to() chuyển link thành một form với một vài JavaScript. Do test browser không thực thi JavaScript, chúng ta cần

bắt buộc method là PUT bằng cách cung cấp nó như là tham số thứ 3 của phương thức click(). Thêm vào đó, helper link_to() cũng nhúng một CSRF token do chúng ta đã enable CSRF protection từ ngày 1; _with_csrf option mô phỏng token này.

Test link "Delete" hoàn toàn tương tự:

$browser->info(' 3.4 - On the preview page, you can delete the job')-> createJob(array('position' => 'FOO2'))-> click('Delete', array(), array('method' => 'delete', '_with_csrf' => true))->  with('propel')->begin()-> check('JobeetJob', array( 'position' => 'FOO2', ), false)-> end();

Tests SafeGuard

Khi một công việc được publish, bạn không thể sửa nó nữa. Link "Edit" sẽ không xuất hiện ở trang preview, ta thêm một vài test cho yêu cầu này.

Đầu tiên, thêm một tham số khác cho phương thức createJob() để tự động publication công việc, và tạo phương thức getJobByPosition() để trả về công việc theo giá trị position cung cấp:

// lib/test/JobeetTestFunctional.class.phpclass JobeetTestFunctional extends sfTestFunctional{ public function createJob($values = array(), $publish = false) { $this-> get('/job/new')-> click('Preview your job', array('job' => array_merge(array( 'company' => 'Sensio Labs', 'url' => 'http://www.sensio.com/', 'position' => 'Developer', 'location' => 'Atlanta, USA', 'description' => 'You will work with symfony to develop websites for our customers.', 'how_to_apply' => 'Send me an email', 'email' => '[email protected]', 'is_public' => false, 'type' => 'full-time', ), $values)))-> followRedirect() ;  if ($publish) {

$this->click('Publish', array(), array('method' => 'put', '_with_csrf' => true)); }  return $this; }  public function getJobByPosition($position) { $criteria = new Criteria(); $criteria->add(JobeetJobPeer::POSITION, $position);  return JobeetJobPeer::doSelectOne($criteria); } // ...}

Nếu một công việc được publish, trang edit phải trả về mã trạng thái 404:

$browser->info(' 3.5 - When a job is published, it cannot be edited anymore')-> createJob(array('position' => 'FOO3'), true)-> get(sprintf('/job/%s/edit', $browser->getJobByPosition('FOO3')->getToken()))->  with('response')->begin()-> isStatusCode(404)-> end();

Nhưng nếu bạn chạy test, bạn sẽ không thu được kết quả như mong đợi, do chúng ta đã quên thực hiện vấn đề này ngày hôm qua. Viết test cũng là một cách tốt để tìm ra các bug, và bạn cần nghĩ tới tất cả các trường hợp có thể.

Sửa lỗi này khá đơn giản, chúng ta cần forward trang edit tới trang 404 khi một công việc đã activated:

// apps/frontend/modules/job/actions/actions.class.phppublic function executeEdit(sfWebRequest $request){ $jobeet_job = $this->getRoute()->getObject(); $this->form = new JobeetJobForm($jobeet_job);  $this->forward404If($jobeet_job->getIsActivated());}

Cách sửa thật đơn giản, nhưng bạn có chắc rằng mọi thứ sẽ làm việc như mong đợi? Bạn có thể mở trình duyệt và bắt đầu test mọi cách có thể để truy cập trang edit. Nhưng có một cách đơn giản hơn: chạy test suite của bạn;

Chuyển đến tương lai trong một Test

Khi một công việc hết hạn sau ít nhất 5 ngày, user có thể gia hạn cho công việc thêm 30 ngày nữa kể từ ngày hiện tại.

Test yêu cầu này từ trình duyệt không dễ dàng do ngày hết hạn được tự động tạo là sau 30 ngày từ ngày tạo công việc. Vì thế, khi chuyển đến trang công việc, link để extend cho công việc chưa tồn tại. Tất nhiên, bạn có thể sửa lại ngày hết hạn trong database, hoặc chỉnh lại template để hiển thị link này, nhưng việc này thật chán ngắt và dễ sinh ra lỗi. Bạn cũng có thể đoán được, viết một vài test sẽ giúp chúng ta thực hiện dễ dàng.

Trước tiên, chúng ta cần thêm một route mới cho extend:

# apps/frontend/config/routing.ymljob: class: sfPropelRouteCollection options: model: JobeetJob column: token object_actions: { publish: PUT, extend: PUT } requirements: token: \w+

Sau đó, sửa lại link "Extend" trong partial _admin:

<!-- apps/frontend/modules/job/templates/_admin.php --><?php if ($job->expiresSoon()): ?> - <?php echo link_to('Extend', 'job_extend', $job, array('method' => 'put')) ?> for another <?php echo sfConfig::get('app_active_days') ?> days<?php endif; ?>

Sau đó, tạo action extend:

// apps/frontend/modules/job/actions/actions.class.phppublic function executeExtend(sfWebRequest $request){ $request->checkCSRFProtection();  $job = $this->getRoute()->getObject(); $this->forward404Unless($job->extend());  $this->getUser()->setFlash('notice', sprintf('Your job validity has been extend until %s.', $job->getExpiresAt('m/d/Y')));  $this->redirect($this->generateUrl('job_show_user', $job));}

Phương thức extend() của JobeetJob trả về true nếu công việc đã được gia hạn và false trong trường hợp ngược lại:

// lib/model/JobeetJob.phpclass JobeetJob extends BaseJobeetJob{

public function extend() { if (!$this->expiresSoon()) { return false; }  $this->setExpiresAt(time() + 86400 * sfConfig::get('app_active_days'));  return $this->save(); }  // ...}

Cuối cùng, thêm một kịch bản test:

$browser->info(' 3.6 - A job validity cannot be extended before the job expires soon')-> createJob(array('position' => 'FOO4'), true)-> call(sprintf('/job/%s/extend', $browser->getJobByPosition('FOO4')->getToken()), 'put', array('_with_csrf' => true))-> with('response')->begin()-> isStatusCode(404)-> end(); $browser->info(' 3.7 - A job validity can be extended when the job expires soon')-> createJob(array('position' => 'FOO5'), true); $job = $browser->getJobByPosition('FOO5');$job->setExpiresAt(time());$job->save(); $browser-> call(sprintf('/job/%s/extend', $job->getToken()), 'put', array('_with_csrf' => true))-> with('response')->isRedirected(); $job->reload();$browser->test()->is( $job->getExpiresAt('y/m/d'), date('y/m/d', time() + 86400 * sfConfig::get('app_active_days')));

Kịch bản test này có một vài thứ mới:

Phương thức call() nhận một URL với một method khác GET hay POST Sau khi công việc đã được update bởi action, chúng ta cần nạp lại local object với

$job->reload()

Cuối cùng, chúng ta dùng đối tượng lime nhúng trực tiếp để test ngày hết hạn mới.

Forms Security

Form Serialization Magic!

Propel forms rất dễ sử dụng do chúng đã tự động thực hiện nhiều công việc. Ví dụ, lưu một form vào database đơn giản là gọi $form->save(). Nó hoạt động như thế nào?

Bình thường, phương thức save() tiến hành qua các bước sau:

Bắt đầu một transaction (because nested Propel forms are all saved in one fell swoop)

Thực hiện việc submit giá trị (bằng cách gọi phương thức updateCOLUMNColumn() nếu chúng tồn tại)

Gọi Propel object fromArray() method để update giá trị các cột Lưu object vào database Commit transaction

Tính năng bảo mật có sẵn

Phương thức fromArray() nhận một mảng các giá trị và updates giá trị các cột tương ứng. Vấn đề bảo mật ở đây là gì? Điều gì xảy ra nếu ai đó thử submit một giá trị cho một cột mà anh ta không được phép? Ví dụ cột token chẳng hạn?

Hãy viết một test để mô phỏng việc submit một công việc với field token:

// test/functional/frontend/jobActionsTest.php$browser-> get('/job/new')-> click('Preview your job', array('job' => array( 'token' => 'fake_token', )))->  with('form')->begin()-> hasErrors(8)-> hasGlobalError('extra_fields')-> end();

Khi submit form, bạn phải có một extra_fields global error. Đó là bởi vì mặc định, form không cho phép các field mở rộng để chứa các giá trị submit. Đó cũng là lí do tại sao tất cả các form field phải có một validator.

Bạn cũng có thể submit thêm các field từ trình duyệt bằng các sử dụng các công cụ như Firefox Web Developer Toolbar.

Bạn có thể bypass giới hạn bảo mật này bằng cách setting allow_extra_fields thành true:

class MyForm extends sfForm{ public function configure() { // ...  $this->validatorSchema->setOption('allow_extra_fields', true); }}

Test bây giờ phải pass nhưng giá trị token được filtered out of the values. Vì thế bạn vẫn không thể bypass giới hạn bảo mật này. Nếu bạn vẫn muốn lưu giá trị này, set filter_extra_fields thành false:

$this->validatorSchema->setOption('filter_extra_fields', false);

Test viết trong mục này chỉ mang tính chất giới thiệu. Bạn có thể xoá nó khỏi Jobeet project do ta không cần test các tính năng của symfony.

XSS và CSRF Protection

Trong ngày 1, chúng ta đã tạo application frontend với lệnh sau:

$ php symfony generate:app --escaping-strategy=on --csrf-secret=Unique$ecret frontend

Option --escaping-strategy cho phép chống lại tấn công XSS. Điều đó có nghĩa là tất cả các giá trị dùng trong templates đều được escaped. Nếu bạn thử submit một công việc với mô tả chứa một vài tag HTML, bạn sẽ thấy rằng khi symfony render trang job, các HTML tag trong phần mô tả không được thực thi, mà hiển thị như plain text.

Option --csrf-secret giúp bảo vệ ứng dụng trước tấn công CSRF. Khi bạn cung cấp lựa chọn này, tất cả các form đều nhúng thêm một field ẩn _csrf_token.

Escaping strategy và CSRF secret có thể thay đổi bằng cách chỉnh sửa trong file cấu hình apps/frontend/config/settings.yml. Tương tự như file databases.yml, cấu hình cũng phụ thuộc môi trường:

all: .settings: # Form security secret (CSRF protection) csrf_secret: Unique$ecret  # Output escaping settings escaping_strategy: on escaping_method: ESC_SPECIALCHARS

Maintenance Tasks

Mặc dù symfony là một web framework, nó sử dụng rất nhiều công cụ dòng lệnh. Bạn đã dùng nó để tạo cấu trúc thư mục mặc định của project và application, và rất nhiều loại file khác nhau. Thêm một lệnh mới là một việc đơn giản do các lệnh sử dụng trong symfony command line được đóng gói trong một framework.

Khi một user tạo một công việc, anh ta phải activate nó để nó có thể xuất hiện. Nhưng nếu không, database sẽ phình to với rất nhiều công việc cũ. Hãy tạo thêm một task để xóa các công việc cũ khỏi database. Task này sẽ được chạy thường xuyên trong một cron job.

// lib/task/JobeetCleanupTask.class.phpclass JobeetCleanupTask extends sfBaseTask{ protected function configure() { $this->addOptions(array( new sfCommandOption('env', null, sfCommandOption::PARAMETER_REQUIRED, 'The environement', 'prod'), new sfCommandOption('days', null, sfCommandOption::PARAMETER_REQUIRED, '', 90), ));  $this->namespace = 'jobeet'; $this->name = 'cleanup'; $this->briefDescription = 'Cleanup Jobeet database';  $this->detailedDescription = <<<EOFThe [jobeet:cleanup|INFO] task cleans up the Jobeet database:  [./symfony jobeet:cleanup --env=prod --days=90|INFO]EOF; }  protected function execute($arguments = array(), $options = array()) { $databaseManager = new sfDatabaseManager($this->configuration);  $nb = JobeetJobPeer::cleanup($options['days']); $this->logSection('propel', sprintf('Removed %d stale jobs', $nb)); }}

Cấu hình cho task được thực hiện trong phương thức configure(). Mỗi task phải có một tên duy nhất (namespace:name), và có thể có các tham số (argument) và lựa chọn (option).

Mở thư mục lib/task/ để xem các task có sẵn trong symfony.

Lệnh jobeet:cleanup có 2 lựa chọn: --env và --days với một vài giá trị mặc định.

Chạy lệnh này tương tự như chạy các lệnh khác trong symfony:

$ php symfony jobeet:cleanup --days=10 --env=dev

Để code trở nên sáng sủa ta chuyển phương thức cleanup vào model:

// lib/model/JobeetJobPeer.phpstatic public function cleanup($days){ $criteria = new Criteria(); $criteria->add(self::IS_ACTIVATED, false); $criteria->add(self::CREATED_AT, time() - 86400 * $days, Criteria::LESS_THAN);  return self::doDelete($criteria);}

The doDelete() method removes database records matching the given Criteria object. It can also takes an array of primary keys.

The symfony tasks behave nicely with their environment as they return a value according to the success of the task. You can force a return value by returning an integer explicitly at the end of the task.

Hẹn gặp lại ngày mai

Test là một phần quan trọng trong symfony. Hôm nay, chúng ta đã học thêm về cách sử dụng các công cụ của symfony khiến cho việc phát triển ứng dụng trở nên nhanh chóng, dễ dàng, và an toàn hơn.

Symfony form framework cung cấp nhiều widget và validator: nó giúp bạn dễ dàng test form để đảm bảo rằng form của bạn hoàn toàn bảo mật.

Chuyến hành trình khám phá những tính năng tuyệt vời của symfony vẫn chưa kết thúc. Ngày mai, chúng ta sẽ tạo backend application cho Jobeet. Tạo backend interface là yêu cầu bắt buộc với hầu hết các dự án web, và Jobeet cũng không là ngoại lệ. Nhưng chúng ta có thể phát triển toàn bộ chúng trong một giờ? Với symfony admin generator framework, việc đó trở nên đơn giản. Hãy đón xem vào ngày mai :)).

Tóm tắt

Với những việc chúng ta đã làm ngày hôm qua, frontend application đã đầy đủ tính năng cho việc gửi công việc mới và xem danh sách công việc. Bây giờ chúng ta sẽ nói về backend application.

Hôm nay, nhờ có chức năng admin generator của symfony, chúng ta sẽ phát triển toàn bộ backend interface cho Jobeet chỉ trong 1 giờ.

Tạo Backend

Việc đầu tiên là phải tạo backend application. Có thể bạn đã biết cách thực hiện việc này thông qua lệnh generate:app:

$ php symfony generate:app --escaping-strategy=on --csrf-secret=Unique$ecret1 backend

Mặc dù backend application chỉ được dùng bởi administrators, nhưng chúng ta vẫn enable tất cả các tính năng bảo mật có sẵn của symfony.

Backend application bây giờ đã có thể truy cập ở http://jobeet.localhost/backend.php đối với môi trường prod, và http://jobeet.localhost/backend_dev.php với môi trường dev.

Khi bạn tạo frontend application, production front controller có tên là index.php. Do bạn chỉ có thể có một file index.php trong thư mục, symfony sẽ dùng file index.php cho production front controller đầu tiên còn các production front controller khác thì có tên như tên application.

Nếu bạn thử reload data fixtures với lệnh propel:data-load , nó sẽ không làm việc. Đó là bởi vì phương thức JobeetJob::save() cần truy cập file cấu hình app.yml từ frontend application. Bây giờ chúng ta có 2 application, symfony sẽ sử dụng file đầu tiên nó tìm thấy, đó là file nằm trong backend.

Nhưng như đã nói trong ngày 8, các setting có thể được cấu hình ở các mức khác nhau. Bằng cách chuyển nội dung từ file apps/frontend/config/app.yml sang file config/app.yml, những thiết lập này có thể được chia sẻ giữa các application khác nhau và vấn đề được giải quyết. Chúng ta sẽ dùng các class model rất nhiều trong admin generator, vì thế chúng ta cần các biến thiết lập trong file app.yml cho backend application.

Lệnh propel:data-load cũng có option --application. Vì thế nếu bạn cần một vài cấu hình cụ thể từ một application, bạn có thể dùng option này:

$ php symfony propel:data-load --application=frontend

Module Backend

Với frontend application, lệnh propel:generate-module được dùng để tạo module với các thao tác cơ bản CRUD dựa trên model class. Với backend, lệnh propel:generate-admin cũng được dùng để tạo ra backend interface với đầy đủ các chức năng ứng với một model class:

$ php symfony propel:generate-admin backend JobeetJob --module=job

$ php symfony propel:generate-admin backend JobeetCategory --module=category

Hai lệnh trên tạo ra module job và category ứng với các class model JobeetJob và JobeetCategory.

Option --module sẽ thay thế tên của module được tạo mặc định bởi lệnh (là jobeet_job với lớp JobeetJob).

Lệnh này cũng tạo route cho mỗi module:

# apps/backend/config/routing.ymljobeet_job: class: sfPropelRouteCollection options: model: JobeetJob module: job prefix_path: job column: id with_wildcard_routes: true

Bạn sẽ không ngạc nhiên khi thấy admin generator dùng route class sfPropelRouteCollection, do mục đích chính của một admin interface là quản lý vòng đời của các model objet.

Route cũng có một vài option chúng ta chưa thấy trước đây:

prefix_path: Defines the prefix path for the generated route (for instance, the edit page will be something like /job/1/edit).

column: Defines the table column to use in the URL for links that references an object.

with_wildcard_routes: As the admin interface will have more than the classic CRUD operations, this option allows to define more object and collection actions without editing the route.

Nên đọc help trước khi sử dụng một task mới.

$ php symfony help propel:generate-admin

Nó sẽ cung cấp cho bạn tất cả các argument và option cùng một vài ví dụ đơn giản.

Backend Look and Feel

Bây giờ, bạn có thể sử dụng các module đã được tạo ra:

http://jobeet.localhost/backend_dev.php/jobhttp://jobeet.localhost/backend_dev.php/category

Admin module có nhiều tính năng hơn các module đơn giản được tạo tự động trong những ngày trước. Mặc dù không phải viết dòng code PHP nào, chúng ta cũng có đầy đủ những tính năng sau:

Hiển thị danh sách cách đối tượng có phân trang Có thể sắp xếp danh sách Có thể lọc danh sách Có thể tạo, sửa, và xóa đối tượng Có thể chọn nhiều đối tượng để cùng thực hiện một thao tác nào đó (batch) Form nhập được validation Hiển thị Flash messages tới user ... và nhiều tính năng khác

Admin generator cung cấp đầy đủ các tính năng bạn cần để tạo một backend interface và đơn giản trong việc cấu hình.

Để thuận tiện cho người dùng, ta thay đổi lại layout mặc định của backend. Chúng ta sẽ thêm một menu đơn giản để dễ dàng chuyển qua lại giữa các modules khác nhau. Thay thế nội dung trong file layout.php bằng đoạn sau:

// apps/backend/templates/layout.php<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <title>Jobeet Admin Interface</title> <link rel="shortcut icon" href="/favicon.ico" /> <?php use_stylesheet('admin.css') ?> <?php include_javascripts() ?> <?php include_stylesheets() ?> </head> <body> <div id="container"> <div id="header"> <h1> <a href="<?php echo url_for('@homepage') ?>"> <img src="/images/jobeet.gif" alt="Jobeet Job Board" /> </a> </h1> </div>  <div id="menu"> <ul> <li><?php echo link_to('Jobs', '@jobeet_job') ?></li> <li><?php echo link_to('Categories', '@jobeet_category') ?></li> </ul> </div>  <div id="content"> <?php echo $sf_content ?> </div>

  <div id="footer"> <img src="/images/jobeet-mini.png" /> powered by <a href="http://www.symfony-project.org/"> <img src="/images/symfony.gif" alt="symfony framework" /></a> </div> </div> </body></html>

Giống như frontend, chúng tôi cũng chuẩn bị một file stylesheet đơn giản cho backend. File admin.css có thể download từ subversion tag của ngày hôm nay.

Cuối cùng, đổi lại trang chủ mặc định trong file routing.yml:

# apps/backend/config/routing.ymlhomepage: url: / param: { module: job, action: index }

Symfony Cache

Nếu bạn tò mò mở những file đã được tự động tạo trong thư mục apps/backend/modules/ bạn sẽ thấy ngạc nhiên! Thư mục templates hoàn toàn trống, và file actions.class.php cũng không có gì:

// apps/backend/modules/job/actions/actions.class.phprequire_once dirname(__FILE__).'/../lib/jobGeneratorConfiguration.class.php';require_once dirname(__FILE__).'/../lib/jobGeneratorHelper.class.php'; class jobActions extends autoJobActions{}

Làm thế nào để nó có thể hoạt động được? Nếu bạn để ý kĩ, bạn sẽ thấy rằng lớp jobActions thừa kế từ autoJobActions. Lớp autoJobActions được tự động tạo bởi symfony nếu nó không tồn tại. Nó có thể được tìm thấy trong thư mục cache/backend/dev/modules/autoJob/, và chứa các module "thực sự":

// cache/backend/dev/modules/autoJob/actions/actions.class.phpclass autoJobActions extends sfActions{ public function preExecute() { $this->configuration = new jobGeneratorConfiguration();  if (!$this->getUser()->hasCredential( $this->configuration->getCredentials($this->getActionName()) )) { // ...

Các module được tạo ra có thể được cấu hình bằng cách chỉnh sửa file config/generator.yml trong mỗi module:

# apps/backend/modules/job/config/generator.ymlgenerator: class: sfPropelGenerator param: model_class: JobeetJob theme: admin non_verbose_templates: true with_show: false singular: ~ plural: ~ route_prefix: jobeet_job with_propel_route: 1  config: actions: ~ fields: ~ list: ~ filter: ~

form: ~ edit: ~ new: ~

Mỗi khi bạn sửa file generator.yml, symfony sẽ tạo lại cache. Như chúng ta sẽ thấy trong hôm nay, chỉnh sửa admin generated modules là công việc dễ dàng, nhanh chóng và thú vị.

Việc tự động tạo lại các file cache chỉ được thực hiện trong môi trường development. Trong môi trường production, bạn cần tự xoá cache thông qua lệnh cache:clear.

Cấu hình Backend

Một module trong admin có thể được cấu hình thông qua file generator.yml. Nội dung cấu hình được tổ chức trong 7 mục:

actions: Default configuration for the actions found on the list and on the forms fields: Default configuration for the fields list: Configuration for the list filter: Configuration for the filters form: Configuration for the new/edit form edit: Specific configuration for the edit page new: Specific configuration for the new page

Hãy bắt đầu công việc cấu hình!

Chỉnh sửa tiêu đề

Tiêu đề của các mục list, edit, và new của module category có thể được chỉnh sửa thông qua option title:

config: actions: ~ fields: ~ list: title: Category Management filter: ~ form: ~ edit: title: Editing Category "%%name%%" (#%%id%%) new: title: New Category

title của mục edit chứa vài giá trị động: tất cả các chuỗi nằm giữa cụm %% được thay thế bởi giá trị của các cột tương ứng.

Cấu hình cho module job hoàn toàn tương tự:

config: actions: ~ fields: ~ list: title: Job Management filter: ~ form: ~ edit: title: Editing Job "%%company%% is looking for a %%position%% (#%%id%%)" new: title: Job Creation

Cấu hình các Field

Một field có thể là một cột trong model class, cũng có thể là một giá trị do ta tạo ra.

Cấu hình cho các field nằm trong mục fields:

config: fields: is_activated: { label: Activated?, help: Whether the user has activated the job, or not } is_public: { label: Public? }

Mục fields sẽ cấu hình cho các field ở tất cả các module, cấu hình trên sẽ thay đổi label của field is_activated ở các trang list, edit, và new.

Cấu hình trong admin generator dựa trên nguyên lý xếp tầng. Ví dụ, nếu bạn chỉ muốn thay đổi tiêu đề trong trang list, hãy tạo nó ở option fields dưới mục list:

config: list:

fields: is_public: { label: "Public? (label for the list)" }

Bất kì cấu hình nào nằm dưới mục fields chính đều có thể được thay đổi bởi cấu hình ở từng trang cụ thể. Luật cấu hình như sau:

new và edit thừa kế từ form, form lại được thừa kế từ fields list thừa kế từ fields filter thừa kế từ fields

Trong các mục về form (form, edit, và new), các option label và help sẽ thay thế nội dung mặc định trong các lớp form.

Cấu hình trang List

display

Mặc định, trang list hiển thị tất cả các cột trong model. Option display sẽ thay đổi hiển thị mặc định đó bằng cách chỉ rõ cột nào được hiển thị:

config: list: title: Category Management display: [=name, slug]

Kí tự = đặt trước cột name sẽ tạo link cho chuỗi này.

Làm tương tự với module job:

config: list: title: Job Management display: [company, position, location, url, is_activated, email]

layout

Trang list có thể được hiển thị trong các layout khác nhau. Mặc định là layout tabular, các cột sẽ được hiển thị trong một bảng. Nhưng với module job, ta nên sử dụng layout stacked, là layout khác có sẵn trong hệ thống:

config: list: title: Job Management layout: stacked display: [company, position, location, url, is_activated, email] params: | %%is_activated%% <small>%%category_id%%</small> - %%company%% (<em>%%email%%</em>) is looking for a %%=position%% (%%location%%)

Trong layout stacked, mỗi đối tượng được mô tả trong một chuỗi, tạo thành từ giá trị các cột xác định trong option params.

Option display vẫn cần thiết để người dùng có thể sắp xếp danh sách theo thứ tự tăng/giảm của từng cột.

Cột ảo

Với cấu hình trên, đoạn %%category_id%% sẽ được thay bằng khóa chính của category. Nhưng sẽ hữu ích hơn nếu hiển thị tên của category.

Khi bạn sử dụng kí hiệu %%, biến trong đó không nhất thiết phải tương ứng với một cột trong database schema. Khi đó, admin generator sẽ tìm phương thức getter tương ứng trong model class.

Để hiển thị category name, chúng ta có thể tạo phương thức getCategoryName() trong class model JobeetJob và thay thế %%category_id%% bằng %%category_name%%.

Nhưng lớp JobeetJob đã có phương thức getJobeetCategory() trả về đối tượng category liên quan. Và nếu bạn sử dụng %%jobeet_category%%, nó cũng hiển thị tên category vì lớp JobeetCategory có một magic method __toString() trả về tên của đối tượng.

%%is_activated%% <small>%%jobeet_category%%</small> - %%company%% (<em>%%email%%</em>) is looking for a %%=position%% (%%location%%)

sort

Là administrator, bạn sẽ muốn biết các công việc đưa lên gần đây nhất. Bạn có thể cấu hình để sắp xếp theo một cột nào đó bằng cách thêm option sort:

config: list: sort: [expires_at, desc]

max_per_page

Mặc định, danh sách sẽ được phân trang: 20 công việc mỗi trang. Bạn có thể thay đổi nó với option max_per_page:

config: list: max_per_page: 10

batch_actions

Ở trang list, ta có thể thực hiện một hành động với vài đối tượng một lúc (batch action). Batch action này là không cần thiết trong module category, vì thế ta bỏ nó đi:

config: list: batch_actions: {}

Option batch_actions xác định danh dách các batch action. Mảng rỗng có nghĩa là ta bỏ mọi chức năng.

Mặc định, mỗi module có một batch action delete tạo sẵn bởi framework, nhưng với module job, ta cần thêm một batch action để gia hạn cho những công việc được chọn thêm 30 ngày:

config: list: batch_actions: _delete: ~ extend: ~

Tất cả các action bắt đầu bằng _ là các action có sẵn trong framework. Nếu bạn refresh lại trình duyệt và chọn batch action extend, symfony sẽ hiện ra thông báo lỗi rằng bạn chưa có phương thức executeBatchExtend():

// apps/backend/modules/job/actions/actions.class.phpclass jobActions extends autoJobActions{ public function executeBatchExtend(sfWebRequest $request) { $ids = $request->getParameter('ids');  $criteria = new Criteria();

$criteria->add('jobeet_job.ID', $ids, Criteria::IN);  foreach (JobeetJobPeer::doSelect($criteria) as $job) { $job->extend(true); $job->save(); }  $this->getUser()->setFlash('notice', 'The selected jobs have been extended successfully.');  $this->redirect('@jobeet_job'); }}

Các khóa chính được chọn được chứa trong request parameter ids. Với mỗi công việc được chọn, phương thức JobeetJob::extend() được gọi với một tham số để bypass một vài kiểm tra trong phương thức. Chúng ta cần sửa lại phương thức extend() với tham số mới:

// lib/model/JobeetJob.phpclass JobeetJob extends BaseJobeetJob{ public function extend($force = false) { if (!$force && !$this->expiresSoon()) { return false; }  $this->setExpiresAt(time() + 86400 * sfConfig::get('app_active_days')); $this->save();  return true; }  // ...}

Sau khi tất cả các công việc đã được gia hạn, user được chuyển tới trang chủ của module job.

object_actions

Trong trang list, có thêm một cột chứa các action bạn có thể thực hiện với từng đối tượng riêng biệt. Với module category, cột này là không cần thiết, do đó chúng ra có thể bỏ chúng đi:

config: list: object_actions: {}

Với module job, chúng ta cần những action này và cần thêm một action mới extend tương tự như batch action chúng ta đã làm:

config: list: object_actions: extend: ~ _edit: ~ _delete: ~

Tương tự như batch action, các action _delete và _edit được tạo sẵn bởi framework. Chúng ta cần viết action listExtend() để link extend có thể làm việc:

// apps/backend/modules/job/actions/actions.class.phpclass jobActions extends autoJobActions{ public function executeListExtend(sfWebRequest $request) { $job = $this->getRoute()->getObject(); $job->extend(true); $job->save();  $this->getUser()->setFlash('notice', 'The selected jobs have been extended successfully.');

  $this->redirect('@jobeet_job'); }  // ...}

actions

Chúng ta đã biết cách tạo link cho một action đối với một danh sách các đối tượng được lựa chọn, hoặc một đối tượng riêng lẻ. Option actions xác định action không tác động đến đối tượng có sẵn nào, như: tạo một đối tượng mới. Hãy bỏ action new có sẵn và thêm một action mới xóa tất cả các công việc không còn được activate bởi người post quá 60 ngày:

list: list: actions: deleteNeverActivated: { label: Delete never activated jobs }

Mỗi action có thể được cấu hình bằng cách xác định một mảng các tham số. Action listDeleteNeverActivated rất đơn giản:

// apps/backend/modules/job/actions/actions.class.phpclass jobActions extends autoJobActions{

public function executeListDeleteNeverActivated(sfWebRequest $request) { $nb = JobeetJobPeer::cleanup(60);  if ($nb) { $this->getUser()->setFlash('notice', sprintf('%d never activated jobs have been deleted successfully.', $nb)); } else { $this->getUser()->setFlash('notice', 'No job to delete.'); }  $this->redirect('@jobeet_job'); }  // ...}

We have reused the JobeetJobPeer::cleanup() method defined yesterday. That's another great example of the reusability provided by the MVC pattern.

bạn cũng có thể thay đổi action để thực thi bằng các cung cấp giá trị cho tham số action:

deleteNeverActivated: { label: Delete never activated jobs, action: foo }

peer_method

Số câu truy vấn cần thiết để hiển thị trang list công việc là 13, được chỉ ra trên thanh web debug toolbar.

Nếu bạn click vào số này, bạn sẽ thấy phần lớn các câu truy vấn là lấy category name cho mỗi công việc.

Để giảm số câu truy vấn, chúng ta có thể thay đối phương thức mặc định để lấy danh sách các công việc bằng cách sử dụng option peer_method:

config: list: peer_method: doSelectJoinJobeetCategory

phương thức doSelectJoinJobeetCategory() thêm join giữa bảng job và bảng category và tự động tạo đối tượng category tương ứng với mỗi công việc.

Số câu truy vấn giờ giảm xuống còn 3:

Cấu hình Form

Cấu hình form nằm trong 3 mục: form, edit, và new. Các mục này có cấu hình như nhau và mục form chỉ tồn tại như một fallback cho các mục edit và new.

display

Giống như trang list, bạn có thể thay đổi thứ tự hiển thị các fields với option display. Nhưng do form hiển thị được tạo bởi một class, nên việc bỏ một field có thể dẫn đến lỗi khi validation.

Option display cho form có thể dùng để nhóm các field lại trong một nhóm:

config: form: display: Content: [category_id, type, company, logo, url, position, location, description, how_to_apply, is_public, email] Admin: [_token, is_activated, expires_at]

Cấu hình trên tạo ra 2 nhóm (Content và Admin), mỗi nhóm chứa một số field.

Admin generator cũng hỗ trợ sẵn quan hệ nhiều-nhiều. Ở form category, bạn có một input cho trường name, một cho trường slug, và một drop-down box cho các affiliate liên quan. Chúng ta không sửa giá trị này ở trang này nên ta cần bỏ đi:

// lib/model/JobeetCategoryForm.class.phpclass JobeetCategoryForm extends BaseJobeetCategoryForm{ public function configure() { unset($this['jobeet_category_affiliate_list']); }}

Cột ảo

Field _token bắt đầu bằng kí tự (_). Điều đó có nghĩa là việc render ra field này được tạo bởi một partial do ta tự tạo _token.php:

// apps/backend/modules/job/templates/_token.php<div class="sf_admin_form_row"> <label>Token</label> <?php echo $form->getObject()->getToken() ?></div>

Trong partial, bạn có thể truy cập form hiện tại ($form) và các đối tượng liên quan thông qua phương thức getObject().

Bạn cũng có thể sử dụng kết quả của một component bằng kí tự (~) đặt trước tên field

class

Do form được sử dụng bởi administrators, chúng ta cần hiển thị nhiều thông tin hơn khi được dùng bởi user. Nhưng hiện tại, một vài thông tin không được hiển thị do chúng ta đã bỏ chúng đi trong lớp JobeetJobForm.

Để form khác nhau giữa frontend và backend, chúng ta cần tạo 2 lớp form khác nhau. Hãy tạo lớp BackendJobeetJobForm thừa kế từ lớp JobeetJobForm. Do chúng ta không có những field ẩn đi như ở frontend, nên chúng ta cần refactor lớp JobeetJobForm bằng cách chuyển các đoạn unset() vào một phương thức để có thể overridden trong lớp BackendJobeetJobForm:

// lib/form/JobeetJobForm.class.phpclass JobeetJobForm extends BaseJobeetJobForm{ public function configure() { $this->removeFields();  $this->validatorSchema['email'] = new sfValidatorEmail();  // ... }  protected function removeFields() { unset( $this['created_at'], $this['updated_at'], $this['expires_at'], $this['token'], $this['is_activated'] ); }} // lib/form/BackendJobeetJobForm.class.phpclass BackendJobeetJobForm extends JobeetJobForm{ public function configure() { parent::configure(); }  protected function removeFields() { unset( $this['created_at'], $this['updated_at'], $this['token'] ); }}

Lớp form mặc định dùng bởi admin generator có thể thay đổi thông qua option class:

config: form: class: BackendJobeetJobForm

Form edit hiện vẫn còn một vấn đề nhỏ. Logo upload lên hiện chưa được hiển thị và bạn không thể xóa nó. Widger sfWidgetFormInputFileEditable thêm tính năng chỉnh sửa đối với một input file widget:

// lib/form/BackendJobeetJobForm.class.phpclass BackendJobeetJobForm extends JobeetJobForm{ public function configure() { parent::configure();  $this->widgetSchema['logo'] = new sfWidgetFormInputFileEditable(array( 'label' => 'Company logo', 'file_src' => '/uploads/jobs/'.$this->getObject()->getLogo(), 'is_image' => true, 'edit_mode' => !$this->isNew(), 'template' => '<div>%file%<br />%input%<br />%delete% %delete_label%</div>', )); }  // ...}

Widget sfWidgetFormInputFileEditable có vài option cho việc tinh chỉnh các tính năng và render:

file_src: đường dẫn đến file được upload is_image: nếu true, ảnh sẽ được hiển thị edit_mode: form ở chế độ edit hay không with_delete: có hiển thị checkbox delete hay không template: template dùng để render widget

Giao diện của admin generator có thể chỉnh sửa dễ dàng do template được tạo ra bao gồm rất nhiều class và id attributes. Ví dụ, có thể chỉnh sửa field logo thông qua class sf_admin_form_field_logo (class trong css :D). Mỗi field cũng có một class phụ thuộc vào kiểu field như sf_admin_text hay sf_admin_boolean.

Option edit_mode sử dụng phương thức sfPropel::isNew().

Nó trả về true nếu model object của form là mới, và false trong trường hợp ngược lại. Nó giúp ích rất nhiều khi bạn càn có những widget hay validator khác nhau tùy vào trạng thái của đối tượng.

Cấu hình Filter

Cấu hình cho filter tương tự như cấu hình cho form. Đơn giản vì filter chính là các form. Và các form này là các lớp được tạo thông qua lệnh propel:build-all. Bạn cũng có thể tạo lại chúng với lệnh propel:build-filters.

Các lớp form filter nằm trong thư mục lib/filter và mỗi model class có một filter form class tương ứng (JobeetJobFormFilter ứng với JobeetJobForm).

Chúng ta không cần filter trong module category:

config: filter: class: false

Với module job, ta bỏ một số field:

filter: display: [category_id, company, position, description, is_activated, is_public, email, expires_at]

Do filter là không bắt buộc, nên ta không cần override lớp filter form để cấu hình hiển thị các field.

Actions Customization

Khi việc cấu hình không thỏa mãn được yêu cầu của bạn, bạn có thể thêm một phương thức mới vào action như chúng ta đã làm với tính năng extend, nhưng bạn cũng có thể override các phương thức được tạo ra:

Method Mô tảexecuteIndex() list view action

Method Mô tảexecuteFilter() Updates the filtersexecuteNew() new view actionexecuteCreate() Creates a new JobexecuteEdit() edit view actionexecuteUpdate() Updates a JobexecuteDelete() Deletes a JobexecuteBatch() Executes a batch actionexecuteBatchDelete()Executes the _delete batch actionprocessForm() Processes the Job formgetFilters() Returns the current filterssetFilters() Sets the filtersgetPager() Returns the list pagergetPage() Gets the pager pagesetPage() Sets the pager pagebuildCriteria() Builds the Criteria for the listaddSortCriteria() Adds the sort Criteria for the listgetSort() Returns the current sort columnsetSort() Sets the current sort column

Do mỗi phương thức tạo ra chỉ thực hiện một công việc, nên việc thay đổi behavior là khá dễ dàng mà không cần copy & paste quá nhiều code.

Templates Customization

Chúng ta đã biết cách chỉnh sửa templates được tạo ra nhờ có các class và id attribute trong mã HTML.

Bạn cũng có thể thay thế template được tạo ra. Do template là file PHP đơn thuần chứ không chứa lớp PHP, nên bạn có thể thay thế template bằng cách tạo một template cùng tên trong module (ví dụ trong thư mục apps/backend/modules/job/templates/ của admin module job):

Template Mô tả_assets.php Renders the CSS and JS to use for templates_filters.php Renders the filters box_filters_field.php Renders a single filter field_flashes.php Renders the flash messages_form.php Displays the form_form_actions.php Displays the form actions_form_field.php Displays a singe form field_form_fieldset.php Displays a form fieldset_form_footer.php Displays the form footer

Template Mô tả_form_header.php Displays the form header_list.php Displays the list_list_actions.php Displays the list actions_list_batch_actions.php Displays the list batch actions_list_field_boolean.php Displays a single boolean field in the list_list_footer.php Displays the list footer_list_header.php Displays the list header_list_td_actions.php Displays the object actions for a row_list_td_batch_actions.phpDisplays the checkbox for a row_list_td_stacked.php Displays the stacked layout for a row_list_td_tabular.php Displays a single field for the list_list_th_stacked.php Displays a single column name for the header_list_th_tabular.php Displays a single column name for the header_pagination.php Displays the list paginationeditSuccess.php Displays the edit viewindexSuccess.php Displays the list viewnewSuccess.php Displays the new view

Kết quả cuối cùng

Kết quả cuối cùng của việc cấu hình cho Jobeet admin:

# apps/backend/modules/job/config/generator.ymlconfig: actions: ~ fields: is_activated: { label: Activated?, help: Whether the user has activated the job, or not } is_public: { label: Public? } list: title: Job Management layout: stacked display: [company, position, location, url, is_activated, email] params: | %%is_activated%% <small>%%jobeet_category%%</small> - %%company%% (<em>%%email%%</em>) is looking for a %%=position%% (%%location%%) max_per_page: 10 sort: [expires_at, desc] batch_actions: _delete: ~ extend: ~ object_actions: extend: ~ _edit: ~ _delete: ~

actions: deleteNeverActivated: { label: Delete never activated jobs } peer_method: doSelectJoinJobeetCategory filter: display: [category_id, company, position, description, is_activated, is_public, email, expires_at] form: class: BackendJobeetJobForm display: Content: [category_id, type, company, logo, url, position, location, description, how_to_apply, is_public, email] Admin: [_token, is_activated, expires_at] edit: title: Editing Job "%%company%% is looking for a %%position%% (#%%id%%)" new: title: Job Creation # apps/backend/modules/category/config/generator.ymlconfig: actions: ~ fields: ~ list: title: Category Management display: [=name, slug] batch_actions: {} object_actions: {} filter: class: false form: actions: _delete: ~ _list: ~ _save: ~ edit: title: Editing Category "%%name%%" (#%%id%%) new: title: New Category

Chỉ với 2 file cấu hình, chúng ta đã phát triển một backend interface đầy đủ các tính năng cho Jobeet trong vài phút.

Bạn đã biết rằng khi một thứ có thể cấu hình trong file YAML, nó cũng có thể được cấu hình sử dụng code PHP. Với admin generator, bạn có thể sửa file apps/backend/modules/job/lib/jobGeneratorConfiguration.class.php. Nó cũng có các option như file YAML nhưng thông qua code PHP. Để biết tên của những phương thức này, hãy xem trong lớp cache/backend/dev/modules/autoJob/lib/BaseJobGeneratorConfiguration.clas

s.php.

Hẹn gặp lại ngày mai

Chỉ trong một giờ, chúng ta đã xây dựng đầ đủ các tính năng của một backend interface cho Jobeet project. Để làm tất cả những việc đó, chúng ta chỉ viết chưa đến 50 dòng code PHP. Thật tuyệt!

Ngày mai, chúng ta sẽ học cách bảo mật application backend với username và password. Đồng thời, chúng ta cũng đề cập đến lớp symfony user.

Tóm tắt

Ngày hôm qua chúng ta đã học rất nhiều thứ. Với rất ít dòng code PHP, symfony admin generator cho phép lập trình viên tạo một backend interfaces trong vài phút.

Hôm nay, chúng ta sẽ khám phá cách symfony quản lý các persistent data giữa các HTTP requests. Như bạn đã biết, giao thức HTTP là stateless, có nghĩa là mỗi request không phụ thuộc vào request trước hay hiện tại. Các website ngày nay cần có cách để lưu lại các dữ liệu giữa các request để nâng cao khả năng tương tác với user.

Một user session có thể xác định thông qua cookie. Trong symfony, lập trình viên không cần trực tiếp quản lý session, mà có thể sử dụng đối tượng sfUser.

User Flashes

Ở admin, chúng ta đã dùng đối tượng user với flash. Một flash là một thông điệp nhanh được chứa trong user session, sẽ được tự động xóa đi ở request tiếp theo. Nó rất hữu ích khi bạn muốn hiển thị một thông điệp tới người dùng sau khi redirect. Admin generator sử dụng flash rất nhiều để hiển thị phản hồi tới người dùng mỗi khi công việc được lưu, xóa, hay gia hạn.

Một flash được set thông qua phương thức setFlash() của lớp sfUser:

// apps/frontend/modules/job/actions/actions.class.phppublic function executeExtend(sfWebRequest $request){ $request->checkCSRFProtection();  $job = $this->getRoute()->getObject(); $this->forward404Unless($job->extend());  $this->getUser()->setFlash('notice', sprintf('Your job validity has been extend until %s.', $job->getExpiresAt('m/d/Y')));  $this->redirect($this->generateUrl('job_show_user', $job));}

Tham số đầu tiên xác định kiểu flash và tham số thứ 2 là nội dung thông báo. Bạn cũng có thể dùng bất kì kiểu flash nào, nhưng notice và error là 2 loại phổ biến (chúng được sử dụng bởi admin generator).

Ta sẽ thêm một flash message vào templates. Với Jobeet, chúng ta thêm vào layout.php:

// apps/frontend/templates/layout.php<?php if ($sf_user->hasFlash('notice')): ?> <div class="flash_notice"><?php echo $sf_user->getFlash('notice') ?></div><?php endif; ?> <?php if ($sf_user->hasFlash('error')): ?> <div class="flash_error"><?php echo $sf_user->getFlash('error') ?></div><?php endif; ?>

Trong template, ta dùng biến sf_user để truy cập.

Một vài đối tượng của symfony có thể truy cập từ template, không cần thông qua action: sf_request, sf_user, và sf_response.

User Attributes

Trong ngày 2 chúng ta đã quên không có yêu cầu chứa một số thứ trong user session. Ta sẽ thêm một yêu cầu mới: để dễ dàng xem các công việc, 3 công việc người dùng xem lần cuối sẽ được hiển thị trên menu với link đến trang công việc đó.

Khi người dùng xem một công việc, job object hiện tại cần được thêm vào danh sách công việc đã xem và chứa trong session:

// apps/frontend/modules/job/actions/actions.class.phpclass jobActions extends sfActions{ public function executeShow(sfWebRequest $request) { $this->job = $this->getRoute()->getObject();  // fetch jobs already stored in the job history $jobs = $this->getUser()->getAttribute('job_history', array());  // add the current job at the beginning of the array array_unshift($jobs, $this->job->getId());  // store the new job history back into the session $this->getUser()->setAttribute('job_history', $jobs); }  // ...}

Chúng ta hoàn toàn có thể chứa trực tiếp đối tượng JobeetJob trong session. Nhưng điều này là không nên bởi nếu đối tượng bị thay đổi thì thông tin chứa trong session sẽ không còn giá trị.

getAttribute(), setAttribute()

Phương thức sfUser::getAttribute() lấy các giá trị từ user session. Ngược lại, phương thức setAttribute() chứa các biến PHP vào session.

Phương thức getAttribute() cũng có một optional chứa giá trị mặc định nếu không lấy được giá trị trong session.

Giá trị mặc định tạo bởi phương thức getAttribute() tương đương với:

if (!$value = $this->getAttribute('job_history')){ $value = array();}

Lớp myUser

Để thuận tiện cho việc tổ chức code, hãy chuyển code vào lớp myUser class. Lớp myUser override lớp mặc định sfUser với các behavior riêng cho ứng dụng:

// apps/frontend/modules/job/actions/actions.class.phpclass jobActions extends sfActions{ public function executeShow(sfWebRequest $request) { $this->job = $this->getRoute()->getObject();  $this->getUser()->addJobToHistory($this->job); }  // ...} // apps/frontend/lib/myUser.class.phpclass myUser extends sfBasicSecurityUser{ public function addJobToHistory(JobeetJob $job) { $ids = $this->getAttribute('job_history', array());  if (!in_array($job->getId(), $ids)) { array_unshift($ids, $job->getId());  $this->setAttribute('job_history', array_slice($ids, 0, 3)); } }}

Code đã được sửa lại một chút cho hợp với yêu cầu:

!in_array($job->getId(), $ids): một công việc không thể được chứa 2 lần

array_slice($ids, 0, 3): chỉ chứa 3 công việc được xem gần nhất

Trong layout, thêm đoạn code sau vào trước đoạn biến $sf_content:

// apps/frontend/templates/layout.php<div id="job_history"> Recent viewed jobs: <ul> <?php foreach ($sf_user->getJobHistory() as $job): ?> <li> <?php echo link_to($job->getPosition().' - '.$job->getCompany(), 'job_show_user', $job) ?> </li> <?php endforeach; ?> </ul></div> <div class="content"> <?php echo $sf_content ?></div>

Layout sử dụng một phương thức mới getJobHistory() để nhận các job history hiện tại:

// apps/frontend/lib/myUser.class.phpclass myUser extends sfBasicSecurityUser{ public function getJobHistory() { $ids = $this->getAttribute('job_history', array());  return JobeetJobPeer::retrieveByPKs($ids); }  // ...}

The getJobHistory() method uses the Propel retrieveByPKs() method to retrieve several JobeetJob objects in one call.

Để giao diện dễ nhìn chút, chúng ta cần thêm một vài css vào cuối file main.css:

/* web/css/main.css */#job_history{ padding: 7px; background: #eee; font-size: 80%;} #job_history ul{ display: inline;

} #job_history li{ margin-right: 10px; display: inline;}

sfParameterHolder

Để hoàn thiện job history API, ta cần thêm một phương thức để xóa history:

// apps/frontend/lib/myUser.class.phpclass myUser extends sfBasicSecurityUser{ public function resetJobHistory() { $this->getAttributeHolder()->remove('job_history'); }  // ...}

User attributes được quản lý bởi một object của lớp sfParameterHolder. Phương thức getAttribute() và setAttribute() là cách viết tắt của getParameterHolder()->get() và getParameterHolder()->set(). Do phương thức remove() không có cách viết khác nào trong sfUser, nên chúng ta cần dùng trực tiếp đối tượng holder.

LớpsfParameterHolder cũng được sử dụng bởi sfRequest để chứa các parameter.

Bảo mật ứng dụng

Xác thực người dùng

Giống như nhiều tính năng khác trong symfony, vấn đề xác thực cũng được quản lý bởi file YAML security.yml. Bạn có thể thấy cấu hình mặc định cho backend application trong thư mục config/:

// apps/backend/config/security.ymldefault: is_secure: off

Nếu bạn chuyển is_secure thành on, toàn bộ backend application sẽ yêu cầu user phải đăng nhập để sử dụng.

Trong file YAML, môt giá trị boolean xác định bằng chuỗi true và false, hoặc on và off.

Nếu bạn xem nội dung log ở web debug toolbar, bạn sẽ thấy rằng phương thức executeLogin() của lớp defaultActions được gọi mỗi khi bạn truy cập vào trang yêu cầu đăng nhập.

Khi người dùng chưa đăng nhập truy cập vào một trang yêu cầu đăng nhập, symfony sẽ chuyển người dùng sang trang login được xác định trong settings.yml:

all: .actions: login_module: default login_action: login

Không được thiết lập secure cho action login để tránh đệ quy.

Như chúng ta đã biết trong ngày 4, các file cấu hình giống nhau có thể để ở nhiều nơi. File security.yml cũng vậy. Để secure hay un-secure cho một action hay toàn bộ module, hãy tạo file security.yml trong thư mục config/ của module đó:

index: is_secure: offall: is_secure: on

Mặc định, lớp myUser kế thừa từ sfBasicSecurityUser, chứ không phải từ sfUser. sfBasicSecurityUser cung cấp thêm các phương thức để quản lý user authentication và authorization.

Để quản lý user authentication, sử dụng phương thức isAuthenticated() và setAuthenticated():

if (!$this->getUser()->isAuthenticated()){ $this->getUser()->setAuthenticated(true);}

Phân quyền

Khi một user đã đăng nhập, việc truy cập một số action có thể bị hạn chế hay không tùy theo quyền hạn của họ. User cần có quyền hạn để truy cập một trang:

default: is_secure: off credentials: admin

Hệ thống phân quyền của symfony đơn giản mà mạnh mẽ. Bạn có thể xác định quyền truy cập bất kì phần nào của ứng dụng.

Phân quyền phức tạp

Mục credentials trong security.yml hỗ trợ toán tử Boolean để mô tả các yêu cầu phân quyền phức tạp.

Nếu một user cần phải có cả quyền A và B, ta sử dụng dấu ngoặc vuông:

index: credentials: [A, B]

Nếu một user cần có quyền A hoặc B, ta dùng 2 dấu ngoặc vuông:

index: credentials: [[A, B]]

Bạn có thể sử dụng nhiều dấu ngoặc để mô tả bất kì biểu thức Boolean nào với bất kì kiểu phân quyền nào.

Để quản lý user credentials, sfBasicSecurityUser cung cấp vài phương thức:

// Add one or more credentials$user->addCredential('foo');$user->addCredentials('foo', 'bar'); // Check if the user has a credentialecho $user->hasCredential('foo'); => true // Check if the user has both credentialsecho $user->hasCredential(array('foo', 'bar')); => true // Check if the user has one of the credentialsecho $user->hasCredential(array('foo', 'bar'), false); => true // Remove a credential$user->removeCredential('foo');echo $user->hasCredential('foo'); => false // Remove all credentials (useful in the logout process)$user->clearCredentials();echo $user->hasCredential('bar'); => false

Với Jobeet backend, chúng ta không sử dụng bất kì credential nào do chúng ta chỉ có một profile: administrator.

Plugins

Chúng ta không nên làm lại cái bánh xe, chúng ta sẽ không phát triển action login từ đầu. Thay vào đó, chúng ta sẽ cài đặt một symfony plugin.

Một trong những điểm mạnh của symfony framework là hệ thống plugin. Như chúng ta sẽ thấy trong vài ngày sau, tạo plugin là một việc dễ dàng. Nó thực sự mạnh mẽ, do một plugin có thể chứa bất cứ thứ gì từ cấu hình đến các module và các asset.

Hôm nay, chúng ta sẽ cài đặt sfGuardPlugin để secure cho backend application:

$ php symfony plugin:install sfGuardPlugin

Lệnh plugin:install cài đặt một plugin thông qua tên của nó. Tất cả các plugin đều nằm trong thư mục plugins/ và mỗi plugin là một thư mục có tên là tên plugin.

Bạn cần cài PEAR để có thể chạy lệnh plugin:install.

Khi bạn cài một plugin sử dụng lệnh plugin:install, symfony sẽ cài bản mới nhất của plugin đó. Để cài cụ thể một phiên bản nào đó, hãy cung cấp phiên bản thông qua option --release.

The plugin page lists all available version grouped by symfony version.

As a plugin is self-contained into a directory, you can also download the package from the symfony website and unarchive it, or alternatively make an svn:externals link to its Subversion repository.

Backend Security

Mỗi plugin có một file README miêu tả cách cấu hình cho plugin đó.

Hãy xem cách cấu hình cho plugin vừa cài. Do plugin cần vài model class mới để quản lý users, groups, và permissions, bạn cần rebuild model:

$ php symfony propel:build-all-load

Lệnh propel:build-all-reload sẽ xóa tất cả các bảng đã có trước khi tạo lại chúng. Để tránh điều này, bạn có thể build models, forms, và filters, sau đó, tạo thêm các bảng mới bằng cách thực thi câu SQL sinh ra trong data/sql.

Đồng thời, khi các lớp mới được tạo, bạn cần xóa cache:

$ php symfony cc

Do sfGuardPlugin thêm một vài phương thức cho lớp user, nên bạn cần đổi lớp cha của myUser thành sfGuardSecurityUser:

// apps/backend/lib/myUser.class.phpclass myUser extends sfGuardSecurityUser{}

sfGuardPlugin cung cấp một action mặc định để authenticate users:

// apps/backend/config/settings.ymlall: .settings: enabled_modules: [default, sfGuardAuth]  # ...

  .actions: login_module: sfGuardAuth login_action: signin  # ...

Do plugin có thể được sử dụng ở tất cả các application của project, nên bạn cần chỉ rõ những module nào bạn muốn sử dụng thông qua setting enabled_modules.

Cuối cùng tạo một tài khoản administrator:

$ php symfony guard:create-user fabien $ecretPa$$$ php symfony guard:promote fabien

Plugin này cung cấp các lệnh để quản lý users, groups, và permissions từ dòng lệnh. Dùng lệnh list để xem danh sách tất cả các lệnh của namespace guard:

$ php symfony list guard

Khi user không được authenticate, ta cần ẩn đi menu bar:

// apps/backend/templates/layout.php<?php if ($sf_user->isAuthenticated()): ?> <div id="menu"> <ul> <li><?php echo link_to('Jobs', '@jobeet_job') ?></li> <li><?php echo link_to('Categories', '@jobeet_category') ?></li> </ul> </div><?php endif; ?>

Khi user được authenticate, chúng ta cần thêm 1 link để logout:

// apps/backend/templates/layout.php<li><?php echo link_to('Logout', '@sf_guard_signout') ?></li>

Để xem tất cả các routes, sử dụng lệnh app:routes.

Để Jobeet backend thuận tiện hơn, ta cần một module để quản lý các administrator user. May thay, plugin đã cung cấp sẵn cho ta một module như vậy. Đó là module sfGuardAuth, và bạn cần enable nó trong settings.yml:

// apps/backend/config/settings.ymlall: .settings: enabled_modules: [default, sfGuardAuth, sfGuardUser]

Thêm link vào menu:

// apps/backend/templates/layout.php<li><?php echo link_to('Users', '@sf_guard_user') ?></li>

Chúng ta đã làm xong!

User Testing

Hướng dẫn ngày hôm nay chưa kết thúc, chúng ta chưa có user testing!. Do symfony browser có thể giả lập cookies, nên việc test user behaviors thực sự đơn giản bằng cách sử dụng tester có sẵn sfTesterUser.

Thêm functional tests cho tính năng menu chúng ta đã tạo hôm nay. Thêm đoạn code sau vào cuối functional tests của module job:

// test/functional/frontend/jobActionsTest.php$browser-> info('4 - User job history')->  loadData()-> restart()->  info(' 4.1 - When the user access a job, it is added to its history')-> get('/')-> click('Web Developer', array('position' => 1))-> get('/')-> with('user')->begin()-> isAttribute('job_history', array($browser->getMostRecentProgrammingJob()->getId()))-> end()->  info(' 4.2 - A job is not added twice in the history')-> click('Web Developer', array('position' => 1))-> get('/')-> with('user')->begin()-> isAttribute('job_history', array($browser->getMostRecentProgrammingJob()->getId()))-> end();

Để thuận tiện cho việc test, đầu tiên chúng ta cần nạp lại fixtures data và restart trình duyệt để bắt đầu với một session mới.

Phương thức isAttribute() kiểm tra user attribute.

sfTesterUser tester cũng cung cấp phương thức isAuthenticated() và hasCredential() để test user authentication và autorizations.

Hẹn gặp lại ngày mai

Lớp symfony user là cách tốt để trừu tượng hóa việc quản lý PHP session. Cùng với hệ thống plugin tuyệt vời của symfony và plugin sfGuardPlugin , chúng ta có thể bảo mật cho Jobeet backend trong vài phút. Và chúng ta cũng có thể quản lý các tài khoản administrator, nhờ module cung cấp bởi plugin.

Ngày mai là ngày cuối cùng của tuần 2, và chúng ta hi vọng sẽ thu được nhiều điều bổ ích.

Tóm tắt

Với những hướng dẫn về lớp symfony User ngày hôm qua, chúng ta đã điểm qua hầu hết các tính năng cơ bản của symfony. Bạn vẫn cần học thêm rất nhiều, nhưng bạn đã có thể tự tạo một symfony projects đơn giản.

Để chuẩn bị cho những bài học tiếp theo, hôm nay chúng ta sẽ nghỉ ngơi. Đúng hơn là tôi sẽ nghỉ ngơi vào ngày hôm nay :D. Sẽ không có hướng dẫn nào, nhưng tôi sẽ đưa ra một vài gợi ý bạn có thể làm hôm nay để nâng cao kĩ năng sử dụng symfony.

Học qua thực hành

Symfony framework, cũng như các phần mềm khác, đều có chung một cách học. Đầu tiên là học qua các ví dụ trong các tutorial hay một cuốn sách nào đó. Sau đó là thực hành. Không gì có thể thay thế được thực hành.

Đó là những gì bạn có thể làm ngày hôm nay. Hãy nghĩ về một web project đơn giản nhất: quản lý các công việc phải làm, một blog đơn giản, một công cụ chuyển đổi thời gian hay tiền tệ, ... Hãy chọn lấy một cái và bắt đầu triển khai nó với những kiến thức bạn đã học được. Sử dụng task help để xem các option khác của lệnh, xem các code được symfony tạo sẵn, sử dụng một text editor có chức năng PHP auto-completion như Eclipse, đọc online API khi bạn cần tìm một phương thức mới, đưa các câu hỏi của bạn lên user mailing-list, chat trên #symfony IRC channel on freenode.

Hãy sử dụng tất cả các công cụ miễn phí có sẵn để học nhiều hơn về symfony.

bão

Hẹn gặp lại ngày mai

Bài học hôm nay kết thúc ở đây ;)). Trong những ngày tiếp theo, chúng ta sẽ nói về AJAX, plugin, internationalization, caching, deployment, và nhiều thứ khác.

Hãy quay lại vào ngày mai để bắt đầu một tuần mới của Jobeet.

Tóm tắt

Hôm qua, bạn đã phát triển ứng dụng đầu tiên của mình với symfony. Đừng dừng lại. Khi bạn học được nhiều hơn về symfony, hãy thêm những tính năng mới vào ứng dụng, đưa nó lên đâu đó, và chia sẻ với cộng đồng.

Hôm nay, chúng ta sẽ chuyển sang một vấn đề hoàn toàn mới.

Nếu bạn đang tìm kiếm một công việc, có lẽ bạn sẽ muốn được biết ngay khi công việc được đưa lên. Và thật bất tiện khi phải thường xuyên truy cập vào website để kiểm tra. Để người dùng có thể cập nhật các công việc mới ở Jobeet, hôm nay chúng ta sẽ thêm vài job feed.

Format

Symfony framework hỗ trợ sẵn vấn đề formats và mime-types. Có nghĩa là cùng một Model và Controller có thể có các template khác nhau tùy vào format được yêu cầu. Format mặc định là HTML nhưng symfony cũng hỗ trợ những format khác như txt, js, css, json, xml, rdf, và atom.

Format có thể được set nhờ phương thức setRequestFormat() của đối tượng request:

$request->setRequestFormat('xml');

Nhưng format thường được nhúng trong URL. Trong trường hợp này, symfony sẽ thiết lập cho bạn dựa vào biến sf_format dùng trong route. Với job list, URL là:

http://jobeet.localhost/frontend_dev.php/job

URL này tương đương với:

http://jobeet.localhost/frontend_dev.php/job.html

2 URL là như nhau vì route tạo bởi lớp sfPropelRouteCollection có chứa sf_format. Bạn có thể kiểm tra điều này bằng cách chạy lệnh app:routes:

Feed

Feed các công việc mới nhất

Hỗ trợ các format khác nhau đơn giản là tạo các template khác nhau. Để tạo một Atom feed cho các công việc mới nhất, ta tạo template indexSuccess.atom.php:

<!-- apps/frontend/modules/job/templates/indexSuccess.atom.php --><?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom"> <title>Jobeet</title> <subtitle>Latest Jobs</subtitle> <link href="" rel="self"/> <link href=""/> <updated></updated> <author><name>Jobeet</name></author> <id>Unique Id</id>  <entry> <title>Job title</title> <link href="" /> <id>Unique id</id> <updated></updated> <summary>Job description</summary> <author><name>Company</name></author> </entry></feed>

Tên Template

Do html là format được dùng phổ biến nhất đối với ứng dụng web, nên bạn có thể bỏ qua trong tên của template. Cả 2 template indexSuccess.php và indexSuccess.html.php là như nhau và symfony sẽ sử dụng cái đầu tiên nó tìm thấy.

Tại sao các template mặc định đều có cụm Success? Việc xác định template nào được render dựa vào giá trị trả về của action. Nếu action không trả về gì cả, nó tương đương với:

return sfView::SUCCESS; // == 'Success'

Nếu bạn muốn thay đổi cụm sau, hãy trả về một giá trị khác:

return sfView::ERROR; // == 'Error' return 'Foo';

Bạn cũng có thể đổi tên của template bằng cách sử dụng phương thức setTemplate():

$this->setTemplate('foo');

Mặc định, symfony sẽ thay đổi response Content-Type tùy theo format, và với tất cả format không phải HTML, layout sẽ không được sử dụng. Với Atom feed, symfony thay đổi Content-Type thành application/atom+xml; charset=utf-8

Ở Jobeet footer, sửa lại link của feed:

<!-- apps/frontend/templates/layout.php --><li class="feed"> <a href="<?php echo url_for('@job?sf_format=atom') ?>">Full feed</a></li>

Internal URI tương tự như job list với một biến được thêm vào sf_format.

Thêm tag <link> vào head ở layout:

<!-- apps/frontend/templates/layout.php --><link rel="alternate" type="application/atom+xml" title="Latest Jobs" href="<?php echo url_for('@job?sf_format=atom', true) ?>" />

Với href attribute của link, ta sử dụng đường dẫn tuyệt đối nhờ tham số thứ 2 của helper url_for().

Sửa lại Atom template header:

<!-- apps/frontend/modules/job/templates/indexSuccess.atom.php --><title>Jobeet</title><subtitle>Latest Jobs</subtitle><link href="<?php echo url_for('@job?sf_format=atom', true) ?>" rel="self"/><link href="<?php echo url_for('@homepage', true) ?>"/><updated><?php echo gmstrftime('%Y-%m-%dT%H:%M:%SZ', JobeetJobPeer::getLatestPost()->getCreatedAt('U')) ?></updated><author> <name>Jobeet</name>

</author><id><?php echo sha1(url_for('@job?sf_format=atom', true)) ?></id>

Notice the usage of U as an argument to getCreatedAt() to get the date as a timestamp. To get the date of the latest post, create the getLatestPost() method:

// lib/model/JobeetJobPeer.phpclass JobeetJobPeer extends BaseJobeetJobPeer{ static public function getLatestPost() { $criteria = new Criteria(); self::addActiveJobsCriteria($criteria);  return JobeetJobPeer::doSelectOne($criteria); }  // ...}

Các feed entry được tạo bằng đoạn code sau:

<!-- apps/frontend/modules/job/templates/indexSuccess.atom.php --><?php use_helper('Text') ?><?php foreach ($categories as $category): ?> <?php foreach ($category->getActiveJobs(sfConfig::get('app_max_jobs_on_homepage')) as $job): ?> <entry> <title> <?php echo $job->getPosition() ?> (<?php echo $job->getLocation() ?>) </title> <link href="<?php echo url_for('job_show_user', $job, true) ?>" /> <id><?php echo sha1($job->getId()) ?></id> <updated><?php echo gmstrftime('%Y-%m-%dT%H:%M:%SZ', $job->getCreatedAt('U')) ?></updated> <summary type="xhtml"> <div xmlns="http://www.w3.org/1999/xhtml"> <?php if ($job->getLogo()): ?> <div> <a href="<?php echo $job->getUrl() ?>"> <img src="http://<?php echo $sf_request->getHost().'/uploads/jobs/'.$job->getLogo() ?>" alt="<?php echo $job->getCompany() ?> logo" /> </a> </div> <?php endif; ?>  <div> <?php echo simple_format_text($job->getDescription()) ?> </div>  <h4>How to apply?</h4>  <p><?php echo $job->getHowToApply() ?></p>

</div> </summary> <author> <name><?php echo $job->getCompany() ?></name> </author> </entry> <?php endforeach; ?><?php endforeach; ?>

Phương thức getHost() của đối tượng request ($sf_request) trả về host hiện tại, cần cho việc tạo đường dẫn tuyệt đối tới company logo.

Khi tạo một feed, bạn có thể debug dễ dàng thông qua các lệnh như curl hay wget, bạn sẽ thấy được nội dung thực sự của các feed.

nếu bạn gặp lỗi Parse error: syntax error, unexpected T_STRING, có thể do bạn để "short_open_tags = On" trong php.ini, hãy chuyển nó thành Off (người dịch)

Feed các công việc mới nhất của một Category

Một trong những mục đích của Jobeet là giúp mọi người tìm được công việc phù hợp. Vì thế, chúng ta cần cung cấp feed cho mỗi category.

Đầu tiên, sửa lại route category để hỗ trợ các format khác:

// apps/frontend/config/routing.ymlcategory: url: /category/:slug.:sf_format class: sfPropelRoute param: { module: category, action: show, sf_format: html } options: { model: JobeetCategory, type: object } requirements: sf_format: (?:html|atom)

Bây giờ, route category đã hiểu cả format html và atom. Sửa lại link tới category feed trong templates:

<!-- apps/frontend/modules/job/templates/indexSuccess.php --><div class="feed"> <a href="<?php echo url_for('category', array('sf_subject' => $category, 'sf_format' => 'atom')) ?>">Feed</a></div> [php]<!-- apps/frontend/modules/category/templates/showSuccess.php --><div class="feed"> <a href="<?php echo url_for('category', array('sf_subject' => $category, 'sf_format' => 'atom')) ?>">Feed</a></div>

Cuối cùng, tạo template showSuccess.atom.php. Nhưng feed này cũng liệt kê các công việc, nên chúng ta có thể refactor mã nguồn bằng cách cho các feed entry vào trong partial _list.atom.php. Partial bây giờ có format atom:

<!-- apps/frontend/job/templates/_list.atom.php --><?php use_helper('Text') ?> <?php foreach ($jobs as $job): ?> <entry> <title><?php echo $job->getPosition() ?> (<?php echo $job->getLocation() ?>)</title> <link href="<?php echo url_for('job_show_user', $job, true) ?>" /> <id><?php echo sha1($job->getId()) ?></id> <updated><?php echo gmstrftime('%Y-%m-%dT%H:%M:%SZ', $job->getCreatedAt('U')) ?></updated> <summary type="xhtml"> <div xmlns="http://www.w3.org/1999/xhtml"> <?php if ($job->getLogo()): ?> <div> <a href="<?php echo $job->getUrl() ?>"> <img src="http://<?php echo $sf_request->getHost().$job->getLogo() ?>" alt="<?php echo $job->getCompany() ?> logo" /> </a> </div> <?php endif; ?>  <div>

<?php echo simple_format_text($job->getDescription()) ?> </div>  <h4>How to apply?</h4>  <p><?php echo $job->getHowToApply() ?></p> </div> </summary> <author> <name><?php echo $job->getCompany() ?></name> </author> </entry><?php endforeach; ?>

Bạn có thể sử dụng partial _list.atom.php trong template job feed:

<!-- apps/frontend/modules/job/templates/indexSuccess.atom.php --><?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom"> <title>Jobeet</title> <subtitle>Latest Jobs</subtitle> <link href="<?php echo url_for('@job?sf_format=atom', true) ?>" rel="self"/> <link href="<?php echo url_for('@homepage', true) ?>"/> <updated><?php echo gmstrftime('%Y-%m-%dT%H:%M:%SZ', JobeetJobPeer::getLatestPost()->getCreatedAt('U')) ?></updated> <author> <name>Jobeet</name> </author> <id><?php echo sha1(url_for('@job?sf_format=atom', true)) ?></id> <?php foreach ($categories as $category): ?> <?php include_partial('job/list', array('jobs' => $category->getActiveJobs(sfConfig::get('app_max_jobs_on_homepage')))) ?><?php endforeach; ?></feed>

Cuối cùng, tạo template showSuccess.atom.php:

<!-- apps/frontend/modules/category/templates/showSuccess.atom.php --><?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom"> <title>Jobeet (<?php echo $category ?>)</title> <subtitle>Latest Jobs</subtitle> <link href="<?php echo url_for('category', array('sf_subject' => $category, 'sf_format' => 'atom'), true) ?>" rel="self" /> <link href="<?php echo url_for('category', array('sf_subject' => $category), true) ?>" /> <updated><?php echo gmstrftime('%Y-%m-%dT%H:%M:%SZ', $category->getLatestPost()->getCreatedAt('U')) ?></updated> <author> <name>Jobeet</name> </author> <id><?php echo sha1(url_for('category', array('sf_subject' => $category), true)) ?></id>

  <?php include_partial('job/list', array('jobs' => $pager->getResults())) ?></feed>

Như job feed, chúng ta cần ngày của công việc mới nhất trong một category:

// lib/model/JobeetCategory.phpclass JobeetCategory extends BaseJobeetCategory{ public function getLatestPost() { $jobs = $this->getActiveJobs(1);  return $jobs[0]; }  // ...}

Hẹn gặp lại ngày mai

Như nhiều tính năng khác của symfony, việc hỗ trợ sẵn các định dạng cho phép bạn thêm các feed cho website của mình mà không tốn nhiều công sức.

Hôm nay, chúng ta đã bổ sung thêm tính năng giúp người dùng dễ dàng cập nhật các công việc. Ngày mai, chúng ta sẽ nó về Web Service để cung cấp công cụ cho các job poster.

Tóm tắt

Với tính năng feed được thêm vào hôm qua, người dùng bây giờ đã có thể theo dõi được công việc ngay khi nó được đưa lên.

Đối với nhà tuyển dụng, khi đưa một tuyển dụng lên, họ luôn muốn càng nhiều người biết đến càng tốt. Nếu công việc đó được đăng trên nhiều website, thì họ sẽ có nhiều cơ hội hơn để tìm đúng người. Đó chính là sức mạnh của long tail. Affiliates có thể đưa những tuyển dụng mới nhất lên websites của họ nhờ tính năng web service chúng ta sẽ phát triển trong ngày hôm nay.

Affiliates

Yêu cầu trong ngày 2:

"Story F7: Một affiliate có thể nhận danh sách các công việc mới nhất"

Fixture

Ta tạo một file fixture mới cho affiliates:

// data/fixtures/030_affiliates.ymlJobeetAffiliate: sensio_labs: url: http://www.sensio-labs.com/ email: [email protected] is_active: true token: sensio_labs jobeet_category_affiliates: [programming]  symfony: url: http://www.symfony-project.org/ email: [email protected] is_active: false token: symfony jobeet_category_affiliates: [design, programming]

Creating records for the middle table of a many-to-many relationship is as simple as defining an array with a key of the middle table name plus an s.

Trong file fixture, giá trị của token được định sẵn để thuận tiện cho việc test, nhưng trong thực tế, ta cần tạo token:

// lib/model/JobeetAffiliate.php

class JobeetAffiliate extends BaseJobeetAffiliate{ public function save(PropelPDO $con = null) { if (!$this->getToken()) { $this->setToken(sha1($this->getEmail().rand(11111, 99999))); }  return parent::save($con); }  // ...}

Bây giờ, bạn có thể nạp lại dữ liệu:

$ php symfony propel:data-load

Job Web Service

Khi bạn tạo một resource mới, đầu tiên hãy xác định URL (đó là một thói quen tốt):

// apps/frontend/config/routing.ymlapi_jobs: url: /api/:token/jobs.:sf_format class: sfPropelRoute param: { module: api, action: list } options: { model: JobeetJob, type: list, method: getForToken } requirements: sf_format: (?:xml|json|yaml)

Với route này, biến sf_format ở cuối URL và nhận một trong các giá trị xml, json, hoặc yaml.

Phương thức getForToken() sẽ được gọi khi action nhận tập các object dựa vào route:

// lib/model/JobeetJobPeer.phpclass JobeetJobPeer extends BaseJobeetJobPeer{ static public function getForToken(array $parameters) { $affiliate = JobeetAffiliatePeer::getByToken($parameters['token']); if (!$affiliate || !$affiliate->getIsActive()) { throw new sfError404Exception(sprintf('Affiliate with token "%s" does not exist or is not activated.', $parameters['token'])); }  return $affiliate->getActiveJobs(); }  // ...

}

Nếu token không tồn tại trong database, bạn sẽ được chuyển sang sfError404Exception exception. Lớp exception này tự động trả về một trang 404. Đó là cách đơn giản nhất để tạo một trang 404 từ một model class.

Phương thức getForToken() sử dụng 2 phương thức mới.

Phương thức đầu tiên là getByToken() lấy một affiliate dựa vào token:

// lib/model/JobeetAffiliatePeer.phpclass JobeetAffiliatePeer extends BaseJobeetAffiliatePeer{ static public function getByToken($token) { $criteria = new Criteria(); $criteria->add(self::TOKEN, $token);  return self::doSelectOne($criteria); }}

Sau đó, phương thức getActiveJobs() trả về danh sách các công việc mới nhất:

// lib/model/JobeetAffiliate.phpclass JobeetAffiliate extends BaseJobeetAffiliate{ public function getActiveJobs() { $cas = $this->getJobeetCategoryAffiliates(); $categories = array(); foreach ($cas as $ca) { $categories[] = $ca->getCategoryId(); }  $criteria = new Criteria(); $criteria->add(JobeetJobPeer::CATEGORY_ID, $categories, Criteria::IN); JobeetJobPeer::addActiveJobsCriteria($criteria);  return JobeetJobPeer::doSelect($criteria); }  // ...}

Cuối cùng, tạo module api với lệnh generate:module:

$ php symfony generate:module frontend api

Action

Tất cả các format đều dùng chung action list:

// apps/frontend/modules/api/actions/actions.class.phppublic function executeList(sfWebRequest $request){ $this->jobs = array(); foreach ($this->getRoute()->getObjects() as $job) { $this->jobs[$this->generateUrl('job_show_user', $job, true)] = $job->asArray($request->getHost()); }}

Thay vì cung cấp mảng các object JobeetJob cho template, chúng ta cung cấp mảng các string. Do chúng ta có 3 template khác nhau cho cùng một action, nên quá trình lấy giá trị được đưa riêng vào phương thức JobeetJob::asArray():

// lib/model/JobeetJob.phpclass JobeetJob extends BaseJobeetJob{ public function asArray($host) { return array( 'category' => $this->getJobeetCategory()->getName(), 'type' => $this->getType(), 'company' => $this->getCompany(), 'logo' => $this->getLogo() ? 'http://'.$host.'/uploads/jobs/'.$this->getLogo() : null, 'url' => $this->getUrl(), 'position' => $this->getPosition(), 'location' => $this->getLocation(), 'description' => $this->getDescription(), 'how_to_apply' => $this->getHowToApply(), 'expires_at' => $this->getCreatedAt('c'), ); }

xml Format

Việc hỗ trợ xml format đơn giản là tạo một template:

<!-- apps/frontend/modules/api/templates/listSuccess.xml.php --><?xml version="1.0" encoding="utf-8"?><jobs><?php foreach ($jobs as $url => $job): ?> <job url="<?php echo $url ?>"><?php foreach ($job as $key => $value): ?> <<?php echo $key ?>><?php echo $value ?></<?php echo $key ?>><?php endforeach; ?> </job><?php endforeach; ?></jobs>

json Format

JSON format cũng tương tự:

<!-- apps/frontend/modules/api/templates/listSuccess.json.php -->[<?php $nb = count($jobs); $i = 0; foreach ($jobs as $url => $job): ++$i ?>{ "url": "<?php echo $url ?>",<?php $nb1 = count($job); $j = 0; foreach ($job as $key => $value): ++$j ?> "<?php echo $key ?>": <?php echo json_encode($value).($nb1 == $j ? '' : ',') ?> <?php endforeach; ?>}<?php echo $nb == $i ? '' : ',' ?> <?php endforeach; ?>]

yaml Format

Với những format được hỗ trợ, symfony tự động thực hiện một số cấu hình như thay đổi content type, hay disable layout.

Do YAML format không nằm trong số đó, nên ta cần đổi content type trả về và disable layout trong action:

class apiActions extends sfActions{ public function executeList(sfWebRequest $request) { $this->jobs = array(); foreach ($this->getRoute()->getObjects() as $job) { $this->jobs[$this->generateUrl('job_show_user', $job, true)] = $job->asArray($request->getHost()); }  switch ($request->getRequestFormat()) { case 'yaml': $this->setLayout(false); $this->getResponse()->setContentType('text/yaml'); break; } }}

Trong một action, phương thức setLayout() thay đổi layout mặc định hoặc disable nó khi được set là false.

Template cho YAML như sau:

<!-- apps/frontend/modules/api/templates/listSuccess.yaml.php --><?php foreach ($jobs as $url => $job): ?>- url: <?php echo $url ?> <?php foreach ($job as $key => $value): ?> <?php echo $key ?>: <?php echo sfYaml::dump($value) ?> <?php endforeach; ?><?php endforeach; ?>

Nếu bạn thử gọi một web service với token không hợp lệ, bạn sẽ nhận được trang 404 XML với XML format, và trang 404 JSON với JSON format. Nhưng với YAML format, symfony không biết cách render trang này như thế nào.

Mỗi khi tạo một format, bạn cần tạo một error template. Template này sẽ được dùng cho trang 404, và tất cả các exception khác.

Do exception có thể khác nhau trong môi trường production và development, nên ta cần 2 file (config/error/exception.yaml.php để debug, và config/error/error.yaml.php cho production):

// config/error/exception.yaml.php<?php echo sfYaml::dump(array( 'error' => array( 'code' => $code, 'message' => $message, 'debug' => array( 'name' => $name, 'message' => $message, 'traces' => $traces, ),)), 4) ?> // config/error/error.yaml.php<?php echo sfYaml::dump(array( 'error' => array( 'code' => $code, 'message' => $message,))) ?>

Trước khi chạy thử, bạn cần tạo một layout cho YAML format:

// apps/frontend/templates/layout.yaml.php<?php echo $sf_content ?>

Thay thế trang 404 error và exception có sẵn bằng cách tạo file trong thư mục config/error/.

Web Service Test

Để test web service, copy affiliate fixture từ data/fixtures/ vào thư mục test/fixtures/ và thay thế nội dung của file apiActionsTest.php bằng đoạn sau:

// test/functional/frontend/apiActionsTest.phpinclude(dirname(__FILE__).'/../../bootstrap/functional.php'); $browser = new JobeetTestFunctional(new sfBrowser());$browser->loadData(); $browser-> info('1 - Web service security')->  info(' 1.1 - A token is needed to access the service')-> get('/api/foo/jobs.xml')-> with('response')->isStatusCode(404)->  info(' 1.2 - An inactive account cannot access the web service')-> get('/api/symfony/jobs.xml')-> with('response')->isStatusCode(404)->  info('2 - The jobs returned are limited to the categories configured for the affiliate')-> get('/api/sensio_labs/jobs.xml')-> with('request')->isFormat('xml')-> with('response')->checkElement('job', 33)->  info('3 - The web service supports the JSON format')-> get('/api/sensio_labs/jobs.json')-> with('request')->isFormat('json')-> with('response')->contains('"category": "Programming"')-> 

info('4 - The web service supports the YAML format')-> get('/api/sensio_labs/jobs.yaml')-> with('response')->begin()-> isHeader('content-type', 'text/yaml; charset=utf-8')-> contains('category: Programming')-> end();

Trong test này, có 2 phương thức mới:

isFormat(): kiểm tra format của request contains(): với format không phải HTML, nó kiểm tra xem nội dung trả về có

chứa đúng đoạn dữ liệu yêu cầu.

Affiliate Application Form

Bây giờ web service đã có thể sử dụng, ta cần form để tạo account cho affiliates. Chúng ta sẽ lặp lại các bước để thêm một tính năng mới cho ứng dụng.

Routing

Đầu tiên, chúng ta tạo Route:

// apps/frontend/config/routing.ymlaffiliate: class: sfPropelRouteCollection options: model: JobeetAffiliate actions: [new, create] object_actions: { wait: GET }

Đây là một Propel collection route quen thuộc với một option cấu hình mới: actions. Do chúng ta không cần cả 7 action tạo ra bởi route, optioon actions chỉ cho route biết chỉ dùng 2 action new và create. Route wait được thêm vào dùng để thông báo cho affiliate sau khi anh ta đăng kí.

Bootstrapping

Bước tiếp theo là tạo module:

$ php symfony propel:generate-module frontend affiliate JobeetAffiliate --non-verbose-templates

Template

Lệnh propel:generate-module tạo 7 action cơ bản và các template tương ứng. Trong thư mục templates/, ta xóa tất cả các file chỉ giữ lại _form.php và newSuccess.php. Và với những file giữ lại, thay thế nội dung của nó bằng đoạn code sau:

<!-- apps/frontend/modules/affiliate/templates/newSuccess.php --><?php use_stylesheet('job.css') ?> <h1>Become an Affiliate</h1> <?php include_partial('form', array('form' => $form)) ?> <!-- apps/frontend/modules/affiliate/templates/_form.php --><?php include_stylesheets_for_form($form) ?><?php include_javascripts_for_form($form) ?> <?php echo form_tag_for($form, 'affiliate') ?> <table id="job_form"> <tfoot> <tr> <td colspan="2"> <input type="submit" value="Submit" /> </td> </tr> </tfoot> <tbody> <?php echo $form ?> </tbody> </table></form>

Tạo waitSuccess.php template:

<!-- apps/frontend/modules/affiliate/templates/waitSuccess.php --><h1>Your affiliate account has been created</h1> <div style="padding: 20px"> Thank you! You will receive an email with your affiliate token as soon as your account will be activated.</div>

Cuối cùng, sửa lại link ở footer để chỉ đến module affiliate:

// apps/frontend/templates/layout.php<li class="last"><a href="<?php echo url_for('@affiliate_new') ?>">Become an affiliate</a></li>

Action

Ở đây, chúng ta chỉ sử dụng form để tạo account, do đó trong file actions.class.php chúng ta xóa hết các phương thức khác, chỉ để lại executeNew(), executeCreate(), và processForm().

Với action processForm(), sửa lại redirect URL tới action wait:

// apps/frontend/modules/affiliate/actions/actions.class.php

$this->redirect($this->generateUrl('affiliate_wait', $jobeet_affiliate));

Action wait không có gì do chúng ta không cần cung cấp gì cho template:

// apps/frontend/modules/affiliate/actions/actions.class.phppublic function executeWait(){}

Affiliate không được tạo token, hay tự kích hoạt tài khoản của mình. Do đó chúng ta cần thêm cấu hình vào file JobeetAffiliateForm:

// lib/form/JobeetAffiliateForm.class.phpclass JobeetAffiliateForm extends BaseJobeetAffiliateForm{ public function configure() { unset($this['is_active'], $this['token'], $this['created_at']); $this->widgetSchema['jobeet_category_affiliate_list']->setOption('expanded', true); $this->widgetSchema['jobeet_category_affiliate_list']->setLabel('Categories');  $this->validatorSchema['jobeet_category_affiliate_list']->setOption('required', true);  $this->widgetSchema['url']->setLabel('Your website URL'); $this->widgetSchema['url']->setAttribute('size', 50);  $this->widgetSchema['email']->setAttribute('size', 50);  $this->validatorSchema['email'] = new sfValidatorEmail(array('required' => true)); }}

Form framework hỗ trợ quan hệ nhiều-nhiều. Mặc định, một quan hệ được renderer ở dạng drop-down box nhờ có sfWidgetFormChoice widget. Trong ngày 10, chúng ta đã biết cách thay đổi rendered tag thông qua option expanded.

Do emails và URLs có thể dài hơn kích thước mặc định của input tag, nên ta set HTML attributes thông qua phương thức setAttribute().

Test

Cuối cùng, ta viết một số functional test cho tính năng mới:

// test/functional/frontend/affiliateActionsTest.phpinclude(dirname(__FILE__).'/../../bootstrap/functional.php'); $browser = new JobeetTestFunctional(new sfBrowser());$browser->loadData(); $browser-> info('1 - An affiliate can create an account')->  get('/affiliate/new')-> click('Submit', array('jobeet_affiliate' => array( 'url' => 'http://www.example.com/', 'email' => '[email protected]', 'jobeet_category_affiliate_list' => array($browser->getProgrammingCategory()->getId()), )))-> isRedirected()->

followRedirect()-> with('response')->checkElement('#content h1', 'Your affiliate account has been created')->  info('2 - An affiliate must at leat select one category')->  get('/affiliate/new')-> click('Submit', array('jobeet_affiliate' => array( 'url' => 'http://www.example.com/', 'email' => '[email protected]', )))-> with('form')->isError('jobeet_category_affiliate_list');

To simulate selecting checkboxes, pass an array of identifiers to check. To simplify the task, a new getProgrammingCategory() method has been created in the JobeetTestFunctional class:

// lib/model/JobeetTestFunctional.class.phpclass JobeetTestFunctional extends sfTestFunctional{ public function getProgrammingCategory() { $criteria = new Criteria(); $criteria->add(JobeetCategoryPeer::SLUG, 'programming');  return JobeetCategoryPeer::doSelectOne($criteria); }  // ...}

But as we already have this code in the getMostRecentProgrammingJob() method, it is time to refactor the code and create a getForSlug() method in JobeetCategoryPeer:

// lib/model/JobeetCategoryPeer.phpstatic public function getForSlug($slug){ $criteria = new Criteria(); $criteria->add(self::SLUG, $slug);  return self::doSelectOne($criteria);}

Then, replace the two occurrences of this code in JobeetTestFunctional.

Affiliate Backend

Với backend, ta cần tạo một module affiliate để admin kích hoạt các tài khoản affiliate:

$ php symfony propel:generate-admin backend JobeetAffiliate --module=affiliate

Để truy cập module vừa tạo, thêm một link vào main menu kèm với số affiliate cần kích hoạt:

<!-- apps/backend/templates/layout.php --><li> <a href="<?php echo url_for('@jobeet_affiliate') ?>"> Affiliates - <strong><?php echo JobeetAffiliatePeer::countToBeActivated() ?></strong> </a></li> // lib/model/JobeetAffiliatePeer.phpclass JobeetAffiliatePeer extends BaseJobeetAffiliatePeer{ static public function countToBeActivated() { $criteria = new Criteria(); $criteria->add(self::IS_ACTIVE, 0);  return self::doCount($criteria); }

Do chỉ có một action trong backend là kích hoạt hoặc tạm dừng hoạt động của tài khoản, do đó ta cần sửa lại một chút trong mục config và thêm một link để kích hoạt tài khoản trực tiếp từ danh sách các tài khoản:

# apps/backend/modules/affiliate/config/generator.ymlconfig: fields: is_active: { label: Active? } list: title: Affiliate Management display: [is_active, url, email, token] sort: [is_active] object_actions: activate: ~ deactivate: ~ batch_actions: activate: ~ deactivate: ~ actions: {} filter: display: [url, email, is_active]

Sửa lại filter mặc định:

// apps/backend/modules/affiliate/lib/affiliateGeneratorConfiguration.class.phpclass affiliateGeneratorConfiguration extends BaseAffiliateGeneratorConfiguration

{ public function getFilterDefaults() { return array('is_active' => '0'); }}

Ta chỉ phải viết code cho action activate, deactivate:

// apps/backend/modules/affiliate/actions/actions.class.phpclass affiliateActions extends autoAffiliateActions{ public function executeListActivate() { $this->getRoute()->getObject()->activate();  $this->redirect('@jobeet_affiliate'); }  public function executeListDeactivate() { $this->getRoute()->getObject()->deactivate();  $this->redirect('@jobeet_affiliate'); }  public function executeBatchActivate(sfWebRequest $request) { $affiliates = JobeetAffiliatePeer::retrieveByPks($request->getParameter('ids'));  foreach ($affiliates as $affiliate) { $affiliate->activate(); }  $this->redirect('@jobeet_affiliate'); }  public function executeBatchDeactivate(sfWebRequest $request) { $affiliates = JobeetAffiliatePeer::retrieveByPks($request->getParameter('ids'));  foreach ($affiliates as $affiliate) { $affiliate->deactivate(); }  $this->redirect('@jobeet_affiliate'); }} // lib/model/JobeetAffiliate.phpclass JobeetAffiliate extends BaseJobeetAffiliate{

public function activate() { $this->setIsActive(true);  return $this->save(); }  public function deactivate() { $this->setIsActive(false);  return $this->save(); }  // ...}

Gửi Email

Khi một tài khoản affiliate được kích hoạt bởi administrator, một email sẽ được gửi tới affiliate để xác nhận và gửi cho anh ta token.

PHP có một số thư viện thực hiện việc gửi email như SwiftMailer, Zend_Mail, và ezcMail. Do chúng ta sẽ sử dụng một thư viện khác của Zend Framework trong những ngày tới, nên chúng ta sẽ sử dụng Zend_Mail để gửi email.

Cài đặt và cấu hình Zend Framework

Thư viện Zend Mail là một phần của Zend Framework. Do chúng ta không cần tất cả mọi thứ của Zend Framework, nên chúng ta chỉ cài đặt những thứ cần thiết trong thư mục lib/vendor/, nằm cùng với symfony framework.

Đầu tiên, download Zend Framework và giải nén ta được thư mục lib/vendor/Zend/. Bạn có thể xóa mọi file và thư mục không liên quan, chỉ giữ lại:

Exception.php Loader/ Loader.php Mail/ Mail.php Mime/ Mime.php Search/

Thư mục Search/ không cần thiết cho việc gửi email nhưng sẽ cần cho hướng dẫn ngày mai.

Sau đó, thêm đoạn code sau vào lớp ProjectConfiguration để đăng kí Zend autoloader:

// config/ProjectConfiguration.class.phpclass ProjectConfiguration extends sfProjectConfiguration{ static protected $zendLoaded = false;  static public function registerZend() { if (self::$zendLoaded) { return; }  set_include_path(sfConfig::get('sf_lib_dir').'/vendor'.PATH_SEPARATOR.get_include_path()); require_once sfConfig::get('sf_lib_dir').'/vendor/Zend/Loader.php'; Zend_Loader::registerAutoload(); self::$zendLoaded = true; }  // ...}

Gửi Email

Sửa lại action activate:

// apps/backend/modules/affiliate/actions/actions.class.phpclass affiliateActions extends autoAffiliateActions{ public function executeListActivate() { $affiliate = $this->getRoute()->getObject(); $affiliate->activate();  // send an email to the affiliate ProjectConfiguration::registerZend(); $mail = new Zend_Mail(); $mail->setBodyText(<<<EOFYour Jobeet affiliate account has been activated. Your token is {$affiliate->getToken()}. The Jobeet Bot.EOF); $mail->setFrom('[email protected]', 'Jobeet Bot'); $mail->addTo($affiliate->getEmail()); $mail->setSubject('Jobeet affiliate token'); $mail->send();  $this->redirect('@jobeet_affiliate'); }  // ...}

Để code có thể hoạt động, bạn cần sửa [email protected] thành một địa chỉ email thật.

Hướng dẫn đầy đủ về thư viện Zend_Mail có thể tìm ở Zend Framework website.

Hẹn gặp lại ngày mai

Nhờ có kiến trúc REST của symfony, việc tạo một web service cho project của bạn trở nên đơn giản. Mặc dù hôm nay chúng ta chỉ tạo ra một read-only web service, nhưng bạn cũng đã đủ kiến thức về symfony để tạo ra một read-write web service.

Việc tạo tài khoản affiliate ở frontend và quản lý ở backend giờ đây trở nên thật dễ dàng do bạn đã quen với cách tạo một tính năng mới cho project của mình.

Nếu bạn còn nhớ yêu cầu ở ngày 2:

"Affiliate có thể giới hạn số công việc trả về, và chỉ lấy về một category nào đó."

Việc thực hiện yêu cầu này thật đơn giản và chúng tôi sẽ dành việc này cho bạn.

Ngày mai, chúng ta sẽ xây dựng tính năng duy nhất còn thiếu của Jobeet website, đó là search engine.

Hai ngày trước, chúng ta đã thêm tính năng feed để giúp người dùng luôn theo dõi được các công việc mới nhất. Hôm nay chúng ta sẽ tiếp tục đem đến sự tiện dụng cho người dùng bằng cách cung cấp tính năng chính cuối cùng của Jobeet website: search engine.

Công nghệ

Trước khi bắt đầu, hãy nói một chút về lịch sử của symfony. Chúng tôi luôn ủng hộ nhiều best practices, như test và refactoring, và chúng tôi cũng luôn cố gắng đưa chúng vào trong framework. Chúng tôi rất thích khẩu hiệu "Đừng làm lại cái bánh xe (Don't reinvent the wheel)". Trên thực tế, symfony framework được bắt đầu 4 năm trước đây như một sự kết hợp từ 2 Open-Source software: Mojavi và Propel. Và mỗi khi chúng tôi cần giải quyết một vấn đề mới, chúng tôi luôn tìm kiếm một thư viện có sẵn thực hiện tốt công việc đó trước khi bắt đầu code chúng từ đầu.

Hôm nay, chúng ta muốn thêm một search engine cho Jobeet, và Zend Framework đã cung cấp một thư viện tuyệt vời, đó là Zend Lucene, dựa trên Java Lucene project. Thay vì tạo một search engine khác cho Jobeet, đó thực sự là một công việc phức tạp, chúng ta sẽ sử dụng Zend Lucene.

Ở trang Zend Lucene, thư viện được mô tả như sau:

... một text search engine được viết trên PHP 5. Nó chứa các index trên file và không yêu cầu một database server, nên có thể search trên bất kì PHP-driven website nào. Zend_Search_Lucene hỗ trợ những tính năng sau:

Ranked searching - hiển thị kết quả phù hợp nhất lên trên Hỗ trợ nhiều kiểu truy vấn: phrase query, boolean query, wildcard query,

proximity query, range query, ... Search theo field cụ thể (ví dụ. title, author, contents)

Chương này không có mục đích hướng dẫn sử dụng thư viện Zend Lucene, mà là cách tương tác với thư viện này trong Jobeet website; hay rộng hơn, là cách tương tác với các thư viện ngoài trong symfony project. Nếu bạn muốn tìm hiểu nhiều hơn về Zend Lucene, hãy tham khảo ở Zend Lucene documentation.

Zend Lucene đã được cài đặt ngày hôm qua trong khi chúng ta thực hiện việc gửi email.

Indexing

Jobeet search engine cần trả về tất cả các công việc hợp với từ khóa nhập bởi người dùng. Trước khi có thể search, chúng ta cần tạo index cho các công việc; với Jobeet, nó được chứa trong thư mục data/.

Zend Lucene cung cấp 2 phương thức để nhận một index phụ thuộc vào nó đã có hay chưa. Ta tạo một phương thức trả về index đã có hoặc tạo mới nếu chưa có:

// lib/model/JobeetJobPeer.phpstatic public function getLuceneIndex(){ ProjectConfiguration::registerZend();  if (file_exists($index = self::getLuceneIndexFile())) { return Zend_Search_Lucene::open($index); } else { return Zend_Search_Lucene::create($index); }} static public function getLuceneIndexFile(){ return sfConfig::get('sf_data_dir').'/job.'.sfConfig::get('sf_environment').'.index';}

Mỗi khi công việc được tạo, sửa hay xóa, index cũng phải được cập nhật.

Phương thức save()

Sửa lại JobeetJob để cập nhật index mỗi khi một công việc được lưu vào database:

// lib/model/JobeetJob.phppublic function save(PropelPDO $con = null){ // ...  $ret = parent::save($con);  $this->updateLuceneIndex();  return $ret;}

Và tạo phương thức updateLuceneIndex() thực hiện việc cập nhật:

// lib/model/JobeetJob.phppublic function updateLuceneIndex(){ $index = JobeetJobPeer::getLuceneIndex();

  // remove an existing entry if ($hit = $index->find('pk:'.$this->getId())) { $index->delete($hit->id); }  // don't index expired and non-activated jobs if ($this->isExpired() || !$this->getIsActivated()) { return; }  $doc = new Zend_Search_Lucene_Document();  // store job primary key URL to identify it in the search results $doc->addField(Zend_Search_Lucene_Field::UnIndexed('pk', $this->getId()));  // index job fields $doc->addField(Zend_Search_Lucene_Field::UnStored('position', $this->getPosition(), 'utf-8')); $doc->addField(Zend_Search_Lucene_Field::UnStored('company', $this->getCompany(), 'utf-8')); $doc->addField(Zend_Search_Lucene_Field::UnStored('location', $this->getLocation(), 'utf-8')); $doc->addField(Zend_Search_Lucene_Field::UnStored('description', $this->getDescription(), 'utf-8'));  // add job to the index $index->addDocument($doc); $index->commit();}

Do Zend Lucene không thể cập nhật một entry đã có, nên nếu công việc đã được index thì trước tiên ta cần remove nó.

Index cho công việc đơn giản là lưu khóa chính dùng để tham chiếu khi tìm kiếm và index các cột chính (position, company, location, và description) nhưng không chứa nó trong index vì chúng ta sẽ sử dụng các object thực để hiển thị kết quả.

Propel Transaction

Điều gì xảy ra nếu có lỗi trong khi index công việc hay công việc chưa được lưu vào database? Cả Propel và Zend Lucene sẽ hiện ra một exception. Nhưng trong một số trường hợp, chúng ta sẽ có công việc được lưu vào database mà không được index. Để ngăn ngừa điều đó xảy ra, chúng ta có thể đưa 2 cập nhật này vào trong một transaction và rollback trong trường hợp có lỗi:

// lib/model/JobeetJob.phppublic function save(PropelPDO $con = null){ // ...

  if (is_null($con)) { $con = Propel::getConnection(JobeetJobPeer::DATABASE_NAME, Propel::CONNECTION_WRITE); }  $con->beginTransaction(); try { $ret = parent::save($con);  $this->updateLuceneIndex();  $con->commit();  return $ret; } catch (Exception $e) { $con->rollBack(); throw $e; }}

delete()

Chúng ta cũng cần override phương thức delete() để xóa entry trong index của công việc bị xóa:

// lib/model/JobeetJob.phppublic function delete(PropelPDO $con = null){ $index = JobeetJobPeer::getLuceneIndex();  if ($hit = $index->find('pk:'.$this->getId())) { $index->delete($hit->id); }  return parent::delete($con);}

Mass delete

Whenever you load the fixtures with the propel:data-load task, symfony removes all the existing job records by calling the JobeetJobPeer::doDeleteAll() method. Let's override the default behavior to also delete the index altogether:

// lib/model/JobeetJobPeer.phppublic static function doDeleteAll($con = null){ if (file_exists($index = self::getLuceneIndexFile())) {

sfToolkit::clearDirectory($index); rmdir($index); }  return parent::doDeleteAll($con);}

Searching

Bây giờ bạn cần nạp lại fixture data để index chúng:

$ php symfony propel:data-load --env=dev

Lệnh với option --env sẽ index cho môi trường tương ứng và môi trường mặc định của lệnh là cli.

Với người dùng Unix: do index sẽ bị chỉnh sửa từ dòng lệnh hay từ web, nên bạn cần sửa lại quyền của thư mục index tùy vào cấu hình của bạn: kiểm tra để chắc rằng cả người thực hiện dòng lệnh và người dùng web server đều có quyền ghi vào thư mục index.

Thực hiện việc search bây giờ trở nên thật dễ dàng. Đầu tiên, hãy tạo một route:

job_search: url: /search param: { module: job, action: search }

Và action tương ứng:

// apps/frontend/modules/job/actions/actions.class.phpclass jobActions extends sfActions{ public function executeSearch(sfWebRequest $request) { if (!$query = $request->getParameter('query')) { return $this->forward('job', 'index'); }  $this->jobs = JobeetJobPeer::getForLuceneQuery($query); }  // ...}

Template cũng rất đơn giản:

// apps/frontend/modules/job/templates/searchSuccess.php<?php use_stylesheet('jobs.css') ?> <div id="jobs"> <?php include_partial('job/list', array('jobs' => $jobs)) ?>

</div>

Việc search do phương thức getForLuceneQuery() thực hiện:

// lib/model/JobeetJobPeer.phpstatic public function getForLuceneQuery($query){ $hits = self::getLuceneIndex()->find($query);  $pks = array(); foreach ($hits as $hit) { $pks[] = $hit->pk; }  $criteria = new Criteria(); $criteria->add(self::ID, $pks, Criteria::IN); $criteria->setLimit(20);  return self::doSelect(self::addActiveJobsCriteria($criteria));}

Sau khi đã nhận tất cả các kết quả từ Lucene index, chúng ta bỏ đi các công việc đã hết hạn, và giới hạn kết quả là 20.

Để tính năng có thể hoạt động, sửa lại layout:

// apps/frontend/templates/layout.php<h2>Ask for a job</h2><form action="<?php echo url_for('@job_search') ?>" method="get"> <input type="text" name="query" value="<?php echo isset($query) ? $query : '' ?>" id="search_keywords" /> <input type="submit" value="search" /> <div class="help"> Enter some keywords (city, country, position, ...) </div></form>

Zend Lucene hỗ trợ câu truy vấn sử dụng Booleans, wildcards, fuzzy search, .... Tất cả đều được mô tả trong Zend Lucene manual

Unit Test

Chúng ta sẽ tạo ra unit test như thế nào để thực hiện việc test search engine? Rõ ràng, chúng ta sẽ không test thư viện Zend Lucene, mà là sự tương tác của nó với lớp JobeetJob class.

Thêm các test sau vào cuối file JobeetJobTest.php và đừng quên cập nhật lại số test ở đầu file:

// test/unit/model/JobeetJobTest.php

$t->comment('->getForLuceneQuery()');$job = create_job(array('position' => 'foobar', 'is_activated' => false));$job->save();$jobs = JobeetJobPeer::getForLuceneQuery('position:foobar');$t->is(count($jobs), 0, '::getForLuceneQuery() does not return non activated jobs'); $job = create_job(array('position' => 'foobar', 'is_activated' => true));$job->save();$jobs = JobeetJobPeer::getForLuceneQuery('position:foobar');$t->is(count($jobs), 1, '::getForLuceneQuery() returns jobs matching the criteria');$t->is($jobs[0]->getId(), $job->getId(), '::getForLuceneQuery() returns jobs matching the criteria'); $job->delete();$jobs = JobeetJobPeer::getForLuceneQuery('position:foobar');$t->is(count($jobs), 0, '::getForLuceneQuery() does not return delete jobs');

Chúng ta kiểm tra rằng một công việc chưa được kích hoạt, hoặc một công việc bị xóa sẽ không được hiển thị ở kết quả tìm kiếm; chúng ta cũng kiểm tra xem kết quả trả về có đúng với công việc chúng ta cần tìm.

Task

Cuối cùng, chúng ta cần tạo một task để xóa các entry cũ khỏi index (ví dụ khi một công việc hết hạn) và tối ưu index. Chúng ta đã có một cleanup task, hãy sửa lại và thêm những tính năng sau:

// lib/task/JobeetCleanupTask.class.phpprotected function execute($arguments = array(), $options = array()){ $databaseManager = new sfDatabaseManager($this->configuration);  // cleanup Lucene index $index = JobeetJobPeer::getLuceneIndex();  $criteria = new Criteria(); $criteria->add(JobeetJobPeer::EXPIRES_AT, time(), Criteria::LESS_THAN); $jobs = JobeetJobPeer::doSelect($criteria); foreach ($jobs as $job) { if ($hit = $index->find('pk:'.$job->getId())) { $hit->delete(); } }  $index->optimize(); 

$this->logSection('lucene', 'Cleaned up and optimized the job index');  // Remove stale jobs $nb = JobeetJobPeer::cleanup($options['days']);  $this->logSection('propel', sprintf('Removed %d stale jobs', $nb));}

Task xóa tất cả các công việc quá hạn khỏi index và tối ưu lại index nhờ có phương thức optimize() của Zend Lucene.

Hẹn gặp lại ngày mai

Hôm nay chúng ta đã tạo một search engine hoàn chỉnh với nhiều tính năng chỉ trong một giờ. Mỗi khi bạn thêm một tính năng mới cho dự án của mình, hãy kiểm tra xem nó đã được giải quyết ở đâu đó chưa. Đầu tiên, hãy kiểm tra xem nó đã có sẵn trong symfony framework hay chưa. Sau đó kiểm tra trong các symfony plugin. Và cũng đừng quên tìm trong Zend Framework libraries và ezComponent.

Ngày mai, chúng ta sẽ sử dụng unobtrusive JavaScripts cho search engine để hiển thị kết quả ngay khi người dùng gõ trong ô tìm kiếm. Tất nhiên, đó cũng là dịp để nói về cách sử dụng AJAX trong symfony.

Hôm qua, chúng ta xây dựng một search engine cho Jobeet sử dụng thư viện Zend Lucene.

Ngày hôm nay, để nâng cao tính tương tác của search engine, chúng ta sẽ sử dụng AJAX trong search engine.

Do form có thể làm việc dù có JavaScript hay không, nên tính năng live search sẽ được xây dựng sử dụng unobtrusive JavaScript. Sử dụng unobtrusive JavaScript cũng cho phép phân tách code tốt hơn giữa HTML, CSS, và JavaScript behaviors.

Cài đặt jQuery

Thay vì làm lại cái bánh xe và đau đầu vì sự khác nhau giữa các trình duyệt, chúng ta sẽ sử dụng thư viện JavaScript jQuery. Framework symfony là độc lập và có thể làm việc với bất kì thư viện JavaScript nào.

Vào trang jQuery, download phiên bản mới nhất, và copy file .js vào thư mục web/js/.

Including jQuery

Do chúng ta cần jQuery ở tất cả các trang, nên ta thêm vào layout để include nó vào trong <head>. Chú ý rằng phương thức use_javascript() cần được dùng trước khi include_javascripts() được gọi:

<!-- apps/frontend/templates/layout.php -->  <?php use_javascript('jquery-1.2.6.min.js') ?> <?php include_javascripts() ?></head>

Chúng ta có thể include file jQuery trực tiếp với tag <script>, nhưng sử dụng helper use_javascript() sẽ đảm bảo rằng file JavaScript không được include 2 lần.

Thêm Behavior

Xây dựng một live search có nghĩa là mỗi khi người dùng gõ một kí tự vào search box, một lời gọi tớ server sẽ được trigger; sau đó server sẽ trả về các thông tin cần thiết để cập nhật vùng được chọn trên trang mà không cần refresh lại toàn bộ trang.

Thay vì thêm một behavior với on*() HTML attributes, nguyên tắc cơ bản của jQuery là thêm một behaviors vào DOM sau khi toàn bộ trang được load. Theo cách này, nếu bạn không cho phép JavaScript trong trình duyệt, không có behavior nào được register,, khi đó form sẽ làm việc như trước đây.

Đầu tiên xác định sự kiện khi người dùng gõ một kí tự vào search box:

$('#search_keywords').keyup(function(key) { if (this.value.length >= 3 || this.value == '') { // do something }});

Đừng thêm code ngay bây giờ, do chúng ta sẽ chỉnh sửa nó khá nhiều. JavaScript code hoàn chỉnh sẽ được thêm vào layout ở mục sau.

Mỗi khi người dùng gõ một kí tự, jQuery thực thi anonymous function xác định trong code ở trên, nhưng chỉ khi người dùng gõ nhiều hơn 3 kí tự hoặc nếu anh ta xóa mọi thứ trong ô nhập.

Tạo một AJAX gọi tới server đơn giản là sử dụng phương thức load() trên DOM element:

$('#search_keywords').keyup(function(key) { if (this.value.length >= 3 || this.value == '') { $('#jobs').load( '<?php echo url_for('@job_search') ?>', { query: this.value + '*' } } ); }});

Lời gọi AJAX sẽ vẫn được thực thi bởi action đó. Sự thay đổi trong action sẽ được tiến hành ở mục sau.

Đầu tiên, ta cần bỏ nút search nếu JavaScript được enable:

$('.search input[type="submit"]').hide();

User Feedback

Mỗi khi có một lời gọi AJAX, trang sẽ không được cập nhật ngay lập tức. Trình duyệt sẽ đợi server trả lời rồi mới cập nhật lại trang. Trong thời gian đó, bạn cần cung cấp một visual feedback cho người dùng để thông báo cho anh ta biết là quá trình đang được thực hiện.

Thông thường sẽ hiển thị một loader icon trong một lời gọi AJAX. Thêm vào layout một loader image và mặc định để ẩn:

// apps/frontend/templates/layout.php<div class="search"> <h2>Ask for a job</h2> <form action="<?php echo url_for('@job_search') ?>" method="get"> <input type="text" name="query" value="<?php echo $sf_request->getParameter('query') ?>" id="search_keywords" /> <input type="submit" value="search" /> <img id="loader" src="/images/loader.gif" style="vertical-align: middle; display: none" /> <div class="help"> Enter some keywords (city, country, position, ...) </div> </form></div>

Bạn có thể download loader image trong kho chứa ngày hôm nay.

Loader được chỉnh sửa để phù hợp với giao diện hiện tại của Jobeet. Nếu bạn muốn tự tạo cho riêng mình, bạn có thể tìm nhiều dịch vụ online miễn phí như http://www.ajaxload.info/.

Bây giờ bạn đã có tất cả những thứ cần thiết để làm cho HTML hoạt động, mở file layout và thêm đoạn code JavaScript sau vào cuối của mục<head>:

// apps/frontend/templates/layout.php<script type="text/javascript"> $(document).ready(function() { $('.search input[type="submit"]').hide();  $('#search_keywords').keyup(function(key) { if (this.value.length >= 3 || this.value == '') { $('#loader').show();

$('#jobs').load( '<?php echo url_for('@job_search') ?>', { query: this.value + '*' }, function() { $('#loader').hide(); } ); } }); });</script>

AJAX in an Action

Nếu JavaScript được enable, jQuery sẽ theo theo dõi tất cả các từ nhập vào trong search box, và gọi search action. Nếu không, search action đó cũng sẽ được gọi khi người dùng submit form bằng cách ấn phím "enter" hoặc click vào nút "search".

Vì thế, bây giờ search action cần biết lời gọi là tạo bởi AJAX hay không. Mỗi khi một request được tạo bởi một AJAX call, phương thức isXmlHttpRequest() của request object trả về true.

Phương thức isXmlHttpRequest() hoạt động với tất cả các thư viện JavaScript phổ biến như Prototype, Mootools, hay jQuery.

// apps/frontend/modules/job/actions/actions.class.phppublic function executeSearch(sfWebRequest $request){ if (!$query = $request->getParameter('query')) { return $this->forward('job', 'index'); }  $this->jobs = JobeetJobPeer::getForLuceneQuery($query);  if ($request->isXmlHttpRequest()) { return $this->renderPartial('job/list', array('jobs' => $this->jobs)); }}

jQuery không load lại trang mà chỉ thay thế DOM element #jobs bằng nội dung trả về, nội dung này sẽ không sử dụng layout. Do trường hợp này là phổ biến, nên mặc định layout sẽ được disable với một AJAX request.

Thêm vào đó, thay vì trả về toàn bộ template, chúng ta chỉ trả về nội dung của job/list partial. Phương thức renderPartial() được sử dụng trong action trả về partial thay vì toàn bộ template.

Nếu người dùng xóa tất cả các kí tự trong search box, hoặc không có kết quả, chúng ta cần hiển thị một thông báo thay vì hiển thị một nội dung rỗng. Chúng ta sẽ sử dụng phương thức renderText() để render một chuỗi đơn giản:

// apps/frontend/modules/job/actions/actions.class.phppublic function executeSearch(sfWebRequest $request){ if (!$query = $request->getParameter('query')) { return $this->forward('job', 'index'); }  $this->jobs = JobeetJobPeer::getForLuceneQuery($query);  if ($request->isXmlHttpRequest()) { if ('*' == $query || !$this->jobs) { return $this->renderText('No results.'); } else { return $this->renderPartial('job/list', array('jobs' => $this->jobs)); } }}

Bạn cũng có thể trả về một component bằng cách sử dụng phương thức renderComponent().

JavaScript as an Action

Đặt JavaScript trong trong thẻ <head> ở layout giúp Jobeet search engine trở nên thân thiện hơn, nhưng tốt hơn ta nên tạo một file để chứa nó. Nhưng do phần lớn JavaScripts tạo lời gọi AJAX đều cần một vài URLs, chúng cần sử dụng url_for() helper, nhờ đó mà chúng động hơn.

JavaScript là một định dạng không phải HTML, và như đã biết trong những ngày trước, symfony có thể quản lý các định dạng khác một cách dễ dàng. Do file JavaScript sẽ chứa toàn bộ behavior của trang, bạn có thể sử dụng URL của trang cho file JavaScript, nhưng kết thúc với .js. Ví dụ, nếu bạn muốn tạo một file cho search engine behavior, bạn có thể sửa lại job_search route như sau:

job_search: url: /search.:sf_format param: { module: job, action: search, sf_format: html } requirements: sf_format: (?:html|js)

Do URLs trên website là cố định, file JavaScript phần lớn là tĩnh, không thay đổi trong phần lớn thời gian. Nó thích hợp cho việc cache, như chúng ta sẽ thấy trong những ngày sau.

Testing AJAX

Do symfony browser không thể giả lập JavaScript, bạn cần giúp nó trong quá trình test AJAX call. Có nghĩa là bạn cần tự thêm header mà jQuery và tất cả các thư viện JavaScript khác sẽ gửi với request:

// test/functional/frontend/jobActionsTest.php$browser->setHttpHeader('X_REQUESTED_WITH', 'XMLHttpRequest');$browser-> info('5 - Live search')->  get('/search?query=sens*')-> with('response')->begin()-> checkElement('table tr', 3)-> end();

Phương thức setHttpHeader() thiết lập một HTTP header cho request tiếp theo của trình duyệt.

Hẹn gặp lại ngày mai

Hôm qua, chúng ta đã sử dụng thư viện Zend Lucene để xây dựng search engine. Hôm nay, chúng ta đã dùng jQuery để làm cho nó thân thiện hơn. Symfony framework cung cấp tất cả các công cụ cơ bản để xây dựng ứng dụng MVC một các dễ dàng, và nó cũng dễ dàng kết hợp với các thành phần khác. Do đó, hãy sử dụng công cụ tốt nhất cho công việc của bạn.

Ngày mai, chúng ta sẽ nói về việc internationalize Jobeet website.

Ngày hôm qua, chúng ta đã hoàn thành chức năng tìm kiếm bằng cách thêm vào một vài tiện ích AJAX.

Hôm nay, chúng ta sẽ nói về việc internationalization (i18n) và localization (l10n) ứng dụng Jobeet.

From Wikipedia:

Internationalization is the process of designing a software application so that it can be adapted to various languages and regions without engineering changes.

Localization is the process of adapting software for a specific region or language by adding locale-specific components and translating text.

Như thường lệ, symfony framework không "làm lại cái bánh xe" và hỗ trợ i18n và l10n dựa trên ICU standard.

User

Không thể internationalization mà không có user. Khi website của bạn có các lựa chọn ngôn ngữ khác nhau cho cac vùng trên thế giới, user sẽ chọn ngôn ngữ phù hợp với mình.

Chúng ta đã nói về class symfony User ở ngày 13.

User Culture

Tính năng i18n và l10n của symfony dựa trên user culture. Culture là sự kết hợp giữa ngôn ngữ và quốc gia của user. Ví dụ, culture của user nói tiếng Pháp (fr) và sống ở nước Pháp sẽ là fr_FR.

Bạn có thể quản lý user culture thông qua phương thức setCulture() và getCulture() của object User:

// in an action$this->getUser()->setCulture('fr_BE');echo $this->getUser()->getCulture();

Mã ngôn ngữ là 2 kí tự viết thường, theo tiêu chuẩn ISO 639-1, và mã nước là 2 kí tự viết hoa, theo tiêu chuẩn ISO 3166-1.

Culture mặc định

Culture mặc định được xác định trong file settings.yml:

# apps/frontend/config/settings.ymlall: .settings: default_culture: it_IT

Do culture được quản lý bởi object User, nên nó được chứa trong session. Trong quá trình phát triển, nếu bạn thay đổi culture mặc định, bạn cần phải xóa session cookie để thiết lập mới có hiệu quả.

Khi một user bắt đầu một session trên Jobeet website, chúng ta có thể quyết định culture phù hợp nhất, dựa trên thông tin cung cấp bởi Accept-Language HTTP header.

Phương thức getLanguages() của request object trả về mảng các ngôn ngữ phù hợp với user hiện tại, sắp xếp theo thứ tự ưu tiên:

// in an action$languages = $request->getLanguages();

Nhưng website của bạn thường không có hết 136 ngôn ngữ chính trên thế giới. Phương thức getPreferredCulture() trả về ngôn ngữ thích hợp nhất bằng cách so sánh các ngôn ngữ phù hợp với user với các ngôn ngữ mà website hỗ trợ:

// in an action$language = $request->getPreferredCulture(array('en', 'fr'));

Nếu không có ngôn ngữ nào phù hợp, ngôn ngữ đầu tiên trong mảng (ở đây là English) sẽ được chọn.

Culture trên URL

Website Jobeet hỗ trợ English và French. Do mỗi URL chỉ có thể tương ứng với một resource, nên culture phải được nhúng trong URL. Để thực hiện điều này, mở file routing.yml, và thêm biến :sf_culture vào tất cả routes trừ api_jobs và homepage. Để đơn giản route, ta thêm /:sf_culture vào trước của url. Với collection route, ta thêm một prefix_path option bắt đầu với /:sf_culture.

# apps/frontend/config/routing.ymlaffiliate: class: sfPropelRouteCollection options: model: JobeetAffiliate actions: [new, create] object_actions: { wait: get } prefix_path: /:sf_culture/affiliate category: url: /:sf_culture/category/:slug.:sf_format class: sfPropelRoute param: { module: category, action: show, sf_format: html } options: { model: JobeetCategory, type: object } requirements: sf_format: (?:html|atom) job_search: url: /:sf_culture/search param: { module: job, action: search } job: class: sfPropelRouteCollection options: model: JobeetJob column: token object_actions: { publish: put, extend: put } prefix_path: /:sf_culture/job requirements: token: \w+ job_show_user: url: /:sf_culture/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+ sf_method: get

Khi biến sf_culture được sử dụng ở route, symfony sẽ tự động sử dụng giá trị này để xác định culture của user.

Do chúng ta cần trang chủ hỗ trợ nhiều ngôn ngữ (/en/, /fr/, ...), nên trang chủ mặc định (/) phải được chuyển sang ngôn ngữ phù hợp với culture của user. Nhưng nếu user chưa xác định culture, do anh ta truy cập vào Jobeet lần đầu tiên, hệ thống sẽ tự chon culture thích hợp.

Thêm phương thức isFirstRequest() vào myUser, trả về true nếu là request đầu tiên của một user session:

// apps/frontend/lib/myUser.class.phppublic function isFirstRequest($boolean = null){ if (is_null($boolean)) { return $this->getAttribute('first_request', true); } else { $this->setAttribute('first_request', $boolean); }}

Thêm route localized_homepage :

# apps/frontend/config/routing.ymllocalized_homepage: url: /:sf_culture/ param: { module: job, action: index } requirements: sf_culture: (?:fr|en)

Sửa lại action index của module job để chuyển người dùng trang trang chủ với ngôn ngữ phù hợp:

// apps/frontend/modules/job/actions/actions.class.phppublic function executeIndex(sfWebRequest $request){ if (!$request->getParameter('sf_culture')) { if ($this->getUser()->isFirstRequest())

{ $culture = $request->getPreferredCulture(array('en', 'fr')); $this->getUser()->setCulture($culture); $this->getUser()->isFirstRequest(false); } else { $culture = $this->getUser()->getCulture(); }  $this->redirect('@localized_homepage'); }  $this->categories = JobeetCategoryPeer::getWithJobs();}

Nếu biến sf_culture chưa có trong request, có nghĩa là user truy cập vào trang web từ URL /. Trong trường hợp này và session là mới, culture thích hợp nhất sẽ được chọn. Ngược lại, culture hiện tại chứa trong session sẽ được sử dụng.

Cuối cùng, chuyển user tới URL localized_homepage. Chú ý rằng, ta ko cần cung cấp biến sf_culture, symfony sẽ tự động làm việc này cho bạn.

Khi truy cập URL /it/, symfony sẽ trả về một 404 error do chúng ta giới hạn biến sf_culture là en, hoặc fr. Thêm yêu cầu này vào tất cả các route có chứa culture:

requirements: sf_culture: (?:fr|en)

Test Culture

Bây giờ, đã đến lúc test những gì chúng ta đã làm. Nhưng trước khi thêm các test, chúng ta cần sửa lại các test cũ. Do tất cả các URL đã thay đổi, ta cần sửa lại tất cả các functional test trong test/functional/frontend/ và thêm /en vào trước các URL. Đừng quên đổi lại các URL trong file lib/test/JobeetTestFunctional.class.php. Chạy test suite để kiểm tra xem bạn đã sửa đúng chưa:

$ php symfony test:functional frontend

User tester cung cấp phương thức isCulture() để kiểm tra culture hiện tại của user. Mở file jobActionsTest và thêm đoạn test sau:

// test/functional/frontend/jobActionsTest.php$browser->setHttpHeader('ACCEPT_LANGUAGE', 'fr_FR,fr,en;q=0.7');$browser-> info('6 - User culture')->  restart()-> 

info(' 6.1 - For the first request, symfony guesses the best culture')-> get('/')-> isRedirected()->followRedirect()-> with('user')->isCulture('fr')->  info(' 6.2 - Available cultures are en and fr')-> get('/it/')-> with('response')->isStatusCode(404); $browser->setHttpHeader('ACCEPT_LANGUAGE', 'en,fr;q=0.7');$browser-> info(' 6.3 - The culture guessing is only for the first request')->  get('/')-> isRedirected()->followRedirect()-> with('user')->isCulture('fr');

Chuyển đổi ngôn ngữ

Để user có thể chuyển sang ngôn ngữ khác, ta cần thêm một language form vào layout. Form framework không cung cấp form ngoài nhưng điều này là cần thiết đối với websites đa ngôn ngữ, do đó symfony core team đã phát triển sfFormExtraPlugin, chứa các validator, widget, và form hữu ích.

Cài đặt plugin với task plugin:install:

$ php symfony plugin:install sfFormExtraPlugin

Xóa cache để các class mới của plugin có tác dụng:

$ php symfony cc

sfFormExtraPlugin chứa các widget yêu cầu một số thư viện liên quan, như thư viện JavaScript. Bạn sẽ tìm thấy một widget cho rich date selectors, một cho WYSIWYG editor, ... Hãy dành chút thời gian đọc hướng dẫn bạn sẽ tìm thấy nhiều thông tin hữu ích.

Plugin sfFormExtraPlugin cung cấp một form sfFormLanguage để quản lý việc chọn ngôn ngữ. Thêm language form vào layout như sau:

Code dưới đây chứa một vài lỗi để chỉ cho bạn thấy cách viết code không đúng. Chúng tôi sẽ chỉ cho bạn cách viết đúng ngay sau đó.

// apps/frontend/templates/layout.php<div id="footer"> <div class="content"> <!-- footer content --> 

<?php $form = new sfFormLanguage( $sf_user, array('languages' => array('en', 'fr')) ) ?> <form action="<?php echo url_for('@change_language') ?>"> <?php echo $form ?><input type="submit" value="ok" /> </form> </div></div>

Vấn đề ở đây là gì? Đó chính là việc tạo form object không nằm ở View layer. Nó phải được tạo từ một action. Nhưng code nằm trong layout, và form phải được tạo với mọi action. Trong trường hợp này, bạn có thể sử dụng component. Một component tương tự như partial nhưng có thêm một vài đoạn code gắn với nó. CÓ thể coi nó là một lightweight action.

Include một component trong template được thực hiện thông qua helper include_component():

// apps/frontend/templates/layout.php<div id="footer"> <div class="content"> <!-- footer content -->  <?php include_component('language', 'language') ?> </div></div>

Helper nhận tham số là tên module và component. Tham số thứ 3 là các giá trị cung cấp cho component đó.

Tạo module language để chứa component và action thực hiện việc chuyển đổi ngôn ngữ cho user:

$ php symfony generate:module frontend language

Components được xác định trong file actions/components.class.php.

Nội dung file này:

// apps/frontend/modules/language/actions/components.class.phpclass languageComponents extends sfComponents{ public function executeLanguage(sfWebRequest $request) { $this->form = new sfFormLanguage( $this->getUser(), array('languages' => array('en', 'fr')) ); }}

Như bạn có thể thấy, class component tương tự như một class action.

Template cho một component có cách đặt tên tương tự partial: một kí tự gạch dưới (_) sau đó là tên component:

// apps/frontend/modules/language/templates/_language.php<form action="<?php echo url_for('@change_language') ?>"> <?php echo $form ?><input type="submit" value="ok" /></form>

Do plugin không cung cấp action thực hiện việc chuyển user culture, thêm vào file routing.yml route change_language:

# apps/frontend/config/routing.ymlchange_language: url: /change_language param: { module: language, action: changeLanguage }

Và tạo action tương ứng:

// apps/frontend/modules/language/actions/actions.class.phpclass languageActions extends sfActions{ public function executeChangeLanguage(sfWebRequest $request) { $form = new sfFormLanguage( $this->getUser(), array('languages' => array('en', 'fr')) );  $form->process($request);  return $this->redirect('@localized_homepage'); }}

Phương thức process() của sfFormLanguage thực hiện việc đổi culture cho user, dựa vào form submission của user.

Internationalization

Languages, Charset, và Encoding

Những ngôn ngữ khác nhau có tập các kí tự khác nhau. Tiếng Anh là ngôn ngữ đơn giản nhất do chỉ sử dụng các kí tự ASCII, tiếng Pháp phức tạp hơn một chút với các kí tự nhấn trọng âm như "é", những ngôn ngữ như Russian, Chinese, hay Arabic thì rất phức tạp do các kí tự nằm ngoài dải kí tự ASCII.

Khi làm việc với dữ liệu international, tốt hơn là sử dụng tiêu chuẩn unicode. Tư tưởng của unicode là thiết lập một tập chung chứa các kí tự của tất cả các ngôn ngữ. Vấn đề với unicode là một kí tự đơn cần 21 bits để mô tả. Do đó, với web, chúng ta sử dụng UTF-8, sẽ map Unicode code points với variable-length sequences của octets. Trong UTF-8, hầu hết các ngôn ngữ có mã kí tự với độ dài ít hơn 3 bits.

UTF-8 là encode mặc định trong symfony, được xác định trong file cấu hình settings.yml:

# apps/frontend/config/settings.ymlall: .settings: charset: utf-8

Để enable internationalization layer của symfony, bạn phải bật i18n setting trong settings.yml:

# apps/frontend/config/settings.ymlall: .settings: i18n: on

Templates

Một website đa ngôn ngữ có nghĩa là nội dung sẽ được dịch ra vài ngôn ngữ.

Trong một template, tất cả các câu phụ thuộc ngôn ngữ được chứa trong helper __() (chú ý rằng có 2 kí tự gạch dưới).

Helper __() là một phần của I18N helper group, chứa các helper để dễ dàng quản lý i18n trong template. Do helper group này mặc định không được load, bạn cần tự thêm nó vào template với use_helper('I18N') như chúng ta đã làm với Text helper group, hoặc có thể load nó ở global bằng cách thêm vào standard_helpers setting:

# apps/frontend/config/settings.ymlall: .settings: standard_helpers: [Partial, Cache, I18N]

Đây là cách sử dụng __() helper cho Jobeet footer:

// apps/frontend/templates/layout.php<div id="footer"> <div class="content"> <span class="symfony"> <img src="/images/jobeet-mini.png" /> powered by <a href="http://www.symfony-project.org/"> <img src="/images/symfony.gif" alt="symfony framework" /></a> </span> <ul> <li> <a href=""><?php echo __('About Jobeet') ?></a> </li> <li class="feed"> <?php echo link_to(__('Full feed'), '@job?sf_format=atom') ?> </li> <li> <a href=""><?php echo __('Jobeet API') ?></a> </li> <li class="last"> <?php echo link_to(__('Become an affiliate'), '@affiliate_new') ?> </li> </ul> <?php include_component('language', 'language') ?> </div></div>

__() helper có thể dùng chuỗi là ngôn ngữ mặc định hoặc dùng một đại diện duy nhất cho mỗi chuỗi. Dùng cách nào là tùy mỗi người. Với Jobeet, chúng tôi sử dụng cách đầu tiên để template dễ đọc.

Khi symfony render một template, mỗi khi __() helper được gọi, symfony tìm bản dịch cho culture hiện tại của user. Nếu một bản dịch được tìm thấy, nó sẽ được sử dụng, nếu không, tham số đầu tiên của helper sẽ được dùng làm giá trị trả về.

Tất cả các bản dịch được chứa trong một catalogue. i18n framework cung cấp nhiều cách khác nhau để chứa bản dịch. Chúng tôi sử dụng format "XLIFF", đó là một chuẩn khá mềm dẻo. Nó cũng được sử dụng bởi admin generator và phần lớn các symfony plugin.

Các catalogue để chứa là gettext, MySQL, và SQLite. Bạn có thể xem chi tiết ở i18n API.

i18n:extract

Thay vì tạo file catalogue bằng tay, chúng ta sử dụng task có sẵn i18n:extract:

$ php symfony i18n:extract frontend fr --auto-save

Task i18n:extract tìm tất cả các chuỗi cần dịch sang fr trong application frontend và tạo/cập nhật vào catalogue tương ứng. Option --auto-save lưu chuỗi mới vào catalogue. Bạn cũng có thể dùng option --auto-delete để tự động xóa chuỗi không còn tồn tại.

Trong trường hợp của chúng ta, file được tạo ra như sau:

<!-- apps/frontend/i18n/fr/messages.xml --><?xml version="1.0" encoding="UTF-8"?><!DOCTYPE xliff PUBLIC "-//XLIFF//DTD XLIFF//EN" "http://www.oasis-open.org/committees/xliff/documents/xliff.dtd"><xliff version="1.0"> <file source-language="EN" target-language="fr" datatype="plaintext" original="messages" date="2008-12-14T12:11:22Z" product-name="messages"> <header/> <body> <trans-unit id="1"> <source>About Jobeet</source> <target/> </trans-unit> <trans-unit id="2"> <source>Feed</source> <target/> </trans-unit> <trans-unit id="3"> <source>Jobeet API</source> <target/> </trans-unit> <trans-unit id="4"> <source>Become an affiliate</source> <target/> </trans-unit> </body> </file></xliff>

Mỗi phần dịch được quản lý bởi một trans-unit tag với một id duy nhất. Bây giờ bạn có thể sửa file này và thêm nội dung dịch ra tiếng Pháp:

<!-- apps/frontend/i18n/fr/messages.xml --><?xml version="1.0" encoding="UTF-8"?><!DOCTYPE xliff PUBLIC "-//XLIFF//DTD XLIFF//EN" "http://www.oasis-open.org/committees/xliff/documents/xliff.dtd"><xliff version="1.0"> <file source-language="EN" target-language="fr" datatype="plaintext" original="messages" date="2008-12-14T12:11:22Z" product-name="messages"> <header/> <body> <trans-unit id="1"> <source>About Jobeet</source> <target>A propos de Jobeet</target> </trans-unit> <trans-unit id="2"> <source>Feed</source> <target>Fil RSS</target> </trans-unit> <trans-unit id="3"> <source>Jobeet API</source> <target>API Jobeet</target> </trans-unit> <trans-unit id="4"> <source>Become an affiliate</source> <target>Devenir un affilié</target> </trans-unit> </body> </file></xliff>

Do XLIFF là một format chuẩn, nên có rất nhiều công cụ có sẵn giúp việc dịch được dễ dàng. Open Language Tools là một dự án Java Open-Source cung cấp một XLIFF editor.

Do XLIFF làm một file-based format, nên nó cũng cùng quy luật như tất cả các file cấu hình khác của symfony. File I18n có thể chứa trong một project, một application, hay một module.

Translations với Arguments

Nguyên tắc cơ bản của internationalization là dịch toàn bộ câu. Nhưng một vài câu chứa giá trị động. Trong Jobeet, đó là ở trang chủ với link "more...":

// apps/frontend/modules/job/templates/indexSuccess.php<div class="more_jobs"> and <?php echo link_to($count, 'category', $category) ?> more...</div>

Số công việc là một biến được thay thế bởi một placeholder trong bản dịch:

// apps/frontend/modules/job/templates/indexSuccess.php<div class="more_jobs"> <?php echo __('and %count% more...', array('%count%' => link_to($count, 'category', $category))) ?>

</div>

Chuỗi cần dịch bây giờ là "and %count% more...", và placeholder %count% sẽ được thay bởi số cụ thể khi chạy, nhờ có giá trị được cung cấp trong tham số thứ 2 của helper __().

Thêm một trans-unit tag mới vào file messages.xml, hoặc sử dụng task i18n:extract để tự động cập nhật file:

$ php symfony i18n:extract frontend fr --auto-save

Sau khi chạy lệnh, mở file XLIFF và thêm phần dịch ra tiếng Pháp:

<trans-unit id="5"> <source>and %count% more...</source> <target>et %count% autres...</target></trans-unit>

Khi dịch, placeholder %count% cần được giữ nguyên và đặt ở nơi thích hợp.

Một số chuỗi phức tạp hơn do quy tắc về số nhiều của ngôn ngữ. Tùy thuộc vào giá trị của số mà câu thay đổi theo, nhưng mỗi ngôn ngữ lại có một nguyên tắc riêng. Một vài ngôn ngữ có quy tắc số nhiều phức tạp như Polish hay Russian.

Trong trang category, số công việc trong category hiện tại được hiển thị:

// apps/frontend/modules/category/templates/showSuccess.php<strong><?php echo $pager->getNbResults() ?></strong> jobs in this category

Khi một câu có các nội dung dịch khác nhau tùy vào giá trị của số, helper format_number_choice() được sử dụng:

<?php echo format_number_choice( '[0]No job in this category|[1]One job in this category|(1,+Inf]%count% jobs in this category', array('%count%' => '<strong>'.$pager->getNbResults().'</strong>'), $pager->getNbResults() )?>

format_number_choice() helper dùng 3 tham số:

Chuỗi sử dụng tùy vào giá trị của số Mảng các placeholder cần thay thế Số dùng để quyết định chuỗi nào được dùng

Chuỗi mô tả các bản dịch khác nhau tùy vào giá trị của số được format theo quy tắc:

Các chuỗi được cách nhau bởi dấu gạch đứng (|)

Mỗi chuỗi bao gồm một range theo sau là bản dịch

range có thể mô tả bất kì dải số nào:

[1,2]: chấp nhận các giá trị từ 1 đến 2 (1,2): chấp nhận các giá trị giữa 1 đến 2 {1,2,3,4}: chỉ chấp nhận các giá trị đã được liệt kê [-Inf,0): chấp nhận các giá trị từ âm vô cùng đến < 0 {n: n % 10 > 1 && n % 10 < 5}: chấp nhận các số như 2, 3, 4, 22, 23, 24

Dịch chuỗi tương tự như các chuỗi khác:

<trans-unit id="6"> <source>[0]No job in this category|[1]One job in this category|(1,+Inf]%count% jobs in this category</source> <target>[0]Aucune annonce dans cette catégorie|[1]Une annonce dans cette catégorie|(1,+Inf]%count% annonces dans cette catégorie</target></trans-unit>

Bây giờ bạn đã hiểu cách internationalize tất cả các loại chuỗi, hãy thêm __() vào tất cả các template của frontend application. Chúng ta sẽ không internationalize backend application.

Forms

Form classes chứa nhiều chuỗi cần được dịch, như labels, error messages, và help messages. Tất cả những chuỗi này được symfony tự động internationalized, bạn chỉ cần thêm bản dịch vào file XLIFF.

Không may thay, task i18n:extract không phân tích các form class để xác định các chuỗi cần dịch, bạn phải tự thêm bằng tay.

Propel Objects

Với Jobeet website, chúng ta sẽ không internationalize tất cả các bảng do không thể yêu cầu nhà tuyển dụng đưa lên bảng dịch cho tuyển dụng của họ sang ngôn ngữ khác. Ta chỉ dịch cho bảng category.

Propel plugin hỗ trợ bảng i18n. Với mỗi bảng chứa dữ liệu local, 2 bảng cần được tạo: một chứa các cột không phụ thuộc i18n, và một cho các cột cần được internationalized. 2 bảng này được liên kết theo quan hệ một-nhiều.

Cập nhật lại schema.yml:

# config/schema.ymljobeet_category: _attributes: { isI18N: true, i18nTable: jobeet_category_i18n } id: ~

 jobeet_category_i18n: id: { type: integer, required: true, primaryKey: true, foreignTable: jobeet_category, foreignReference: id } culture: { isCulture: true, type: varchar, size: 7, required: true, primaryKey: true } name: { type: varchar(255), required: true } slug: { type: varchar(255), required: true }

The _attributes entry defines options for the table.

And update the fixtures for categories:

# data/fixtures/010_categories.ymlJobeetCategory: design: { } programming: { } manager: { } administrator: { } JobeetCategoryI18n: design_en: { id: design, culture: en, name: Design } programming_en: { id: programming, culture: en, name: Programming } manager_en: { id: manager, culture: en, name: Manager } administrator_en: { id: administrator, culture: en, name: Administrator }  design_fr: { id: design, culture: fr, name: Design } programming_fr: { id: programming, culture: fr, name: Programmation } manager_fr: { id: manager, culture: fr, name: Manager } administrator_fr: { id: administrator, culture: fr, name: Administrateur }

Rebuild the model to create the i18n stub classes:

$ php symfony propel:build-all --no-confirmation$ php symfony cc

As the name and slug columns have been moved to the i18n table, move the setName() method from JobeetCategory to JobeetCategoryI18n:

// lib/model/JobeetCategoryI18n.phppublic function setName($name){ parent::setName($name);  $this->setSlug(Jobeet::slugify($name));}

We also need to fix the getForSlug() method in JobeetCategoryPeer:

// lib/model/JobeetCategoryPeer.php

static public function getForSlug($slug){ $criteria = new Criteria(); $criteria->addJoin(JobeetCategoryI18nPeer::ID, self::ID); $criteria->add(JobeetCategoryI18nPeer::CULTURE, 'en'); $criteria->add(JobeetCategoryI18nPeer::SLUG, $slug);  return self::doSelectOne($criteria);}

Do propel:build-all sẽ xóa tất cả các bảng và dữ liệu trong database, nên đừng quên tạo lại tài khoản để truy cập Jobeet backend bằng task guard:create-user. Bạn cũng có thể thêm một fixture file để việc này được tự động thực hiện.

When building the model, symfony creates proxy methods in the main JobeetCategory object to conveniently access the i18n columns defined in JobeetCategoryI18n:

$category = new JobeetCategory(); $category->setName('foo'); // sets the name for the current culture$category->setName('foo', 'fr'); // sets the name for French echo $category->getName(); // gets the name for the current cultureecho $category->getName('fr'); // gets the name for French

To reduce the number of database requests, use the doSelectWithI18n() method instead of the regular doSelect() one. It will retrieve the main object and the i18n one in one request.

$categories = JobeetCategoryPeer::doSelectWithI18n($c, $culture);

Do route category phụ thuộc vào JobeetCategory model class và slug bây giờ là một phần của JobeetCategoryI18n, route sẽ không thể nhận Category object tự động. Để giúp routing system, hãy tạo một phương thức để lấy object:

// lib/model/JobeetCategoryPeer.phpclass JobeetCategoryPeer extends BaseJobeetCategoryPeer{ static public function doSelectForSlug($parameters) { $criteria = new Criteria(); $criteria->addJoin(JobeetCategoryI18nPeer::ID, JobeetCategoryPeer::ID); $criteria->add(JobeetCategoryI18nPeer::CULTURE, $parameters['sf_culture']); $criteria->add(JobeetCategoryI18nPeer::SLUG, $parameters['slug']);  return self::doSelectOne($criteria); } // ...

}

Sau đó, sử dụng option method để route category sử dụng method doSelectForSlug() để nhận object:

# apps/frontend/config/routing.ymlcategory: url: /:sf_culture/category/:slug.:sf_format class: sfPropelRoute param: { module: category, action: show, sf_format: html } options: { model: JobeetCategory, type: object, method: doSelectForSlug } requirements: sf_format: (?:html|atom)

Chúng ta cần nạp lại fixtures để tạo lại các slug cho categories:

$ php symfony propel:data-load

Bây giờ, category route đã được international và URL cho mỗi category chứa category slug đã được dịch:

/frontend_dev.php/fr/category/programmation/frontend_dev.php/en/category/programming

Admin Generator

Do một bug trong symfony 1.2.1, bạn cần comment title trong mục edit:

# apps/backend/modules/category/config/generator.ymledit: #title: Editing Category "%%name%%" (#%%id%%)

Với backend, chúng ta muốn bản dịch French và English được sửa trong cùng một form:

Nhúng form i18n sử dụng method embedI18N():

// lib/form/JobeetCategoryForm.class.phpclass JobeetCategoryForm extends BaseJobeetCategoryForm{ public function configure() { unset($this['jobeet_category_affiliate_list']);  $this->embedI18n(array('en', 'fr')); $this->widgetSchema->setLabel('en', 'English'); $this->widgetSchema->setLabel('fr', 'French'); }}

Giao diện admin generator hỗ trợ internationalization. Nó chứa sẵn bản dịch của hơn 20 ngôn ngữ, và dễ dàng thêm một bản dịch mới, hoặc sửa bản dịch có sẵn. Sửa file cho ngôn ngữ bạn muốn thay đổi (admin translations có thể tìm trong lib/vendor/symfony/lib/plugins/sfPropelPlugin/i18n/) ở application i18n. Do những file trong ứng dụng của bạn cũng được sử dụng, nên chỉ cần quan tâm đến các chuỗi chưa có trong file ứng dụng.

Bạn có thể để ý thấy rằng file dịch của admin generator có tên là sf_admin.fr.xml, thay vì fr/messages.xml. Thực tế là, messages là tên của catalogue, và được thay đổi để có thể dùng ở tất cả các phần của ứng dụng. Sử dụng một catalogue thay vì mặc định yêu cầu bạn phải chỉ rõ nơi dịch với helper __():

<?php echo __('About Jobeet', array(), 'jobeet') ?>

Trong lời gọi __() trên, symfony sẽ tìm chuỗi "About Jobeet" trong catalogue jobeet.

Tests

Fixing tests is an integral part of the internationalization migration. First, update the test fixtures for categories by copying the fixtures we have defined above in test/fixtures/010_categories.yml.

Rebuild the model for the test environment:

$ php symfony propel:build-all-load --no-confirmation --env=test

You can now launch all tests to check that they are running fine:

$ php symfony test:all

When we have developed the backend interface for Jobeet, we have not written functional tests. But whenever you create a module with the symfony command line, symfony also generate test stubs. These stubs are safe to remove.

Localization

Templates

Hỗ trợ các culture khác nhau cũng có nghĩa là hỗ trợ các cách khác nhau để format date và number. Trong một template, có vài helper giúp bạn thực hiện điều này dựa trên culture hiện tại của user:

Trong Date helper group:

Helper Mô tảformat_date() Formats a dateformat_datetime()Formats a date

Trong Number helper group:

Helper Mô tảformat_number() Formats a number

Helper Mô tảformat_currency()Formats a currency

Trong I18N helper group:

Helper Mô tảformat_country() Displays the name of a countryformat_language()Displays the name of a language

Forms

Form framework cung cấp một vài widgets and validators cho dữ liệu local:

sfWidgetFormI18nDate sfWidgetFormI18nDateTime sfWidgetFormI18nTime

sfWidgetFormI18nSelectCountry

sfWidgetFormI18nSelectCurrency sfWidgetFormI18nSelectLanguage

sfValidatorI18nChoiceCountry

sfValidatorI18nChoiceCountry

Hẹn gặp lại ngày mai

Internationalization và localization là tính năng được hỗ trợ sẵn trong symfony. Xây dựng một website đa ngôn ngữ thật đơn giản do symfony cung cấp tất cả các công cụ cơ bản và các task từ dòng lệnh để thực hiện điều đó nhanh chóng.

Để chuẩn bị cho hướng dẫn đặc biệt ngày mai, chúng ta sẽ chuyển rất nhiều files và khám phá sự khác nhau trong cách tổ chức một symfony project.

Hôm nay, chúng ta sẽ nói về cache. Symfony framework đã có sẵn rất nhiều loại cache khác nhau. Ví dụ, file cấu hình YAML được chuyển thành PHP và sau đó được cache thành file hệ thống. Chúng ta cũng đã thấy các module được tạo ra bởi admin generator được cache để tăng hiệu năng.

Nhưng hôm nay, chúng ta sẽ nói về loại cache khác: HTML cache. Để giải quyết vấn đề hiệu năng cho website của mình, bạn có thể cache toàn bộ trang HTML hoặc một phần của trang.

Tạo một môi trường mới

Mặc định, tính năng cache template của symfony được enable trong file cấu hình settings.yml cho môi trường prod , và tắt trong môi trường test và dev:

prod: .settings: cache: on dev: .settings: cache: off test: .settings: cache: off

Do chúng ta cần test tính năng cache trước khi đưa vào sản phẩm, chúng ta có thể kích hoạt cache cho môi trường dev hoặc tạo một môi trường mới. Như ta đã biết, một môi trường được xác định bởi tên gọi (một chuỗi), một front controller tương ứng, và tập các cấu hình cụ thể.

Để sử dụng hệ thống cache cho Jobeet, chúng ta sẽ tạo một môi trường cache, tương tự như môi trường prod, nhưng sẽ có thông tin log và debug như môi trường dev.

Tạo front controller tương ứng cho môi trường cache bằng cách copy file dev front controller web/frontend_dev.php thành web/frontend_cache.php:

// web/frontend_cache.phpif (!in_array(@$_SERVER['REMOTE_ADDR'], array('127.0.0.1', '::1'))){ die('You are not allowed to access this file. Check '.basename(__FILE__).' for more information.');} require_once(dirname(__FILE__).'/../config/ProjectConfiguration.class.php'); $configuration = ProjectConfiguration::getApplicationConfiguration('frontend', 'cache', true);sfContext::createInstance($configuration)->dispatch();

Đó là tất cả những gì phải làm. Môi trường cache đã sẵn sàng để sử dụng. Chỉ có một sự khác biệt là tham số thứ 2 của phương thức getApplicationConfiguration() là tên của môi trường, cache.

Bạn có thể kiểm tra môi trường cache từ trình duyệt bằng cách gọi front controller của nó:

http://jobeet.localhost/frontend_cache.php/

Front controller script bắt đầu bằng một đoạn code để chắc rằng front controller chỉ được gọi từ mội địa chỉ IP ở local. Sự bảo mật này để bảo vệ front controller tránh bị gọi trên production servers. Chúng ta sẽ nói về vấn đề này chi tiết hơn vào ngày mai.

Bây giờ, môi trường cache thừa kế các cấu hình mặc định. Thêm các cấu hình cho môi trường cache vào file settings.yml:

# apps/frontend/config/settings.ymlcache: .settings: error_reporting: <?php echo (E_ALL | E_STRICT)."\n" ?> web_debug: on cache: on etag: off

Trong những thiết lập này,tính năng symfony template cache được kích hoạt và web debug toolbar được enable.

Mặc định, tất cả các cấu hình được cache, nên bạn cần xóa cache để thiết lập có hiệu lực:

$ php symfony cc

Bây giờ, nếu bạn refresh lại trình duyệt, web debug toolbar sẽ hiện ở góc trên phải của trang, như trong môi trường dev.

Cấu hình Cache

Symfony template cache có thể được cấu hình trong file cache.yml. Cấu hình mặc định cho ứng dụng nằm ở apps/frontend/config/cache.yml:

default: enabled: off with_layout: false lifetime: 86400

Mặc định, do tất cả các trang đều có thể chứa nội dung động, nên cache bị disabled ở mức global (enabled: off). Chúng ta không cần thay đổi thiết lập này, bởi vì chúng ta có thể enable cache ở từng trang cụ thể.

lifetime setting xác định thời gian tồn tại của cache tính theo giây (86400 giây tương đương với một ngày).

Bạn cũng có thể dùng các khác: enable cache ở mức global và sau đó disable nó với những trang ko dùng cache. Chọn cách nào phụ thuộc vào ứng dụng của bạn.

Page Cache

Trang chủ có lẽ là trang được truy cập nhiều nhất của Jobeet, nên thay vì request dữ liệu từ database mỗi khi người dùng truy cập, ta có thể cache nó.

Tạo một file cache.yml cho module sfJobeetJob:

# plugins/sfJobeetJob/modules/sfJobeetJob/config/cache.ymlindex: enabled: on with_layout: true

File cấu hình cache.yml cũng giống như bất kì file cấu hình nào khác của symfony. Bạn có thể enable cache cho toàn bộ action của một module bằng cách sử dụng từ khóa all.

Nếu bạn refresh lại trình duyệt, bạn sẽ thấy rằng symfony đã thêm vào một box để chỉ rõ nội dung được cache:

Box cung cấp một vài thông tin về cache, như lifetime của cache, và age của nó.

Nếu bạn refresh trang web lần nữa, màu của box sẽ chuyển từ xanh sang vàng, chỉ ra rằng nội dung được lấy từ cache:

Bạn cũng để ý rằng không có yêu cầu nào tới database , như được chỉ ra trên web debug toolbar.

Mặc dù ngôn ngữ có thể thay đổi tùy vào user, nhưng cache vẫn làm việc do ngôn ngữ được nhúng vào URL.

Khi một trang được cache, và nếu cache chưa có trước đó, symfony sẽ chứa đối tượng trả về vào cache ở cuối mỗi request. Đối với các request sau đó, symfony sẽ lấy nội dung trả về từ cache mà không gọi controller:

Điều này đem lại sự thay đổi lớn về hiệu năng mà bạn có thể thấy thông qua các công cụ như JMeter.

Một request chứa GET parameters hoặc submit với POST, PUT, hoặc DELETE method sẽ không được cache, trong cả trường hợp đã được cấu hình.

Trang đăng tuyển dụng cũng được cache:

# plugins/sfJobeetJob/modules/sfJobeetJob/config/cache.ymlnew: enabled: on index: enabled: on all: with_layout: true

Do 2 trang có thể được cache có layout, chúng ta tạo một mục all xác định cấu hình mặc định cho tất cả action của module sfJobeetJob.

Xóa Cache

Nếu bạn muốn xóa cache, bạn có thể sử dụng lệnh cache:clear

$ php symfony cc

Lệnh cache:clear xóa tất cả các cache nằm trong thư mục cache/. Nó cũng có lựa chọn để xóa một phần cache. Để chỉ xóa cache cho template ở môi trường cache, sử dụng option --type và --env:

$ php symfony cc --type=template --env=cache

Thay vì xóa cache mỗi khi bạn có thay đổi, bạn có thể disable cache bằng cách thêm câu truy vấn bất kì vào URL, hoặc sử dụng nút "Ignore cache" ở web debug toolbar:

Cache Action

Đôi khi, bạn không thể cache toàn bộ trang, nhưng có thể cache action template.

Với ứng dụng Jobeet, chúng ta không thể cache toàn bộ trang do có thanh "history job".

Do đó file cấu hình cache cho module job được thay đổi như sau:

# plugins/sfJobeetJob/modules/sfJobeetJob/config/cache.ymlnew: enabled: on index: enabled: on all: with_layout: false

Bằng cách chuyển with_layout setting thành false, bạn đã disable cache layout.

Xóa cache:

$ php symfony cc

Refresh lại trình duyệt để thấy sự khác biệt:

Even if the flow of the request is quite similar in the simplified diagram, caching without the layout is much more resource intensive.

Cache Partial và Component

Với các website thường xuyên thay đổi, bạn gần như không thể cache toàn bộ action template. Trong trường hợp này, bạn cần cấu hình cache ở mức chi tiết hơn. Thật may mắn là partials và components cũng có thể được cache.

Hãy cache component language bằng cách tạo file cache.yml cho module sfJobeetLanguage:

# plugins/sfJobeetJob/modules/sfJobeetLanguage/config/cache.yml_language: enabled: on

Cấu hình cache cho một partial hay component đơn giản là thêm một mục với tên của thành phần đó. Lựa chọn with_layout trở nên không còn tác dụng trong trường hợp này:

Phụ thuộc ngữ cảnh hay không?

Cùng một component hay partial có thể được sử dụng ở nhiều template khác nhau. Ví dụ, partial job list được sử dụng ở cả module job và category. Do render theo cùng một cách, partial không phụ thuộc vào nội dung nó được sử dụng và cache là giống nhau cho tất cả các template ( cache vẫn khác nhau với các tham số khác nhau).

Nhưng đôi khi, một partial hay component hiển thị khác nhau, tùy vào action sử dụng nó (ví dụ như một blog sidebar, nó sẽ hiển thị khác nhau ở trang homepage và blog post). Trong những trường hợp này, partial và component phụ thuộc ngữ cảnh, và cache phải được cấu hình contextual option là true:

_sidebar: enabled: on contextual: true

Cache Form

Trang đăng tuyển dụng ở cache gặp một rắc rối là nó chứa form. Để hiểu rõ hơn vấn đề, ta truy cập vào trange "Post a Job" từ trình duyệt để cache trang. Sau đó, xóa session cookie, và thử submit một công việc. Bạn sẽ gặp thông báo lỗi "CSRF attack":

Tại sao lại vậy? Chúng ta đã cấu hình một CSRF secret khi tạo frontend application, do đó symfony nhúng một CSRF token vào tất cả các form. Để bảo vệ bạn khỏi tấn công CSRF, token này là duy nhất đối với mỗi user và form.

Lần đầu tiên trang được hiển thị, HTML form tạo ra được chứa trong cache với token của user hiện tại. Nếu user khác sử dụng tiếp sau đó, nội dung trang được lấy từ cache sẽ hiển thị với CSRF token của user trước. Khi submit form, token không match, và lỗi xảy ra.

Làm thế nào sửa vấn đề này để có thể chứa form trong cache? Form đăng tuyển dụng không phụ thuộc vào user, và nó không thay đổi bất kì thứ gì so với user trước. Trong trường hợp này, bảo vệ CSRF là không cần thiết, và chúng ta có thể bỏ CSRF token:

// plugins/sfJobeetPlugin/lib/form/doctrine/PluginJobeetJobForm.class.phpabstract PluginJobeetJobForm extends BaseJobeetJobForm{ public function __construct(sfDoctrineRecord $object = null, $options = array(), $CSRFSecret = null) { parent::__construct($object, $options, false); }  // ...}

Sau khi thực hiện thay đổi trên, xóa cache và thử lại các bước trên để chắc rằng mọi thứ hoạt động như mong đợi.

Cấu hình tương tự cho form language do nó nằm trong layout và sẽ được chứa trong cache. Do mặc định sfLanguageForm được sử dụng, nên thay vì tạo một class mới, để bỏ CSRF token, ta hãy thực hiện từ action và component của sfJobeetLanguage module:

// plugins/sfJobeetJob/modules/sfJobeetLanguage/actions/components.class.phpclass sfJobeetLanguageComponents extends sfComponents{ public function executeLanguage(sfWebRequest $request) { $this->form = new sfFormLanguage($this->getUser(), array('languages' => array('en', 'fr'))); unset($this->form[$this->form->getCSRFFieldName()]); }} // plugins/sfJobeetJob/modules/sfJobeetLanguage/actions/actions.class.phpclass sfJobeetLanguageActions extends sfActions{ public function executeChangeLanguage(sfWebRequest $request) { $form = new sfFormLanguage($this->getUser(), array('languages' => array('en', 'fr'))); unset($form[$this->form->getCSRFFieldName()]);  // ... }}

getCSRFFieldName() trả về tên của field chứa CSRF token. Bằng cách unset field này, widget và validator liên quan được bỏ đi.

Xóa Cache

Mỗi khi người dùng đưa lên một công việc mới và kích hoạt nó, trang chủ phải được cập nhật danh sách.

DO chúng ta không cần công việc xuất hiện ngay lập tức trên trang chủ, nên tốt nhất là chọn thời gian cache ở mức chấp nhận được:

# plugins/sfJobeetJob/modules/sfJobeetJob/config/cache.ymlindex: enabled: on lifetime: 600

Thay vì để cấu hình mặc định là một ngày, cache ở trang chủ sẽ được tự động xóa bỏ sau 10 phút.

Nhưng nếu bạn muốn cập nhật trang chủ ngay khi người dùng kích hoạt một công việc mới, hãy sửa lại phương thức executePublish() của module sfJobeetJob và thêm phần xóa cache:

// plugins/sfJobeetJob/modules/sfJobeetJob/actions/actions.class.phppublic function executePublish(sfWebRequest $request){ $request->checkCSRFProtection();  $job = $this->getRoute()->getObject(); $job->publish();  if ($cache = $this->getContext()->getViewCacheManager()) { $cache->remove('sfJobeetJob/index?sf_culture=*'); $cache->remove('sfJobeetCategory/show?id='.$job->getJobeetCategory()->getId()); }  $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));}

Cache được quản lý bởi class sfViewCacheManager. Phương thức remove() xóa cache liên quan đến internal URI. Để xóa cache cho tất cả các tham số có thể của một biến variable, sử dụng *. sf_culture=* được sử dụng ở trên có nghĩa là symfony sẽ xóa bỏ cache cho cả trang chủ English và French.

Do cache manager là null khi cache chưa được bật, chúng ta đưa đoạn code xóa cache vào trong khối lệnh if.

Lớp sfContext

Object sfContext chứa tham chiếu đến các symfony core object như request, response, user, ... Do sfContext hoạt động như một singleton, bạn có thể sử dụng lệnh sfContext::getInstance() để lấy nó ở bất kì đâu và sau đó truy cập bất kì symfony core object nào:

$user = sfContext::getInstance()->getUser();

Khi bạn muốn sử dụng sfContext::getInstance() trong một class, think twice as it introduces a strong coupling. Nó thực sự tốt hơn là cung cấp đối tượng bạn cần như một tham số.

Bạn cũng có thể sử dụng sfContext như một registry và thêm object của bạn bằng cách sử dụng phương thức set(). Nó nhận tên và object làm tham số và sau này có thể dùng phương thức get() để nhận object thông qua tên:

sfContext::getInstance()->set('job', $job);$job = sfContext::getInstance()->get('job');

Test Cache

Trước khi bắt đầu, chúng ta cần thay đổi cấu hình cho môi trường test để cho phép cache layer:

# apps/frontend/config/settings.ymltest: .settings: error_reporting: <?php echo ((E_ALL | E_STRICT) ^ E_NOTICE)."\n" ?> cache: on web_debug: off etag: off

Bắt đầu test trang đăng tuyển dụng:

// test/functional/frontend/jobActionsTest.php$browser-> info(' 7 - Job creation page')->  get('/fr/')-> with('view_cache')->isCached(true, false)->  createJob(array('category_id' => Doctrine::getTable('CategoryTranslation')->findOneBySlug('programming')->getId()), true)->  get('/fr/')-> with('view_cache')->isCached(true, false)-> with('response')->checkElement('.category_programming .more_jobs', '/23/');

Tester view_cache được dùng để test cache. Phương thức isCached() nhận 2 tham số boolean:

trang có được cache hay không có cache layout hay không

Mặc dù functional test framework cung cấp cho ta đầy đủ công cụ, nhưng đôi khi xem xét vấn đề thông qua trình duyệt đơn giản hơn. Hãy tạo một front controller cho môi trường test. Log chứa trong log/frontend_test.log có thể rất hữu ích.

Hẹn gặp lại ngày mai

Giống như nhiều tính năng khác, symfony cache sub-framework rất mềm dẻo và cho phép lập trình viên cấu hình cache ở các mức khác nhau.

Ngày mai, chúng ta sẽ nói về bước cuối cùng trong vòng đời của một sản phẩm: deployment lên production servers.

Với cấu hình cho hệ thống cache ngày hôm qua, Jobeet website đã sẵn sàng để đưa lên production servers.

Trong 22 ngày qua, chúng ta đã phát triển Jobeet trên môi trường development, và thường là trên máy của bạn; trừ khi bạn phát triển trực tiếp trên production server. Bây giờ là lúc để chuyển website lên production server.

Hôm nay, chúng ta sẽ xem những việc phải làm để đóng gói sản phẩm, những cách để deploy, và những công cụ cần thiết để có thể deployment thành công.

Chuẩn bị Production Server

Trước khi deploying dự án thành production, chúng ta cần chắc rằng production server được cấu hình đúng. Bạn có thể đọc lại ngày 1, chúng ta đã nói về cách cấu hình web server.

Trong mục này, chúng tôi mặc định rằng bạn đã cài web server, database server, và PHP 5.2.4 trở lên.

Nếu bạn không có một SSH để truy cập web server, bỏ qua các phần mà bạn phải truy cập sử dụng dòng lệnh.

Cấu hình Server

Đầu tiên, bạn cần kiểm tra rằng PHP được cài đặt với tất cả các extensions cần thiết và được cấu hình đúng. Trong ngày 1, chúng ta đã sử dụng script check_configuration.php của symfony để kiểm tra. Do chúng ta sẽ không cài symfony trên production server, nên ta cần download file này trực tiếp từ symfony website:

http://trac.symfony-project.org/browser/branches/1.2/data/bin/check_configuration.php?format=raw

Copy file vào thư mục web root, chạy nó trên trình duyệt và từ dòng lệnh:

$ php check_configuration.php

Sửa các lỗi mà script tìm thấy và lặp lại quá trình cho đến khi mọi thứ đều ổn ở cả hai môi trường.

PHP Accelerator

Với production server, bạn sẽ muốn hiệu năng tốt nhất có thể. Cài đặt PHP accelerator sẽ giúp cải thiện tốc độ mà không tốn kém tiền bạc.

Wikipedia: PHP accelerator làm việc bằng cách cache bytecode đã được biên dịch của PHP scripts để tránh việc phân tích và dịch lại mã nguồn mỗi khi có một request.

APC được sử dụng phổ biến, và cài đặt đơn giản:

$ pecl install APC

Tùy vào hệ điều hành của bạn, bạn có thể sử dụng công cụ quản lý gói sẵn có để cài đặt.

Dành chút thời gian để đọc về cách cấu hình APC.

Thư viện symfony

Embedding symfony

Một trong những điểm mạnh của symfony là tất cả những file cần thiết để project hoạt động có thể đóng gói trong một thư mục. Và bạn có thể copy thư mục này đi bất kì đâu mà không cần phải sửa lại gì trong project, do symfony sử dụng đường dẫn tương đối.

Hãy kiểm tra lại đường dẫn đến symfony core autoloader trong file config/ProjectConfiguration.class.php :

// config/ProjectConfiguration.class.phprequire_once dirname(__FILE__).'/../lib/vendor/symfony/lib/autoload/sfCoreAutoload.class.php';

Upgrading symfony

Mọi thứ được chứa trong một thư mục duy nhất, và việc cập nhật symfony lên một phiên bản mới trở nên đơn giản.

Bạn sẽ muốn cập nhật phiên bản của symfony thường xuyên, do chúng tôi sẽ sửa các lỗi phát sinh và các vấn đề bảo mật nếu có. Tin tốt là tất cả các phiên bản của symfony được bảo trì ít nhất một năm và trong suốt quá trình bảo trì, chúng tôi sẽ không thêm tính năng mới cho phiên bản đó. Vì vậy, cách đơn giản, an toàn và bảo mật là nâng cấp lên phiên bản mới.

Cập nhật symfony đơn giản là thay thế nội dung thư mục lib/vendor/symfony/. Nếu bạn cài đặt symfony bằng file nén, hãy xóa tất cả các file hiện tại và thay bằng các file mới vừa tải về.

Nếu bạn sử dụng Subversion cho dự án của mình, bạn có thể liên kết dự án của bạn với tag symfony 1.2 phiên bản mới nhất:

$ svn propedit svn:externals lib/vendor/ # symfony http://svn.symfony-project.com/tags/RELEASE_1_2_1/

Cập nhật symfony đơn giản là thay đổi tag tới phiên bản symfony mới nhất.

Bạn cũng có thể sử dụng branch 1.2 để được fixe trong thời gian thực:

$ svn propedit svn:externals lib/vendor/ # symfony http://svn.symfony-project.com/branches/1.2/

Bây giờ, mỗi khi bạn thực hiện lệnh svn up, bạn sẽ nhận được phiên bản mới nhất của symfony 1.2.

Khi cập nhật lên một phiên bản mới, bạn nên xóa cache, đặc biệt trong môi trường production:

$ php symfony cc

Nếu bạn có thể truy cập FTP tới production server, bạn có thể thay thế việc thực hiện lệnh symfony cc bằng cách xóa tất cả các file trong thư mục cache/.

Bạn cũng có thể test phiên bản mới của symfony mà không cần thay thế phiên bản có sẵn. Nếu bạn muốn test một phiên bản mới, và có thể quay trở lại dùng phiên bản cũ, hãy cài symfony trong một thư mục khác (ví dụ lib/vendor/symfony_test), thay đổi đường dẫn trong class ProjectConfiguration, xóa cache, và mọi thứ đã hoạt động. Bạn có thể quay lại dùng phiên bản cũ bằng cách sửa lại đường dẫn trong file ProjectConfiguration.

Chỉnh sửa cấu hình

Cấu hình cơ sở dữ liệu

Trong nhiều trường hợp, production database có sự phân quyền khác với ở local. Nhờ có symfony environments, cấu hình cho production database trở nên đơn giản:

$ php symfony configure:database "mysql:host=localhost;dbname=prod_dbname" prod_user prod_pass

Bạn cũng có thể sửa trực tiếp trong file cấu hình databases.yml.

Assets

Do Jobeet sử dụng plugins có sẵn các asset, nên symfony tạo đường dẫn tương đối đến thư mục web/. Lệnh plugin:publish-assets sẽ tạo lại hoặc tạo mới chúng nếu bạn cài đặt plugins không dùng lệnh plugin:install:

$ php symfony plugin:publish-assets

Chỉnh sửa trang thông báo lỗi

Trước khi trở thành sản phẩm, bạn nên chỉnh sửa lại các trang mặc định của symfony, như trang "Page Not Found", hay các trang exception mặc định.

Chúng ta đã cấu hình trang thông báo lỗi cho YAML format trong ngày 16, bằng cách tạo file error.yaml.php và exception.yaml.php trong thư mục config/error/ . File error.yaml.php được symfony sử dụng trong môi trường prod, trong khi file exception.yaml.php được sử dụng trong môi trường dev.

Vì thế, để chỉnh sửa trang exception mặc định cho HTML format, ta cần tạo 2 file: config/error/error.html.php và config/error/exception.html.php.

Trang 404 (page not found) có thể được chỉnh sửa bằng cách thay đổi thiết lập error_404_module và error_404_action:

# apps/frontend/config/settings.ymlall: .actions: error_404_module: default error_404_action: error404

Chỉnh sửa cấu trúc thư mục

Để cấu trúc tốt hơn và chuẩn hóa code của bạn, symfony cung cấp một thư mục mặc định với các tên được xác định trước. Nhưng đôi khi, bạn cần phải thay đổi cấu trúc thư mục do yêu cầu bắt buộc từ bên ngoài.

Cấu hình thư mục có thể thực hiện trong class config/ProjectConfiguration.class.php.

Thư mục Web Root

Trong một số web hosts, bạn không thể thay đổi tên thư mục web root. Bạn có thể sửa lại cấu hình để symfony biết tên thư mục là public_html/ thay vì web/:

// config/ProjectConfiguration.class.phpclass ProjectConfiguration extends sfProjectConfiguration{ public function setup() {

$this->setWebDir($this->getRootDir().'/public_html'); }}

Phương thức setWebDir() nhận một đường dẫn tuyệt đối đến thư mục web root. Nếu bạn di chuyển thư mục này đi nơi khác, đừng quên sửa controller scripts để kiểm tra rằng đường dẫn file ProjectConfiguration vẫn đúng:

require_once(dirname(__FILE__).'/../config/ProjectConfiguration.class.php');

Thư mục Cache và Log

Symfony framework chỉ ghi vào 2 thư mục: cache/ và log/. Vì lý do bảo mật, một số web hosts không cho phép ghi vào thư mục chính. Trong trường hợp này, bạn có thể chuyển những thư mục này vào nơi mà hệ thống cho phép ghi:

// config/ProjectConfiguration.class.phpclass ProjectConfiguration extends sfProjectConfiguration{ public function setup() { $this->setCacheDir('/tmp/symfony_cache'); $this->setLogDir('/tmp/symfony_logs'); }}

Như phương thức setWebDir(), phương thức setCacheDir() và setLogDir() nhận tham số là đường dẫn tuyệt đối đến thư mục cache/ và log/.

Factories

Trong suốt Jobeet tutorial, chúng ta đã nói về các đối tượng cơ bản trong symfony như sfUser, sfRequest, sfResponse, sfI18N, sfRouting, ... Những đối tượng này được tự động tạo, cấu hình, và quản lý bởi symfony framework. Chúng cũng có thể được truy cập thông qua sfContext object, và như nhiều thứ khác trong framework, chúng được cấu hình dựa trên file cấu hình: factories.yml. File này được cấu hình theo môi trường.

Khi sfContext khởi tạo core factories, nó đọc file factories.yml để lấy các tên class (class) và tham số (param) cung cấp cho quá trình khởi tạo:

response: class: sfWebResponse param: send_http_headers: false

Trong snippet trước, để tạo response factory, symfony khởi tạo một đối tượng sfWebResponse và cung cấp option send_http_headers như là một parameter.

Khả năng chỉnh sửa factories có nghĩa là bạn có thể sử dụng các class đã chỉnh sửa cho symfony core objects thay vì các class mặc định. Bạn cũng có thể thay đổi behavior mặc định của những class này bằng cách thay đổi tham số gửi cho chúng.

Hãy xem một vài chỉnh sửa có bản có thể bạn muốn thực hiện.

Tên Cookie

Để điều khiển user session, symfony sử dụng một cookie. Cookie này có tên mặc định là symfony, nó có thể đổi trong file factories.yml. Dưới key all, thêm cấu hình sau để đổi tên cookie thành jobeet:

# apps/frontend/config/factories.ymlstorage: class: sfSessionStorage param: session_name: jobeet

Nơi chứa Session

Session storage class mặc định là sfSessionStorage. Nó sử dụng file hệ thống để chứa thông tin session. Nếu bạn có vài web servers, bạn có thể muốn chứa sessions ở nơi khác, như bảng trong cơ sở dữ liệu:

# apps/frontend/config/factories.ymlstorage: class: sfPDOSessionStorage param: session_name: jobeet db_table: session database: propel db_id_col: id db_data_col: data db_time_col: time

Session Timeout

Mặc định, user session timeout nếu quá 1800 giây. Bạn có thể thay đổi bằng cách chỉnh sửa mục user:

# apps/frontend/config/factories.ymluser: class: myUser param: timeout: 1800

Log

Mặc định, không có log trong môi trường prod bởi vì tên logger class là sfNoLogger:

# apps/frontend/config/factories.ymlprod: logger: class: sfNoLogger param: level: err loggers: ~

Bạn có thể enable log trên file hệ thống bằng cách đổi tên logger class thành sfFileLogger:

# apps/frontend/config/factories.ymllogger: class: sfFileLogger param: level: error file: %SF_LOG_DIR%/%SF_APP%_%SF_ENVIRONMENT%.log

Trong file cấu hình factories.yml, chuỗi %XXX% được thay thế bằng giá trị tương ứng từ đối tượng sfConfig. Do đó, %SF_APP% trong file cấu hình tương đương với sfConfig::get('sf_app') trong PHP code. Chú ý này cũng có thể sử dụng trong file cấu hình app.yml. Nó có thể hữu ích khi bạn cần thay thế một đường dẫn trong file cấu hình mà không thể xác định rõ ràng (SF_ROOT_DIR, SF_WEB_DIR, ...).

Deploy

Deploy những gì?

Khi deploy Jobeet website lên production server, chúng ta cần cẩn thận không deploy những file không cần thiết hay override các file do người dùng đưa lên, như logo công ty.

Trong một symfony project, có 3 thư mục được loại trừ khi di chuyển: cache/, log/, và web/uploads/.

Vì lý do bảo mật, bạn có thể không muốn đưa lên "non-production" front controllers, như frontend_dev.php và frontend_cache.php scripts.

Deploying Strategies

Trong mục này, chúng tôi giả sử rằng bạn có toàn quyền quản lý production server(s). Nếu bạn chỉ có quyền truy cập server thông qua tài khoản FTP, thì chỉ có một giải pháp deployment là đưa tất cả các file lên mỗi khi bạn deploy.

Cách đơn giản nhất để deploy website của bạn là sử dụng lệnh có sẵn project:deploy. Nó sử dụng SSH và rsync để kết nối và chuyển những file từ máy tính đến nơi khác.

Servers trong lệnh project:deploy được cấu hình trong file cấu hình config/properties.ini:

# config/properties.ini[production] host=www.jobeet.org port=22 user=jobeet dir=/var/www/jobeet/ type=rsync pass=

Để deploy cho production server vừa mới cấu hình, sử dụng lệnh project:deploy:

$ php symfony project:deploy production

Trước khi chạy lệnh project:deploy lần đầu, bạn cần kết nối tới server để thêm key trong hosts file đã biết.

Nếu bạn chạy lệnh trên, symfony sẽ chỉ giả lập việc vận chuyển. Để thực sự deploy website, thêm option --go:

$ php symfony project:deploy production --go

Mặc dù bạn có thể cung cấp SSH password trong file properties.ini, nhưng tốt hơn nên cấu hình server của bạn với một SSH key để cho phép kết nối không dùng password.

Mặc định, symfony sẽ không chuyển các thư mục chúng ta đã nói phần trước, và không chuyển dev front controller script. Đó là do lệnh project:deploy loại trừ những files và thư mục được cấu hình trong file config/rsync_exclude.txt:

# config/rsync_exclude.txt.svn/web/uploads/*/cache/*/log/*/web/*_dev.php

Với Jobeet, chúng ta cần thêm file frontend_cache.php:

# config/rsync_exclude.txt.svn/web/uploads/*/cache/*/log/*/web/*_dev.php/web/frontend_cache.php

Bạn cũng có thể tạo file config/rsync_include.txt để bắt buộc việc chuyển file hoặc thư mục nào đó.

Mặc dù lệnh project:deploy rất uyển chuyển, nhưng có thể bạn vẫn muốn sửa nó. Do deploy có thể khác nhau tùy vào cấu hình của server và topology, nên bạn có thể cần extend task mặc định.

Mỗi khi bạn deploy một website thành production, đừng quên xóa cache cấu hình trên production server:

$ php symfony cc --type=config

Nếu bạn sửa một vài route, bạn cần xóa cache routing:

$ php symfony cc --type=routing

Xóa phần cache được chọn cho phép giữ lại các phần cache khác, như là template cache.

Hẹn gặp lại ngày mai

Deploy một dự án là bước cuối cùng trong vòng đời phát triển của một dự án symfony. Điều đó không có nghĩa là bạn đã xong. Bạn sẽ còn phải sửa lỗi và muốn thêm tính năng mới. Nhờ cấu trúc của symfony, việc nâng cấp website của bạn trở nên đơn giản, nhanh chóng và an toàn.

Ngày mai là ngày cuối cùng của Jobeet tutorial. Đó là thời gian để nhìn lại những gì chúng ta đã học được trong suốt 23 ngày qua.

Hôm nay chúng ta kết thúc chuyến hành trình thú vị trong thế giới symfony. Trong suốt 23 ngày qua, bạn đã được học symfony qua các ví dụ: từ các design patterns sử dụng trong framework, đến các tính năng mạnh mẽ có sẵn trong framework. Bạn chưa trở thành master về symfony, nhưng bạn đã có đầy đủ những kiến thức cần thiết để bắt đầu xây dựng ứng dụng với symfony.

Hãy tạm quên đi Jobeet, và cùng nhìn lại những tính năng của framework mà ta đã học trong suốt 3 tuần qua.

Symfony là gì?

Symfony framework là một tập các sub-framework riêng biệt được gắn kết lại với nhau, theo cấu trúc của một full-stack MVC framework (Model, View, Controller).

Trước khi tìm hiểu về code, hãy dành chút thời gian để đọc về lịch sử và triết lý của symfony. Sau đó, kiểm tra yêu cầu của framework sử dụng script check_configuration.php.

Cuối cùng, cài đặt symfony. Sau một thời gian, bạn sẽ muốn upgrade lên phiên bản mới nhất của framework.

Framework cũng cung cấp công cụ để dễ dàng deployment.

Model

Model là một phần của symfony được thực hiện nhờ Doctrine ORM. Dựa trên mô tả về database, nó sẽ tạo ra các class cho các object, form, và filter. Doctrine cũng tạo ra các câu SQL dùng để tạo các bảng trong database.

Cấu hình database có thể thực hiện thông qua lệnh hoặc chỉnh sửa file cấu hình. Ngoài việc cấu hình, ta còn có thể thêm các dữ liệu khởi tạo nhờ file fixture. Bạn có thể tạo những file này với dữ liệu động.

Đối tượng Doctrine có thể internationalized một cách dễ dàng.

View

Mặc định, View layer trong kiến trúc MVC sử dụng các file PHP làm templates.

Templates có thể sử dụng helpers cho các thao tác sử dụng thường xuyên như tạo một URL hay tạo một link.

Một template có thể sử dụng layout để thêm header và footer. Để views có thể dùng lại được, bạn có thể tạo các slots, partials, và components.

Để tăng tốc độ, bạn có thể sử dụng cache sub-framework để cache toàn bộ trang, một action, hoặc một partials hay components. Bạn cũng có thể tự xóa cache một cách dễ dàng.

Controller

Controller được quản lý bởi front controllers và actions.

Ta có thể dùng lệnh để tạo các module đơn giản, CRUD modules, thậm chí tạo admin modules với đầy đủ chức năng dựa vào các model class.

Admin modules cho phép bạn xây dựng đầy đủ chức năng admin cho ứng dụng mà không cần phải code một dòng nào.

Để ẩn đi công nghệ dùng để phát triển website, symfony sử dụng routing sub-framework giúp tạo ra các URL đẹp. Để giúp việc phát triển web services đơn giản hơn, symfony hỗ trợ nhiều định dạng khác nhau. Bạn cũng có thể tự tạo định dạng cho riêng mình.

Một action có thể forwarded hoặc redirected tới một action khác.

Cấu hình

Symfony framework giúp dễ dàng có những cấu hình khác nhau cho từng môi trường. Một môi trường là tập các thiết lập để phù hợp với development hay production servers. Bạn cũng có thể tự tạo môi trường riêng.

File cấu hình của symfony được xác định ở các mức khác nhau và tương ứng với từng môi trường:

app.yml cache.yml databases.yml factories.yml generator.yml routing.yml schema.yml security.yml settings.yml view.yml

File cấu hình sử dụng định dạng YAML.

Thay vì sử dụng cấu trúc thư mục mặc định và tổ chức ứng dụng của bạn theo các layer, bạn có thể tổ chức chúng theo chức năng và đóng gói lại thành một plugin. Với cấu trúc thư mục mặc định của symfony, bạn cũng có thể chỉnh sửa nó cho phù hợp với nhu cầu của mình.

Debug

Symfony cung cấp rất nhiều công cụ hữu ích để giúp lập trình viên tìm ra lỗi nhanh nhất có thể: từ file log đến web debug toolbar, và các exception rõ ràng.

Các Symfony Object chính

Symfony frameworks cung cấp một số object trừu tượng các đối tượng hay sử dụng trong web projects: request, response, user, logging, routing, và view cache manager.

Những object này được quản lý bởi sfContext object , và được cấu hình dựa trên factories.

Việc quản lý người dùng sử dụng authentication, authorization, flashes, và attributes lưu trong session.

Bảo mật

Symfony framework có sẵn tính năng bảo mật đối với XSS và CSRF attack. Những thiết lập này có thể được cấu hình từ dòng lệnh, hoặc chỉnh sửa file cấu hình.

Form framework cũng cung cấp sẵn các tính năng bảo mật.

Forms

Do quản lý form là một trong những thao tác phức tạp trong phát triển web, nên symfony cung cấp một form sub-framework để công việc trở nên đơn giản hơn. Form framework có sẵn rất nhiều widgets và validators. Một trong những điểm mạnh của form sub-framework là templates trở nên đơn giản và dễ chỉnh sửa.

Nếu bạn sử dụng Doctrine, form framework cũng dễ dàng tạo ra các forms và filters dựa trên models.

Internationalization và Localization

Symfony hỗ trợ sẵn Internationalization Và localization nhờ ICU standard. User culture xác định ngôn ngữ và quốc gia của người dùng. Nó có thể tạo bởi người dùng hoặc nhúng trong URL.

Test

Thư viện lime, sử dụng cho unit tests, cung cấp rất nhiều testing methods. Doctrine objects cũng có thể được test với một database riêng và các fixtures riêng.

Unit tests có thể chạy riêng biệt hoặc gộp chung lại.

Functional tests được thực hiện nhờ sfFunctionalTest class, sử dụng một trình duyệt giả lập cho phép kiểm tra các object của symfony thông qua các Tester. Có sẵn các Tester cho request object, response object, user object, current form object, cache layer và Doctrine objects.

Bạn cũng có thể sử dụng công cụ debug cho response và forms.

Cũng như unit tests, functional tests cũng có thể chạy riêng biệt hoặc gộp chung lại.

Bạn cũng có thể chạy tất cả các test thông qua một lệnh.

Plugins

Symfony framework chỉ cung cấp nền tảng cho ứng dụng web của bạn và dựa trên các plugins để cung cấp thêm các tính năng. Trong loạt bài hướng dẫn này, chúng ta đã nói về sfGuardPlugin, sfFormExtraPlugin, và sfTaskExtraPlugin.

Một plugin phải được kích hoạt sau khi cài đặt.

Plugins là cách tốt nhất để đóng góp cho dự án symfony.

Tasks

Symfony CLI cung cấp rất nhiều lệnh, và phần lớn các lệnh hữu ích đã được đề cập trong hướng dẫn này:

app:routes cache:clear configure:database generate:project generate:app generate:module help i18n:extract list plugin:install plugin:publish-assets project:deploy doctrine:build-all doctrine:build-all-reload doctrine:build-forms doctrine:build-model doctrine:build-sql doctrine:data-load doctrine:generate-admin doctrine:generate-module doctrine:insert-sql test:all test:coverage test:functional test:unit

Bạn cũng có thể tự tạo lệnh cho mình.

Hẹn gặp lại

Trước khi kết thúc, tôi muốn nói một điều cuối cùng về symfony. Framework có rất nhiều tính năng tốt và rất nhiều tài liệu miễn phí. Nhưng điều quan trọng tạo nên giá trị của Open-Source đó là cộng đồng. Và symfony đã có một cộng đồng đông đảo và năng động. Nếu bạn bắt đầu sử dụng symfony cho dự án của mình, đừng quên tham gia vào cộng đồng symfony:

Theo dõi user mailing-list Theo dõi blog feed trên trang chủ Theo dõi symfony planet feed Tham gia thảo luận trên #symfony IRC

Người dịch: Nguyễn Hữu Quân - huu2uan [at] gmail.com.