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
?>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']);
?>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
| Đặc điểm | Trait | Inheritance |
|---|---|---|
| Quan hệ | Ngang hàng (horizontal) | Dọc (vertical – cha-con) |
| Số lượng | Nhiều Traits | 1 parent class |
| Từ khóa | use | extends |
| Tạo object | ❌ Không | ✅ Có (nếu không abstract) |
| Mục đích | Chia sẻ methods | Quan hệ IS-A |
| Khi nào dùng | Code chung, không IS-A | Quan 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
insteadofvàas - ✅ 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
- Bài 1: Tạo Trait
Sluggablevới method generateSlug($text) chuyển text thành URL slug. Áp dụng vào class Post và Category. - Bài 2: Tạo Trait
JsonSerializablevới methods toJson() và fromJson($json). Áp dụng vào class User, Product có thể export/import JSON. - 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
?>