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

Ngày 13: User

Language
ORM

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.

Flashes

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.php
public 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.', date('m/d/Y', strtotime($job->getExpiresAt()))));
 
  $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 noticeerror 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.

note

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.php
class 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);
  }
 
  // ...
}

note

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.

note

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.php
class jobActions extends sfActions
{
  public function executeShow(sfWebRequest $request)
  {
    $this->job = $this->getRoute()->getObject();
 
    $this->getUser()->addJobToHistory($this->job);
  }
 
  // ...
}
 
// apps/frontend/lib/myUser.class.php
class 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.php
class myUser extends sfBasicSecurityUser
{
  public function getJobHistory()
  {
    $ids = $this->getAttribute('job_history', array());
 
    if (!empty($ids))
    {
      return Doctrine::getTable('JobeetJob')
        ->createQuery('a')
        ->whereIn('a.id', $ids)
        ->execute();
    } else {
      return array();
    }
  }
 
  // ...
}

Để 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;
}

Job history

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.php
class 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()setAttribute() là cách viết tắt của getParameterHolder()->get()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.

note

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.yml
default:
  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.

Login

tip

Trong file YAML, môt giá trị boolean xác định bằng chuỗi truefalse, hoặc onoff.

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.

Web debug

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

note

Không được thiết lập secure cho action login để tránh đệ quy.

tip

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: off
all:
  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()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.

sidebar

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 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 credential
echo $user->hasCredential('foo');                      =>   true
 
// Check if the user has both credentials
echo $user->hasCredential(array('foo', 'bar'));        =>   true
 
// Check if the user has one of the credentials
echo $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 sfDoctrineGuardPlugin để secure cho backend application:

$ php symfony plugin:install sfDoctrineGuardPlugin

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.

note

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.

Trang plugin liệt kê tất cả các phiên bản được nhóm theo version của symfony.

Do một plugin được chứa trong một thư mục, bạn cũng có thể download package từ symfony website và giải nén nó, hoặc dùng svn:externals link tới 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 doctrine:build-all-reload

tip

Lệnh doctrine: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 sfDoctrineGuardPlugin 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.php
class myUser extends sfGuardSecurityUser
{
}

sfDoctrineGuardPlugin cung cấp một action mặc định để authenticate users:

// apps/backend/config/settings.yml
all:
  .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.

sfGuardPlugin login

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

tip

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>

tip

Để 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.yml
all:
  .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>

Backend menu

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.

note

sfTesterUser tester cũng cung cấp phương thức isAuthenticated()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 sfDoctrineGuardPlugin , 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.

This work is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.