Caution: You are browsing the legacy symfony 1.x part of this website.

Ngày 3: Data Model

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.

Sử dụng sfDoctrinePlugin

Nếu bạn đang đọc hướng dẫn này, có nghĩa là bạn đã quyết định sử dụng Doctrine ORM thay cho Propel. Đầu tiên bạn cần enable sfDoctrinePlugin và disable sfPropelPlugin. Để thực hiện điều này, bạn cần thêm đoạn code sau vào file config/ProjectConfiguration.class.php.

public function setup()
{
  $this->enablePlugins(array('sfDoctrinePlugin'));
  $this->disablePlugins(array('sfPropelPlugin'));
}

Nếu bạn muốn tất cả các plugin đều được enabled , bạn có thể viết:

public function setup()
{
  $this->enableAllPluginsExcept(array('sfPropelPlugin', 'sfCompat10Plugin'));
}

note

Sau thay đổi này bạn sẽ gặp một lỗi khi chúng ta cấu hình file config/databases.yml và chúng ta sẽ sử dụng sfDoctrineDatabase.

Xóa cache để thay đổi có hiệu lực.

$ php symfony cc

Như chúng ta sẽ thấy sau này, mỗi plugin có thể có chứa các assets (javascripts, stylesheets, and images). Sau khi cài đặt hay enable một plugin, chúng ta cần cài đặt chúng bằng task plugin:publish-assets:

$ php symfony plugin:publish-assets

Chúng ta cũng cần xóa thư mục web/sfPropelPlugin:

$ rm web/sfPropelPlugin

tip

Khi sử dụng Doctrine thay cho Propel bạn có thể xóa file config/propel.iniconfig/schema.yml.

$ rm config/propel.ini
$ rm config/schema.yml

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:

Entity relationship diagram

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ụ: PropelDoctrine. Trong hướng dẫn này, chúng ta sẽ sử dụng Doctrine.

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

note

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 sfDbDesignerPlugin, bạn có thể convert thành file schema cho doctrine). (note này do người dịch thêm vào)

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/doctrine/schema.yml:

tip

Bạn cần tự tạo thêm tư mục config/doctrine/ trong project do nó chưa có sẵn:

$ mkdir config/doctrine
# config/doctrine/schema.yml
---
JobeetCategory:
  actAs: { Timestampable: ~ }
  columns:
    name: { type: string(255), notnull: true, unique: true }
 
JobeetJob:
  actAs: { Timestampable: ~ }
  columns:
    category_id:  { type: integer, notnull: true }
    type:         { type: string(255) }
    company:      { type: string(255), notnull: true }
    logo:         { type: string(255) }
    url:          { type: string(255) }
    position:     { type: string(255), notnull: true }
    location:     { type: string(255), notnull: true }
    description:  { type: string(4000), notnull: true }
    how_to_apply: { type: string(4000), notnull: true }
    token:        { type: string(255), notnull: true, unique: true }
    is_public:    { type: boolean, notnull: true, default: 1 }
    is_activated: { type: boolean, notnull: true, default: 0 }
    email:        { type: string(255), notnull: true }
    expires_at:   { type: timestamp, notnull: true }
  relations:
    JobeetCategory: { onDelete: CASCADE, local: category_id, foreign: id, foreignAlias: JobeetJobs } 
 
JobeetAffiliate:
  actAs: { Timestampable: ~ }
  columns:
    url:       { type: string(255), notnull: true }
    email:     { type: string(255), notnull: true, unique: true }
    token:     { type: string(255), notnull: true }
    is_active: { type: boolean, notnull: true, default: 0 }
  relations:
    JobeetCategories:
      class: JobeetCategory
      refClass: JobeetCategoryAffiliate
      local: affiliate_id
      foreign: category_id
      foreignAlias: JobeetAffiliates
 
JobeetCategoryAffiliate:
  columns:
    category_id:  { type: integer, primary: true }
    affiliate_id: { type: integer, primary: true }
  relations:
    JobeetCategory:  { onDelete: CASCADE, local: category_id, foreign: id }
    JobeetAffiliate: { onDelete: CASCADE, local: affiliate_id, foreign: id }

tip

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 doctrine:build-schema.Lệnh này sẽ chuyển đổi trực tiếp từ lược đồ quan hệ sang YAML format.

sidebar

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 của cột (boolean, integer, float, decimal, string, array, object, blob, clob, timestamp, time, date, enum, gzip)
  • notnull: true nếu cột đó là bắt buộc
  • unique: true nếu bạn muốn tạo một unique index cho cột.

note

