Traits

Sau khi nắm vững Polymorphism, bước tiếp theo là học Traits – cơ chế tái sử dụng code ngang hàng trong PHP. Khác với Inheritance (kế thừa dọc), Traits cho phép chia sẻ methods giữa các class không có quan hệ cha-con. Đây là giải pháp cho vấn đề PHP không hỗ trợ đa kế thừa.

1. Traits là gì?

Trait là cơ chế cho phép tái sử dụng code trong các class độc lập. Trait chứa methods có thể được “nhúng” vào class bằng từ khóa use.

1.1. Vấn đề cần giải quyết

<?php
// ❌ Vấn đề: Code lặp lại giữa các class không liên quan
class User {
  public function log($message) {
    error_log("[User] $message");
  }
}

class Product {
  public function log($message) {
    error_log("[Product] $message");
  }
}

class Order {
  public function log($message) {
    error_log("[Order] $message");
  }
}

// Method log() lặp lại 3 lần
// Không thể dùng Inheritance vì User, Product, Order không có quan hệ cha-con
?>

1.2. Giải pháp: Dùng Trait

<?php
// ✅ Giải pháp: Tạo Trait chứa code chung
trait Loggable {
  public function log($message) {
    error_log("[" . get_class($this) . "] $message");
  }
}

// Nhúng Trait vào các class
class User {
  use Loggable;  // Nhận method log()
  
  public function register() {
    $this->log("User registered");
  }
}

class Product {
  use Loggable;  // Nhận method log()
  
  public function create() {
    $this->log("Product created");
  }
}

class Order {
  use Loggable;  // Nhận method log()
  
  public function place() {
    $this->log("Order placed");
  }
}

// Sử dụng
$user = new User();
$user->register();  // [User] User registered

