PHP Fundamentals

Traits & Magic Methods

13 min Lesson 25 of 45

Understanding Traits

Traits are a mechanism for code reuse in PHP. They allow you to share methods across multiple classes without using inheritance. Think of traits as "copy and paste" code that you can include in multiple classes.

Why Use Traits?
  • Code Reuse: Share common functionality across unrelated classes
  • Avoid Inheritance: PHP doesn't support multiple inheritance, but traits provide similar benefits
  • Composition: Build classes by combining multiple traits
  • Flexibility: Mix and match functionality as needed

Creating and Using Traits

Let's create a simple trait and use it in multiple classes:

<?php // Define a trait trait Timestampable { protected $createdAt; protected $updatedAt; public function setCreatedAt() { $this->createdAt = date("Y-m-d H:i:s"); } public function setUpdatedAt() { $this->updatedAt = date("Y-m-d H:i:s"); } public function getCreatedAt() { return $this->createdAt; } public function getUpdatedAt() { return $this->updatedAt; } public function touch() { $this->setUpdatedAt(); } } // Use trait in a class class User { use Timestampable; // Include the trait private $name; private $email; public function __construct($name, $email) { $this->name = $name; $this->email = $email; $this->setCreatedAt(); $this->setUpdatedAt(); } public function updateEmail($email) { $this->email = $email; $this->touch(); // Update timestamp } } class Post { use Timestampable; // Same trait in different class private $title; private $content; public function __construct($title, $content) { $this->title = $title; $this->content = $content; $this->setCreatedAt(); $this->setUpdatedAt(); } public function updateContent($content) { $this->content = $content; $this->touch(); } } // Both classes have timestamp functionality $user = new User("Ahmed", "ahmed@example.com"); echo $user->getCreatedAt(); $post = new Post("PHP Traits", "Content about traits..."); echo $post->getCreatedAt(); ?>
Trait Syntax: Use the use keyword inside a class to include a trait. You can use multiple traits in a single class.

Using Multiple Traits

A class can use multiple traits:

<?php trait Loggable { protected $logs = []; public function log($message) { $this->logs[] = [ "message" => $message, "timestamp" => date("Y-m-d H:i:s") ]; } public function getLogs() { return $this->logs; } } trait Validatable { protected $errors = []; public function addError($field, $message) { $this->errors[$field][] = $message; } public function hasErrors() { return !empty($this->errors); } public function getErrors() { return $this->errors; } public function clearErrors() { $this->errors = []; } } class Product { use Timestampable, Loggable, Validatable; // Multiple traits private $name; private $price; public function __construct($name, $price) { $this->name = $name; $this->price = $price; $this->setCreatedAt(); $this->log("Product created"); } public function setPrice($price) { if ($price < 0) { $this->addError("price", "Price cannot be negative"); return false; } $this->price = $price; $this->touch(); $this->log("Price updated to {$price}"); return true; } } $product = new Product("Laptop", 999); $product->setPrice(-100); if ($product->hasErrors()) { print_r($product->getErrors()); } print_r($product->getLogs()); ?>

Trait Conflict Resolution

When two traits have methods with the same name, you need to resolve the conflict:

<?php trait FormatHelper { public function format($text) { return strtoupper($text); } } trait HtmlHelper { public function format($text) { return htmlspecialchars($text); } } class TextProcessor { use FormatHelper, HtmlHelper { // Resolve conflict: use HtmlHelper's format instead of FormatHelper's HtmlHelper::format insteadof FormatHelper; // Give FormatHelper's format an alias FormatHelper::format as formatUpper; } } $processor = new TextProcessor(); echo $processor->format("<b>Hello</b>"); // Uses HtmlHelper: &lt;b&gt;Hello&lt;/b&gt; echo $processor->formatUpper("hello"); // Uses FormatHelper: HELLO ?>

Understanding Magic Methods

Magic methods are special methods that are automatically called in specific situations. They all start with double underscores (__).

Common Magic Methods:
  • __construct(): Called when creating an object
  • __destruct(): Called when an object is destroyed
  • __get(): Called when accessing inaccessible properties
  • __set(): Called when setting inaccessible properties
  • __isset(): Called when checking if a property is set
  • __unset(): Called when unsetting a property
  • __call(): Called when invoking inaccessible methods
  • __toString(): Called when object is used as a string

__get() and __set() Magic Methods

These methods handle property access dynamically:

<?php class DynamicProperties { private $data = []; // Called when trying to read an inaccessible property public function __get($name) { echo "Getting property: {$name}\n"; return $this->data[$name] ?? null; } // Called when trying to write to an inaccessible property public function __set($name, $value) { echo "Setting property: {$name} = {$value}\n"; $this->data[$name] = $value; } // Called when checking if property is set public function __isset($name) { return isset($this->data[$name]); } // Called when unsetting a property public function __unset($name) { unset($this->data[$name]); } } $obj = new DynamicProperties(); $obj->name = "Ahmed"; // Calls __set() echo $obj->name; // Calls __get() var_dump(isset($obj->name)); // Calls __isset() unset($obj->name); // Calls __unset() ?>

__call() Magic Method

Handle calls to undefined methods:

<?php class FluentQuery { private $conditions = []; // Called when invoking an inaccessible method public function __call($name, $arguments) { // Handle where conditions dynamically if (str_starts_with($name, "where")) { $field = lcfirst(substr($name, 5)); // whereUsername -> username $this->conditions[$field] = $arguments[0]; return $this; // For method chaining } // Handle order by dynamically if (str_starts_with($name, "orderBy")) { $field = lcfirst(substr($name, 7)); $this->conditions["order"] = $field; return $this; } throw new Exception("Method {$name} does not exist"); } public function getConditions() { return $this->conditions; } } $query = new FluentQuery(); $query->whereUsername("ahmed") ->whereAge(25) ->orderByCreatedAt(); print_r($query->getConditions()); // Output: ["username" => "ahmed", "age" => 25, "order" => "createdAt"] ?>

__toString() Magic Method

Control how an object is converted to a string:

<?php class User { private $name; private $email; private $role; public function __construct($name, $email, $role) { $this->name = $name; $this->email = $email; $this->role = $role; } // Called when object is used as string public function __toString() { return "{$this->name} ({$this->email}) - {$this->role}"; } } $user = new User("Ahmed Ali", "ahmed@example.com", "Admin"); echo $user; // Outputs: Ahmed Ali (ahmed@example.com) - Admin // Can be used in string concatenation $message = "User: " . $user; echo $message; ?>

__destruct() Magic Method

Called when an object is destroyed or script ends:

<?php class DatabaseConnection { private $connection; public function __construct() { echo "Opening database connection...\n"; $this->connection = "MySQL Connection"; } public function query($sql) { echo "Executing: {$sql}\n"; } // Called automatically when object is destroyed public function __destruct() { echo "Closing database connection...\n"; $this->connection = null; } } function testConnection() { $db = new DatabaseConnection(); $db->query("SELECT * FROM users"); // __destruct() is automatically called when function ends } testConnection(); echo "Function completed\n"; ?>

Real-World Example: Smart Model

Combining traits and magic methods for a powerful model class:

<?php trait Timestampable { protected $createdAt; protected $updatedAt; protected function initTimestamps() { $this->createdAt = date("Y-m-d H:i:s"); $this->updatedAt = date("Y-m-d H:i:s"); } protected function updateTimestamp() { $this->updatedAt = date("Y-m-d H:i:s"); } } trait Validatable { protected $errors = []; protected function validate($field, $value, $rules) { foreach ($rules as $rule) { if ($rule === "required" && empty($value)) { $this->errors[$field][] = "{$field} is required"; } if ($rule === "email" && !filter_var($value, FILTER_VALIDATE_EMAIL)) { $this->errors[$field][] = "{$field} must be a valid email"; } } } public function hasErrors() { return !empty($this->errors); } public function getErrors() { return $this->errors; } } class Model { use Timestampable, Validatable; protected $attributes = []; protected $rules = []; public function __construct($data = []) { $this->initTimestamps(); foreach ($data as $key => $value) { $this->$key = $value; } } // Magic getter public function __get($name) { return $this->attributes[$name] ?? null; } // Magic setter with validation public function __set($name, $value) { if (isset($this->rules[$name])) { $this->validate($name, $value, $this->rules[$name]); } if (!$this->hasErrors()) { $this->attributes[$name] = $value; $this->updateTimestamp(); } } public function __isset($name) { return isset($this->attributes[$name]); } public function __unset($name) { unset($this->attributes[$name]); $this->updateTimestamp(); } public function __toString() { return json_encode($this->attributes); } public function toArray() { return array_merge($this->attributes, [ "created_at" => $this->createdAt, "updated_at" => $this->updatedAt ]); } } class User extends Model { protected $rules = [ "email" => ["required", "email"], "name" => ["required"] ]; } // Usage $user = new User([ "name" => "Ahmed Ali", "email" => "ahmed@example.com", "age" => 25 ]); echo $user->name; // Magic __get() $user->age = 26; // Magic __set() echo $user; // Magic __toString() print_r($user->toArray()); // Validation in action $invalidUser = new User(); $invalidUser->email = "invalid-email"; // Will trigger validation if ($invalidUser->hasErrors()) { print_r($invalidUser->getErrors()); } ?>

__invoke() Magic Method

Make objects callable like functions:

<?php class Multiplier { private $factor; public function __construct($factor) { $this->factor = $factor; } // Called when object is used as a function public function __invoke($number) { return $number * $this->factor; } } $double = new Multiplier(2); $triple = new Multiplier(3); echo $double(5); // Output: 10 echo $triple(5); // Output: 15 // Can be used with array_map $numbers = [1, 2, 3, 4, 5]; $doubled = array_map($double, $numbers); print_r($doubled); // [2, 4, 6, 8, 10] ?>

__debugInfo() Magic Method

Control what information is shown when debugging:

<?php class User { private $name; private $email; private $password; // Sensitive data public function __construct($name, $email, $password) { $this->name = $name; $this->email = $email; $this->password = password_hash($password, PASSWORD_DEFAULT); } // Called by var_dump() and print_r() public function __debugInfo() { return [ "name" => $this->name, "email" => $this->email, "password" => "***HIDDEN***" // Don't show actual password ]; } } $user = new User("Ahmed", "ahmed@example.com", "secret123"); var_dump($user); // Password will show as ***HIDDEN*** ?>

Practical Example: Flexible Configuration

A configuration class using traits and magic methods:

<?php trait ArrayAccessTrait { public function offsetExists($offset): bool { return isset($this->data[$offset]); } public function offsetGet($offset): mixed { return $this->data[$offset] ?? null; } public function offsetSet($offset, $value): void { $this->data[$offset] = $value; } public function offsetUnset($offset): void { unset($this->data[$offset]); } } class Config implements ArrayAccess { use ArrayAccessTrait; private $data = []; public function __construct($config = []) { $this->data = $config; } // Magic getter for dot notation public function __get($name) { return $this->get($name); } public function get($key, $default = null) { // Support dot notation: database.host if (str_contains($key, ".")) { $keys = explode(".", $key); $value = $this->data; foreach ($keys as $k) { if (!isset($value[$k])) { return $default; } $value = $value[$k]; } return $value; } return $this->data[$key] ?? $default; } public function set($key, $value) { $this->data[$key] = $value; } public function __toString() { return json_encode($this->data, JSON_PRETTY_PRINT); } } // Usage $config = new Config([ "app" => [ "name" => "MyApp", "version" => "1.0.0" ], "database" => [ "host" => "localhost", "port" => 3306 ] ]); // Magic getter echo $config->app; // Array // Dot notation echo $config->get("database.host"); // localhost // Array access (from trait) echo $config["app"]["name"]; // MyApp // As string echo $config; // JSON output ?>
Exercise:

Create a flexible Collection class using traits and magic methods:

  • Trait Arrayable with methods: toArray(), toJson()
  • Trait Countable with methods: count(), isEmpty()
  • Class Collection that stores items
  • Magic method __get() to access items by key
  • Magic method __set() to add items
  • Magic method __toString() to display as string
  • Magic method __invoke() to filter items
  • Methods: add(), remove(), first(), last(), map()

Best Practices

Important Guidelines:
  • Use traits for horizontal reuse: Share functionality across unrelated classes
  • Keep traits focused: Each trait should do one thing well
  • Document trait usage: Make it clear what traits a class uses
  • Use magic methods sparingly: They can make code harder to understand
  • Type hint when possible: Magic methods reduce IDE support
  • Always implement __toString(): Makes debugging easier
  • Be careful with __destruct(): Can cause unexpected behavior

Summary

In this lesson, you learned:

  • Traits for code reuse across multiple classes
  • How to use multiple traits and resolve conflicts
  • Magic methods that provide special behaviors
  • __get(), __set(), __isset(), __unset() for dynamic properties
  • __call() for dynamic method calls
  • __toString() for string representation
  • __invoke() for callable objects
  • __destruct() for cleanup operations
  • Practical applications combining traits and magic methods
  • Best practices for using these advanced features

Congratulations! You've completed the Object-Oriented PHP module. You now have the knowledge to build robust, maintainable, and flexible PHP applications using OOP principles!