Thuộc tính onDelete xác định ứng xử khi ON DELETE của khóa ngoài, và Doctrine hỗ trợ CASCADE, SET NULL, và RESTRICT. Ví dụ, khi một job record bị xóa, tất cả các jobeet_category_affiliate liên quan đến records này sẽ tự động được xóa theo.

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

note

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:

Mặc định, config/databases.yml chứa cấu hình cho propel. Do chúng ta sử dụng Doctrine, nên chúng ta cần xóa file config/databases.yml để có thể tạo lại cho Doctrine.

$ rm config/databases.yml

Bây giờ, chạy lệnh dưới đây để tạo cấu hình tới database cho Doctrine:

$ php symfony configure:database --name=doctrine --class=sfDoctrineDatabase "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

note

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 Doctrine để sinh các câu SQL cần thiết để tạo các bảng trong cơ sở dữ liệu:

Để tạo câu SQL, trước tiên bạn cầu build model từ file schema.

$ php symfony doctrine:build-model

Sau khi đã có model, bạn có thể tạo và insert SQL.

$ php symfony doctrine:build-sql

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

# snippet from data/sql/schema.sql
CREATE TABLE jobeet_category (id BIGINT AUTO_INCREMENT, name VARCHAR(255)
NOT NULL COMMENT 'test', created_at DATETIME, updated_at DATETIME, slug
VARCHAR(255), UNIQUE INDEX sluggable_idx (slug), PRIMARY KEY(id))
ENGINE = INNODB;

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

$ php symfony doctrine:insert-sql

tip

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 doctrine: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 doctrine:build-model

Lệnh doctrine: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 Doctrine tạo 3 classes cho 1 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 trong bảng jobeet_job . Ban đầu, class chưa có gì.
  • BaseJobeetJob: parent class của JobeetJob. Mỗi khi bạn chạy lệnh doctrine:build-model, class này sẽ thay đổi, vì thế các thao tác thực hiện bạn phải để trong class JobeetJob.

  • JobeetJobTable: class chứa các phương thức trả về tập các JobeetJob objects. Ban đầu, class chưa có gì.

Giá trị của các cột có thể được truy cập thông qua các phương thức get*()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 doctrine: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 doctrine: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.

tip

Lệnh doctrine:build-all-reload bao gồm lệnh doctrine:build-alldoctrine: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 doctrine: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

tip

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 'doctrine:data-load` để load chúng vào database:

# data/fixtures/categories.yml
JobeetCategory:
  design:
    name: Design
  programming:
    name: Programming
  manager:
    name: Manager
  administrator:
    name: Administrator
 
# data/fixtures/jobs.yml
JobeetJob:
  job_sensio_labs:
    JobeetCategory: programming
    type:         full-time
    company:      Sensio Labs
    logo:         /uploads/jobs/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:   '2008-10-10'
 
  job_extreme_sensio:
    JobeetCategory:  design
    type:         part-time
    company:      Extreme Sensio
    logo:         /uploads/jobs/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:   '2008-10-10'

note

job fixture file sử dụng 2 ảnh. Bạn có thể download chúng (/get/jobeet/sensio-labs.gif, /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.

note

Propel yêu cầu tên file fixtures cần có số ở trước để quyết định thứ tự file sẽ được load. Với Doctrine điều này là không cần thiết vì tất cả các file fixtures sẽ được load và lưu theo đúng thứ tự.

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_atupdated_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 doctrine:data-load:

$ php symfony doctrine: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 doctrine:generate-module --with-show --non-verbose-templates frontend job JobeetJob

Lệnh doctrine: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 module
templates/ 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 table
show hiển thị các fields của 1 record
new hiển thị form để tạo một record mới
create tạo một record mới
edit hiển thị form để sửa một record
update cập nhật một record thông qua các giá trị người dùng chỉnh sửa
delete 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

Job module

Nếu bạn thử sửa thông tin của một công việc, bạn có thể thấy rằng Category id hiện ra danh sách tên các category. Các tên này được trả về bởi phương thức __toString(). Doctrine cung cấp sẵn phương thức __toString() trả về tên của cột: title, name, subject, ... Nếu bạn muốn chỉnh sửa giá trị trả về bạn cần thêm phương thức __toString() như sau.

// lib/model/JobeetJob.php
class JobeetJob extends BaseJobeetJob
{
  public function __toString()
  {
    return sprintf('%s at %s (%s)', $this->getPosition(), $this->getCompany(), $this->getLocation());
  }
}
 
// lib/model/JobeetAffiliate.php
class 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.

validation

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/doctrine/tags/release_day_03/ jobeet/