$product = new Product();
$product->create();  // [Product] Product created
?>
TRAITS - TÁI SỬ DỤNG CODE NGANG HÀNG TRAIT LOGGABLE function log($msg) { error_log($msg); use use use CLASS USER use Loggable; function register() { $this->log("User..."); CLASS PRODUCT use Loggable; function create() { $this->log("Product..."); CLASS ORDER use Loggable; function place() { $this->log("Order..."); 💡 1 Trait → Nhiều classes không liên quan (Không cần quan hệ kế thừa)
Hình 1: Traits - Tái sử dụng code ngang hàng

2. Cú pháp Trait

2.1. Khai báo Trait

<?php
// Khai báo Trait
trait TraitName {
  // Properties
  private $property;
  
  // Methods
  public function method1() {
    // Code
  }
  
  protected function method2() {
    // Code
  }
}

// Sử dụng Trait trong class
class ClassName {
  use TraitName;  // Nhúng Trait
  
  // Class code...
}
?>

2.2. Trait đơn giản

<?php
trait Timestampable {
  public function setCreatedAt() {
    $this->created_at = date("Y-m-d H:i:s");
  }
  
  public function setUpdatedAt() {
    $this->updated_at = date("Y-m-d H:i:s");
  }
  
  public function getCreatedAt() {
    return $this->created_at;
  }
}

class Post {
  use Timestampable;
  
  public $title;
  private $created_at;
  private $updated_at;
  
  public function __construct($title) {
    $this->title = $title;
    $this->setCreatedAt();
  }
  
  public function update() {
    $this->setUpdatedAt();
  }
}

$post = new Post("Hello World");
echo $post->getCreatedAt();  // 2026-05-07 12:30:45

$post->update();
?>

3. Nhiều Traits trong một Class

Class có thể sử dụng nhiều Traits cùng lúc.

<?php
trait Loggable {
  public function log($message) {
    error_log($message);
  }
}

trait Cacheable {
  private $cache = [];
  
  public function getCache($key) {
    return $this->cache[$key] ?? null;
  }
  
  public function setCache($key, $value) {
    $this->cache[$key] = $value;
  }
}

trait Validatable {
  public function validate($data, $rules) {
    foreach ($rules as $field => $rule) {
      if ($rule === 'required' && empty($data[$field])) {
        return false;
      }
    }
    return true;
  }
}

// Class sử dụng 3 Traits
class User {
  use Loggable, Cacheable, Validatable;
  
  public function register($data) {
    // Validate từ Validatable
    if (!$this->validate($data, ['email' => 'required', 'password' => 'required'])) {
      $this->log("Validation failed");  // Log từ Loggable
      return false;
    }
    
    // Cache từ Cacheable
    $this->setCache('last_user', $data['email']);
    
    $this->log("User {$data['email']} registered");
    return true;
  }
}

$user = new User();
$user->register(['email' => 'user@email.com', 'password' => 'password123']);
?>
SỬ DỤNG NHIỀU TRAITS LOGGABLE log($msg) CACHEABLE cache($k,$v) VALIDATABLE validate($d) use use use CLASS USER use Loggable, Cacheable, Validatable; ✅ $this->log("..."); ✅ $this->cache($key, $value); ✅ $this->validate($data); 💡 1 Class có thể use nhiều Traits
Hình 2: Sử dụng nhiều Traits trong một Class

4. Trait Conflict (Xung đột Trait)

Khi 2 Traits có method cùng tên, cần giải quyết xung đột bằng insteadof hoặc as.

4.1. Xung đột và cách giải quyết

<?php
trait Logger {
  public function log($message) {
    echo "Logger: $message
"; } } trait FileLogger { public function log($message) { echo "FileLogger: $message
"; } } // ❌ Lỗi: 2 Traits có method log() giống nhau // class User { // use Logger, FileLogger; // Fatal error: Trait method log has not been applied // } // ✅ Giải quyết 1: Chọn method từ Trait nào class User { use Logger, FileLogger { Logger::log insteadof FileLogger; // Dùng log() từ Logger } public function register() { $this->log("User registered"); // Gọi Logger::log() } } // ✅ Giải quyết 2: Đổi tên method class Admin { use Logger, FileLogger { Logger::log insteadof FileLogger; // Mặc định dùng Logger::log() FileLogger::log as fileLog; // Đổi tên FileLogger::log() thành fileLog() } public function action() { $this->log("Admin action"); // Gọi Logger::log() $this->fileLog("Save to file"); // Gọi FileLogger::log() } } $user = new User(); $user->register(); // Logger: User registered $admin = new Admin(); $admin->action(); // Logger: Admin action // FileLogger: Save to file ?>

4.2. Thay đổi access modifier

<?php
trait Helper {
  public function process() {
    echo "Processing...";
  }
}

class Service {
  use Helper {
    process as protected;  // Đổi public → protected
  }
  
  public function execute() {
    $this->process();  // OK - gọi trong class
  }
}

$service = new Service();
$service->execute();  // OK
// $service->process();  // Lỗi - protected
?>

5. Trait có thể sử dụng Trait khác

<?php
trait Timestampable {
  public function setTimestamp() {
    $this->timestamp = time();
  }
}

trait Loggable {
  use Timestampable;  // Trait sử dụng Trait khác
  
  public function log($message) {
    $this->setTimestamp();  // Từ Timestampable
    echo "[" . date("Y-m-d H:i:s", $this->timestamp) . "] $message
"; } } class Application { use Loggable; // Tự động có cả Timestampable private $timestamp; public function start() { $this->log("Application started"); } } $app = new Application(); $app->start(); // [2026-05-07 12:30:45] Application started ?>

6. Trait vs Inheritance

TRAITS vs INHERITANCE INHERITANCE (Dọc) PARENT class Animal extends CHILD class Dog Đặc điểm: • Quan hệ cha-con (IS-A) • 1 parent class duy nhất • Dọc (vertical) • Dog IS-A Animal • extends TRAITS (Ngang) Trait A Trait B use CLASS class MyClass Đặc điểm: • Không có quan hệ cha-con • Nhiều traits • Ngang (horizontal) • Chia sẻ methods • use 💡 Inheritance = Quan hệ cha-con | Traits = Chia sẻ code
Hình 3: So sánh Traits và Inheritance
Đặc điểmTraitInheritance
Quan hệNgang hàng (horizontal)Dọc (vertical – cha-con)
Số lượngNhiều Traits1 parent class
Từ khóauseextends
Tạo object❌ Không✅ Có (nếu không abstract)
Mục đíchChia sẻ methodsQuan hệ IS-A
Khi nào dùngCode chung, không IS-AQuan hệ cha-con rõ ràng
<?php
// Inheritance - Quan hệ IS-A
class Animal {
  public function breathe() {
    echo "Breathing";
  }
}

class Dog extends Animal {  // Dog IS-A Animal
  // Kế thừa breathe()
}

// Trait - Chia sẻ code không có IS-A
trait Swimmable {
  public function swim() {
    echo "Swimming";
  }
}

class Fish {
  use Swimmable;  // Fish CAN swim (không phải IS-A Swimmable)
}

class Duck {
  use Swimmable;  // Duck CAN swim (không phải IS-A Swimmable)
}

// Fish và Duck không có quan hệ cha-con, nhưng đều swim()
?>

7. Ví dụ thực tế

7.1. Trait Singleton

<?php
trait Singleton {
  private static $instance;
  
  public static function getInstance() {
    if (self::$instance === null) {
      self::$instance = new self();
    }
    return self::$instance;
  }
  
  // Chặn tạo object từ bên ngoài
  private function __construct() {}
  private function __clone() {}
  public function __wakeup() {
    throw new Exception("Cannot unserialize singleton");
  }
}

class Database {
  use Singleton;
  
  private $conn;
  
  private function __construct() {
    $this->conn = new PDO("mysql:host=localhost;dbname=test", "root", "");
    echo "Database connected
"; } public function query($sql) { return $this->conn->query($sql); } } class Config { use Singleton; private $settings = []; private function __construct() { $this->settings = ['debug' => true, 'timezone' => 'Asia/Ho_Chi_Minh']; echo "Config loaded
"; } public function get($key) { return $this->settings[$key] ?? null; } } // Sử dụng $db1 = Database::getInstance(); $db2 = Database::getInstance(); var_dump($db1 === $db2); // true - cùng 1 instance $config = Config::getInstance(); echo $config->get('timezone'); // Asia/Ho_Chi_Minh ?>

7.2. Trait CRUD

<?php
trait CRUD {
  protected $table;
  protected $conn;
  
  public function getAll() {
    $stmt = $this->conn->query("SELECT * FROM {$this->table}");
    return $stmt->fetchAll(PDO::FETCH_ASSOC);
  }
  
  public function getById($id) {
    $stmt = $this->conn->prepare("SELECT * FROM {$this->table} WHERE id = ?");
    $stmt->execute([$id]);
    return $stmt->fetch(PDO::FETCH_ASSOC);
  }
  
  public function create($data) {
    $fields = implode(', ', array_keys($data));
    $placeholders = implode(', ', array_fill(0, count($data), '?'));
    
    $sql = "INSERT INTO {$this->table} ($fields) VALUES ($placeholders)";
    $stmt = $this->conn->prepare($sql);
    $stmt->execute(array_values($data));
    
    return $this->conn->lastInsertId();
  }
  
  public function update($id, $data) {
    $fields = implode(' = ?, ', array_keys($data)) . ' = ?';
    
    $sql = "UPDATE {$this->table} SET $fields WHERE id = ?";
    $stmt = $this->conn->prepare($sql);
    
    $values = array_values($data);
    $values[] = $id;
    
    return $stmt->execute($values);
  }
  
  public function delete($id) {
    $stmt = $this->conn->prepare("DELETE FROM {$this->table} WHERE id = ?");
    return $stmt->execute([$id]);
  }
}

class User {
  use CRUD;
  
  public function __construct($conn) {
    $this->table = 'users';
    $this->conn = $conn;
  }
}

class Product {
  use CRUD;
  
  public function __construct($conn) {
    $this->table = 'products';
    $this->conn = $conn;
  }
}

// Sử dụng
$pdo = new PDO("mysql:host=localhost;dbname=test", "root", "");

$userModel = new User($pdo);
$users = $userModel->getAll();

$productModel = new Product($pdo);
$product = $productModel->getById(1);

// Cả 2 class đều có CRUD methods từ Trait
?>

7.3. Trait Validation

<?php
trait Validatable {
  private $errors = [];
  
  protected function validateRequired($field, $value) {
    if (empty($value)) {
      $this->errors[$field] = "$field is required";
      return false;
    }
    return true;
  }
  
  protected function validateEmail($field, $value) {
    if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
      $this->errors[$field] = "$field must be a valid email";
      return false;
    }
    return true;
  }
  
  protected function validateMinLength($field, $value, $min) {
    if (strlen($value) < $min) {
      $this->errors[$field] = "$field must be at least $min characters";
      return false;
    }
    return true;
  }
  
  protected function validateNumeric($field, $value) {
    if (!is_numeric($value)) {
      $this->errors[$field] = "$field must be a number";
      return false;
    }
    return true;
  }
  
  public function getErrors() {
    return $this->errors;
  }
  
  public function hasErrors() {
    return !empty($this->errors);
  }
}

class RegisterForm {
  use Validatable;
  
  public function validate($data) {
    $this->errors = [];  // Reset errors
    
    $this->validateRequired('name', $data['name'] ?? '');
    $this->validateMinLength('name', $data['name'] ?? '', 3);
    
    $this->validateRequired('email', $data['email'] ?? '');
    $this->validateEmail('email', $data['email'] ?? '');
    
    $this->validateRequired('age', $data['age'] ?? '');
    $this->validateNumeric('age', $data['age'] ?? '');
    
    return !$this->hasErrors();
  }
}

class ProductForm {
  use Validatable;
  
  public function validate($data) {
    $this->errors = [];
    
    $this->validateRequired('name', $data['name'] ?? '');
    $this->validateRequired('price', $data['price'] ?? '');
    $this->validateNumeric('price', $data['price'] ?? '');
    
    return !$this->hasErrors();
  }
}

// Sử dụng
$registerForm = new RegisterForm();
$isValid = $registerForm->validate([
  'name' => 'An',
  'email' => 'invalid-email',
  'age' => 'abc'
]);

if (!$isValid) {
  print_r($registerForm->getErrors());
  // Array (
  //   [name] => name must be at least 3 characters
  //   [email] => email must be a valid email
  //   [age] => age must be a number
  // )
}
?>

7.4. Trait Soft Delete

<?php
trait SoftDelete {
  public function softDelete($id) {
    $sql = "UPDATE {$this->table} SET deleted_at = NOW() WHERE id = ?";
    $stmt = $this->conn->prepare($sql);
    return $stmt->execute([$id]);
  }
  
  public function restore($id) {
    $sql = "UPDATE {$this->table} SET deleted_at = NULL WHERE id = ?";
    $stmt = $this->conn->prepare($sql);
    return $stmt->execute([$id]);
  }
  
  public function forceDelete($id) {
    $sql = "DELETE FROM {$this->table} WHERE id = ?";
    $stmt = $this->conn->prepare($sql);
    return $stmt->execute([$id]);
  }
  
  public function getActive() {
    $sql = "SELECT * FROM {$this->table} WHERE deleted_at IS NULL";
    $stmt = $this->conn->query($sql);
    return $stmt->fetchAll(PDO::FETCH_ASSOC);
  }
  
  public function getTrashed() {
    $sql = "SELECT * FROM {$this->table} WHERE deleted_at IS NOT NULL";
    $stmt = $this->conn->query($sql);
    return $stmt->fetchAll(PDO::FETCH_ASSOC);
  }
}

class Post {
  use SoftDelete;
  
  protected $table = 'posts';
  protected $conn;
  
  public function __construct($conn) {
    $this->conn = $conn;
  }
}

// Database schema
// CREATE TABLE posts (
//   id INT PRIMARY KEY,
//   title VARCHAR(255),
//   deleted_at TIMESTAMP NULL
// );

$post = new Post($pdo);
$post->softDelete(1);        // Đánh dấu xóa
$post->restore(1);           // Khôi phục
$post->forceDelete(1);       // Xóa vĩnh viễn

$activePosts = $post->getActive();     // Chỉ lấy chưa xóa
$trashedPosts = $post->getTrashed();   // Chỉ lấy đã xóa
?>

8. Best Practices

8.1. Khi nào dùng Trait

NÊN dùng Trait khi:

  • Muốn chia sẻ methods giữa các class không liên quan
  • Code chung nhưng không có quan hệ IS-A
  • Tránh lặp code mà không muốn kế thừa
  • Cần “mix-in” nhiều behaviors

KHÔNG nên dùng Trait khi:

  • Có quan hệ IS-A rõ ràng (dùng Inheritance)
  • Chỉ 1-2 methods đơn giản (dùng function helper)
  • Cần polymorphism (dùng Interface)
  • Trait quá phức tạp (nên tách thành class riêng)

8.2. Đặt tên Trait

<?php
// ✅ Đúng - Tên rõ nghĩa, -able/-ible
trait Loggable { }
trait Cacheable { }
trait Timestampable { }
trait Validatable { }
trait Sortable { }

// ❌ Sai - Tên mơ hồ
trait Helper { }
trait Utility { }
trait Common { }
?>

8.3. Trait không nên quá phức tạp

<?php
// ❌ Sai - Trait quá phức tạp
trait GodTrait {
  // 50+ methods
  // Properties phức tạp
  // Logic nghiệp vụ nặng
  // → Nên tách thành class riêng
}

// ✅ Đúng - Trait đơn giản, tập trung
trait Loggable {
  public function log($message) {
    error_log($message);
  }
}
?>

Tóm tắt

Qua bài này bạn đã nắm được:

  • Trait: Cơ chế tái sử dụng code ngang hàng
  • Từ khóa use: Nhúng Trait vào class
  • Nhiều Traits: use Trait1, Trait2, Trait3;
  • Conflict: Giải quyết bằng insteadofas
  • Access modifier: Thay đổi bằng as protected/private
  • Trait dùng Trait: Trait có thể use Trait khác
  • Không tạo object: Trait không phải class
  • Khác Inheritance: Ngang hàng vs Dọc, nhiều vs 1

Bài tiếp theo, bạn sẽ học về Static Methods và Properties – truy cập không cần object.

🎯 Bài tập thực hành

  1. Bài 1: Tạo Trait Sluggable với method generateSlug($text) chuyển text thành URL slug. Áp dụng vào class Post và Category.
  2. Bài 2: Tạo Trait JsonSerializable với methods toJson() và fromJson($json). Áp dụng vào class User, Product có thể export/import JSON.
  3. Bài 3: Tạo 2 Traits: FileLogger (log vào file) và DatabaseLogger (log vào DB) đều có method log(). Tạo class Application dùng cả 2, giải quyết conflict.

Gợi ý Bài 1:

<?php
trait Sluggable {
  public function generateSlug($text) {
    // Chuyển về lowercase
    $text = strtolower($text);
    
    // Chuyển tiếng Việt không dấu
    $text = preg_replace('/[àáạảãâầấậẩẫăằắặẳẵ]/u', 'a', $text);
    $text = preg_replace('/[èéẹẻẽêềếệểễ]/u', 'e', $text);
    $text = preg_replace('/[ìíịỉĩ]/u', 'i', $text);
    $text = preg_replace('/[òóọỏõôồốộổỗơờớợởỡ]/u', 'o', $text);
    $text = preg_replace('/[ùúụủũưừứựửữ]/u', 'u', $text);
    $text = preg_replace('/[ỳýỵỷỹ]/u', 'y', $text);
    $text = preg_replace('/đ/u', 'd', $text);
    
    // Thay khoảng trắng và ký tự đặc biệt bằng -
    $text = preg_replace('/[^a-z0-9]+/', '-', $text);
    
    // Xóa - đầu cuối
    $text = trim($text, '-');
    
    return $text;
  }
}

class Post {
  use Sluggable;
  
  public $title;
  public $slug;
  
  public function setTitle($title) {
    $this->title = $title;
    $this->slug = $this->generateSlug($title);
  }
}

class Category {
  use Sluggable;
  
  public $name;
  public $slug;
  
  public function setName($name) {
    $this->name = $name;
    $this->slug = $this->generateSlug($name);
  }
}

$post = new Post();
$post->setTitle("Học lập trình PHP không khó");
echo $post->slug;  // hoc-lap-trinh-php-khong-kho

$category = new Category();
$category->setName("Công nghệ thông tin");
echo $category->slug;  // cong-nghe-thong-tin
?>

Gợi ý Bài 2:

<?php
trait JsonSerializable {
  public function toJson() {
    $data = [];
    
    // Lấy public properties
    foreach (get_object_vars($this) as $key => $value) {
      $data[$key] = $value;
    }
    
    return json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
  }
  
  public function fromJson($json) {
    $data = json_decode($json, true);
    
    foreach ($data as $key => $value) {
      if (property_exists($this, $key)) {
        $this->$key = $value;
      }
    }
    
    return $this;
  }
}

class User {
  use JsonSerializable;
  
  public $id;
  public $name;
  public $email;
  
  public function __construct($id = null, $name = null, $email = null) {
    $this->id = $id;
    $this->name = $name;
    $this->email = $email;
  }
}

// Export
$user = new User(1, "Nguyễn Văn A", "a@email.com");
$json = $user->toJson();
echo $json;

// Import
$newUser = new User();
$newUser->fromJson($json);
echo $newUser->name;  // Nguyễn Văn A
?>

Để lại bình luận

Email của bạn sẽ không được hiển thị.