فهم السمات
السمات هي آلية لإعادة استخدام الكود في PHP. تسمح لك بمشاركة الدوال عبر أصناف متعددة دون استخدام الوراثة. فكر في السمات على أنها كود "نسخ ولصق" يمكنك تضمينه في أصناف متعددة.
لماذا نستخدم السمات؟
- إعادة استخدام الكود: مشاركة الوظائف الشائعة عبر أصناف غير مرتبطة
- تجنب الوراثة: PHP لا تدعم الوراثة المتعددة، لكن السمات توفر فوائد مماثلة
- التكوين: بناء أصناف من خلال الجمع بين سمات متعددة
- المرونة: مزج ومطابقة الوظائف حسب الحاجة
إنشاء واستخدام السمات
لننشئ سمة بسيطة ونستخدمها في أصناف متعددة:
<?php
// تعريف سمة
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();
}
}
// استخدام السمة في صنف
class User {
use Timestampable; // تضمين السمة
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(); // تحديث الطابع الزمني
}
}
class Post {
use Timestampable; // نفس السمة في صنف مختلف
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();
}
}
// كلا الصنفين لديهما وظيفة الطوابع الزمنية
$user = new User("أحمد", "ahmed@example.com");
echo $user->getCreatedAt();
$post = new Post("سمات PHP", "محتوى عن السمات...");
echo $post->getCreatedAt();
?>
صيغة السمة: استخدم الكلمة المفتاحية use داخل صنف لتضمين سمة. يمكنك استخدام سمات متعددة في صنف واحد.
استخدام سمات متعددة
يمكن للصنف استخدام سمات متعددة:
<?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; // سمات متعددة
private $name;
private $price;
public function __construct($name, $price) {
$this->name = $name;
$this->price = $price;
$this->setCreatedAt();
$this->log("تم إنشاء المنتج");
}
public function setPrice($price) {
if ($price < 0) {
$this->addError("price", "السعر لا يمكن أن يكون سالباً");
return false;
}
$this->price = $price;
$this->touch();
$this->log("تم تحديث السعر إلى {$price}");
return true;
}
}
$product = new Product("لابتوب", 999);
$product->setPrice(-100);
if ($product->hasErrors()) {
print_r($product->getErrors());
}
print_r($product->getLogs());
?>
حل تعارض السمات
عندما تحتوي سمتان على دوال بنفس الاسم، تحتاج إلى حل التعارض:
<?php
trait FormatHelper {
public function format($text) {
return strtoupper($text);
}
}
trait HtmlHelper {
public function format($text) {
return htmlspecialchars($text);
}
}
class TextProcessor {
use FormatHelper, HtmlHelper {
// حل التعارض: استخدم format من HtmlHelper بدلاً من FormatHelper
HtmlHelper::format insteadof FormatHelper;
// أعطِ format من FormatHelper اسماً مستعاراً
FormatHelper::format as formatUpper;
}
}
$processor = new TextProcessor();
echo $processor->format("<b>مرحباً</b>"); // يستخدم HtmlHelper
echo $processor->formatUpper("مرحباً"); // يستخدم FormatHelper
?>
فهم الدوال السحرية
الدوال السحرية هي دوال خاصة يتم استدعاؤها تلقائياً في مواقف محددة. تبدأ جميعها بشرطتين سفليتين (__).
الدوال السحرية الشائعة:
- __construct(): تُستدعى عند إنشاء كائن
- __destruct(): تُستدعى عند تدمير كائن
- __get(): تُستدعى عند الوصول إلى خصائص غير متاحة
- __set(): تُستدعى عند تعيين خصائص غير متاحة
- __isset(): تُستدعى عند التحقق من تعيين خاصية
- __unset(): تُستدعى عند إلغاء تعيين خاصية
- __call(): تُستدعى عند استدعاء دوال غير متاحة
- __toString(): تُستدعى عند استخدام الكائن كسلسلة نصية
دوال __get() و __set() السحرية
هذه الدوال تتعامل مع الوصول للخصائص ديناميكياً:
<?php
class DynamicProperties {
private $data = [];
// تُستدعى عند محاولة قراءة خاصية غير متاحة
public function __get($name) {
echo "الحصول على الخاصية: {$name}\n";
return $this->data[$name] ?? null;
}
// تُستدعى عند محاولة الكتابة إلى خاصية غير متاحة
public function __set($name, $value) {
echo "تعيين الخاصية: {$name} = {$value}\n";
$this->data[$name] = $value;
}
// تُستدعى عند التحقق من تعيين خاصية
public function __isset($name) {
return isset($this->data[$name]);
}
// تُستدعى عند إلغاء تعيين خاصية
public function __unset($name) {
unset($this->data[$name]);
}
}
$obj = new DynamicProperties();
$obj->name = "أحمد"; // تستدعي __set()
echo $obj->name; // تستدعي __get()
var_dump(isset($obj->name)); // تستدعي __isset()
unset($obj->name); // تستدعي __unset()
?>
دالة __call() السحرية
التعامل مع استدعاءات الدوال غير المعرفة:
<?php
class FluentQuery {
private $conditions = [];
// تُستدعى عند استدعاء دالة غير متاحة
public function __call($name, $arguments) {
// التعامل مع شروط where ديناميكياً
if (str_starts_with($name, "where")) {
$field = lcfirst(substr($name, 5)); // whereUsername -> username
$this->conditions[$field] = $arguments[0];
return $this; // للربط المتسلسل
}
// التعامل مع order by ديناميكياً
if (str_starts_with($name, "orderBy")) {
$field = lcfirst(substr($name, 7));
$this->conditions["order"] = $field;
return $this;
}
throw new Exception("الدالة {$name} غير موجودة");
}
public function getConditions() {
return $this->conditions;
}
}
$query = new FluentQuery();
$query->whereUsername("أحمد")
->whereAge(25)
->orderByCreatedAt();
print_r($query->getConditions());
// النتيجة: ["username" => "أحمد", "age" => 25, "order" => "createdAt"]
?>
دالة __toString() السحرية
التحكم في كيفية تحويل الكائن إلى سلسلة نصية:
<?php
class User {
private $name;
private $email;
private $role;
public function __construct($name, $email, $role) {
$this->name = $name;
$this->email = $email;
$this->role = $role;
}
// تُستدعى عند استخدام الكائن كسلسلة نصية
public function __toString() {
return "{$this->name} ({$this->email}) - {$this->role}";
}
}
$user = new User("أحمد علي", "ahmed@example.com", "مسؤول");
echo $user; // النتيجة: أحمد علي (ahmed@example.com) - مسؤول
// يمكن استخدامه في دمج السلاسل النصية
$message = "المستخدم: " . $user;
echo $message;
?>
دالة __destruct() السحرية
تُستدعى عند تدمير كائن أو انتهاء السكريبت:
<?php
class DatabaseConnection {
private $connection;
public function __construct() {
echo "فتح اتصال قاعدة البيانات...\n";
$this->connection = "اتصال MySQL";
}
public function query($sql) {
echo "تنفيذ: {$sql}\n";
}
// تُستدعى تلقائياً عند تدمير الكائن
public function __destruct() {
echo "إغلاق اتصال قاعدة البيانات...\n";
$this->connection = null;
}
}
function testConnection() {
$db = new DatabaseConnection();
$db->query("SELECT * FROM users");
// تُستدعى __destruct() تلقائياً عند انتهاء الدالة
}
testConnection();
echo "انتهت الدالة\n";
?>
مثال واقعي: نموذج ذكي
الجمع بين السمات والدوال السحرية لصنف نموذج قوي:
<?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} مطلوب";
}
if ($rule === "email" && !filter_var($value, FILTER_VALIDATE_EMAIL)) {
$this->errors[$field][] = "{$field} يجب أن يكون بريداً إلكترونياً صالحاً";
}
}
}
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;
}
}
// دالة getter السحرية
public function __get($name) {
return $this->attributes[$name] ?? null;
}
// دالة setter السحرية مع التحقق
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"]
];
}
// الاستخدام
$user = new User([
"name" => "أحمد علي",
"email" => "ahmed@example.com",
"age" => 25
]);
echo $user->name; // السحرية __get()
$user->age = 26; // السحرية __set()
echo $user; // السحرية __toString()
print_r($user->toArray());
// التحقق في العمل
$invalidUser = new User();
$invalidUser->email = "بريد-غير-صالح"; // سيُطلق التحقق
if ($invalidUser->hasErrors()) {
print_r($invalidUser->getErrors());
}
?>
دالة __invoke() السحرية
جعل الكائنات قابلة للاستدعاء مثل الدوال:
<?php
class Multiplier {
private $factor;
public function __construct($factor) {
$this->factor = $factor;
}
// تُستدعى عند استخدام الكائن كدالة
public function __invoke($number) {
return $number * $this->factor;
}
}
$double = new Multiplier(2);
$triple = new Multiplier(3);
echo $double(5); // النتيجة: 10
echo $triple(5); // النتيجة: 15
// يمكن استخدامه مع array_map
$numbers = [1, 2, 3, 4, 5];
$doubled = array_map($double, $numbers);
print_r($doubled); // [2, 4, 6, 8, 10]
?>
دالة __debugInfo() السحرية
التحكم في المعلومات المعروضة عند التصحيح:
<?php
class User {
private $name;
private $email;
private $password; // بيانات حساسة
public function __construct($name, $email, $password) {
$this->name = $name;
$this->email = $email;
$this->password = password_hash($password, PASSWORD_DEFAULT);
}
// تُستدعى من قبل var_dump() و print_r()
public function __debugInfo() {
return [
"name" => $this->name,
"email" => $this->email,
"password" => "***مخفي***" // لا تُظهر كلمة المرور الفعلية
];
}
}
$user = new User("أحمد", "ahmed@example.com", "سري123");
var_dump($user); // ستظهر كلمة المرور كـ ***مخفي***
?>
مثال عملي: تكوين مرن
صنف تكوين باستخدام السمات والدوال السحرية:
<?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;
}
// دالة getter السحرية لتدوين النقطة
public function __get($name) {
return $this->get($name);
}
public function get($key, $default = null) {
// دعم تدوين النقطة: 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);
}
}
// الاستخدام
$config = new Config([
"app" => [
"name" => "تطبيقي",
"version" => "1.0.0"
],
"database" => [
"host" => "localhost",
"port" => 3306
]
]);
// دالة getter السحرية
echo $config->app; // مصفوفة
// تدوين النقطة
echo $config->get("database.host"); // localhost
// الوصول بالمصفوفة (من السمة)
echo $config["app"]["name"]; // تطبيقي
// كسلسلة نصية
echo $config; // مخرجات JSON
?>
تمرين:
أنشئ صنف Collection مرن باستخدام السمات والدوال السحرية:
- سمة
Arrayable مع دوال: toArray(), toJson()
- سمة
Countable مع دوال: count(), isEmpty()
- صنف
Collection يخزن العناصر
- دالة سحرية
__get() للوصول إلى العناصر بالمفتاح
- دالة سحرية
__set() لإضافة عناصر
- دالة سحرية
__toString() للعرض كسلسلة نصية
- دالة سحرية
__invoke() لتصفية العناصر
- دوال:
add(), remove(), first(), last(), map()
أفضل الممارسات
إرشادات مهمة:
- استخدم السمات لإعادة الاستخدام الأفقي: شارك الوظائف عبر أصناف غير مرتبطة
- احتفظ بالسمات مركزة: يجب أن تفعل كل سمة شيئاً واحداً بشكل جيد
- وثق استخدام السمة: اجعل من الواضح أي سمات يستخدمها الصنف
- استخدم الدوال السحرية باعتدال: يمكن أن تجعل الكود أصعب في الفهم
- حدد الأنواع عندما يكون ممكناً: الدوال السحرية تقلل من دعم IDE
- نفذ دائماً __toString(): يجعل التصحيح أسهل
- كن حذراً مع __destruct(): يمكن أن يسبب سلوكاً غير متوقع
الملخص
في هذا الدرس، تعلمت:
- السمات لإعادة استخدام الكود عبر أصناف متعددة
- كيفية استخدام سمات متعددة وحل التعارضات
- الدوال السحرية التي توفر سلوكيات خاصة
__get(), __set(), __isset(), __unset() للخصائص الديناميكية
__call() لاستدعاءات الدوال الديناميكية
__toString() للتمثيل النصي
__invoke() للكائنات القابلة للاستدعاء
__destruct() لعمليات التنظيف
- التطبيقات العملية التي تجمع بين السمات والدوال السحرية
- أفضل الممارسات لاستخدام هذه الميزات المتقدمة
تهانينا! لقد أكملت وحدة PHP الموجهة للكائنات. لديك الآن المعرفة لبناء تطبيقات PHP قوية وقابلة للصيانة ومرنة باستخدام مبادئ OOP!