السمات والأعضاء الثابتة

تتمتع لغة PHP بميزتين لا تتوفران في معظم اللغات الشائعة الأخرى (أو لا يتم تصميمهما بنفس الطريقة): السمات (Traits) والربط الثابت المتأخر (late static binding). يقدم لك هذا الدرس هاتين الأداتين الفريدتين في PHP لكتابة كود أكثر أناقة.

1. السمات — سحر إعادة استخدام الكود في لغة PHP

تتميز لغة PHP بالوراثة الأحادية — أي أن الفئة لا يمكن أن يكون لها سوى فئة أصلية واحدة. لكن في بعض الأحيان، تحتاج فئات غير مرتبطة ببعضها إلى مشاركة نفس الجزء من الكود (مثل «تسجيل الطوابع الزمنية» أو «إنشاء معرّفات فريدة»). وقد تم ابتكار «السمات» (Traits) لهذا الغرض.

PHP
<?php
// Define a Trait
trait HasTimestamp {
    private string $createdAt;
    private string $updatedAt;
    
    public function touch(): void {
        $now = date("Y-m-d H:i:s");
        if (!isset($this->createdAt)) {
            $this->createdAt = $now;
        }
        $this->updatedAt = $now;
    }
    
    public function getCreatedAt(): string {
        return $this->createdAt ?? 'Unknown';
    }
    
    public function getUpdatedAt(): string {
        return $this->updatedAt ?? 'Unknown';
    }
}

trait HasUuid {
    public function generateUuid(): string {
        return uniqid() . bin2hex(random_bytes(8));
    }
}

// Use Traits in completely different classes
class User {
    use HasTimestamp, HasUuid;  // Use multiple Traits
    
    public function __construct(
        public string $name
    ) {
        $this->touch();
    }
}

class Article {
    use HasTimestamp;
    
    public function __construct(
        public string $title,
        public string $content
    ) {
        $this->touch();
    }
}

$user = new User("John");
echo $user->name . " created at " . $user->getCreatedAt() . "<br>";
echo "UUID: " . $user->generateUuid() . "<br>";

$article = new Article("PHP Traits Tutorial", "Main content...");
echo $article->title . " last updated at " . $article->getUpdatedAt() . "<br>";
?>
💡 نصيحة: السمات مقابل الوراثة: استخدم السمات عندما تحتاج عدة فئات غير مرتبطة ببعضها (مثل User ≠ Article) إلى مشاركة نفس المنطق. أما إذا كانت الفئات الفرعية ترتبط بعلاقة «هو نوع من» (مثل Dog هو نوع من Animal)، فاستخدم الوراثة.


2. حل النزاعات المتعلقة بالسمات

عندما تحتوي سمات متعددة على طرق تحمل الاسم نفسه:

PHP
<?php
trait Logger {
    public function log(string $msg): void {
        echo "[LOG] {$msg}<br>";
    }
}

trait FileLogger {
    public function log(string $msg): void {
        echo "[FILE] {$msg}<br>";
    }
}

class App {
    use Logger, FileLogger {
        // Replace Logger's log with FileLogger's log
        FileLogger::log insteadof Logger;
        
        // Give the replaced method an alias so it's still callable
        Logger::log as logSimple;
    }
}

$app = new App();
$app->log("App started");      // [FILE] App started
$app->logSimple("App started"); // [LOG] App started
?>

▶ مثال: مجموعة سمات عملية

PHP
<?php
trait Arrayable {
    public function toArray(): array {
        return get_object_vars($this);
    }
    
    public function toJson(): string {
        return json_encode($this->toArray(), JSON_UNESCAPED_UNICODE);
    }
}

trait Validatable {
    public function validate(array $rules): array {
        $errors = [];
        foreach ($rules as $field => $rule) {
            $value = $this->$field ?? null;
            if (str_contains($rule, 'required') && empty($value)) {
                $errors[$field] = "{$field} is required";
            }
        }
        return $errors;
    }
}

class Product {
    use Arrayable, Validatable;
    
    public function __construct(
        public string $name,
        public float $price,
        public string $description = '',
    ) {}
}

$p = new Product("Mechanical Keyboard", 299);
echo $p->toJson() . "<br>";
print_r($p->validate(['name' => 'required', 'price' => 'required']));
?>
▶ جرّب الكود

3. الخصائص والطرق الثابتة

تنتمي العناصر الثابتة إلى الفئة نفسها، وليس إلى المثيلات. وتشترك جميع الكائنات في نفس الخاصية الثابتة:

PHP
<?php
class Counter {
    private static int $count = 0;
    
    public function __construct() {
        self::$count++;  // self:: accesses static members
    }
    
    public static function getCount(): int {
        return self::$count;
    }
}

echo Counter::getCount();  // 0 (call with ClassName::method)
$a = new Counter();
$b = new Counter();
$c = new Counter();
echo Counter::getCount();  // 3 (all instances share a single $count)
?>
خاصية المثيل $this-> خاصية ثابتة self::$
ينتمي إلى كل كائن الفئة نفسها
الذاكرة نسخة واحدة لكل كائن نسخة عالمية واحدة
الوصول $obj->prop Class::$prop

4. self:: مقابل static:: (الربط الثابت المتأخر)

هذا أحد أكثر مفاهيم لغة PHP دقةً — وأكثرها إثارةً للالتباس —:

PHP
<?php
class ParentClass {
    public static function who(): string {
        return 'Parent';
    }
    
    public static function test(): string {
        return self::who();   // self:: always points to the defining class (compile-time binding)
    }
    
    public static function testLate(): string {
        return static::who(); // static:: points to the actual calling class (runtime binding)
    }
}

class ChildClass extends ParentClass {
    public static function who(): string {
        return 'Child';
    }
}

echo ChildClass::test();     // "Parent"  ← self:: bound to ParentClass
echo ChildClass::testLate(); // "Child"   ← static:: bound to ChildClass
?>
self:: static::
وقت الربط وقت الترجمة وقت التشغيل
تشير إلى الفئة التي تم تعريف الطريقة فيها الفئة الفعلية التي تستدعي الطريقة
الأنسب لـ الطرق المساعدة، الثوابت الطرق الثابتة التي يجب على الفئات الفرعية تجاوزها

▶ مثال: الربط الثابت المتأخر في الممارسة العملية — نموذج ORM بسيط

PHP
<?php
abstract class Model {
    // static:: lets subclasses return their own table name
    public static function table(): string {
        // Default: class name → snake_case → plural
        return strtolower(static::class) . 's';
    }
    
    public static function find(int $id): string {
        return "SELECT * FROM " . static::table() . " WHERE id = {$id}";
    }
}

class User extends Model {}
class Product extends Model {
    public static function table(): string {
        return 'products';  // Custom table name
    }
}

echo User::find(1);     // SELECT * FROM users WHERE id = 1
echo Product::find(5);  // SELECT * FROM products WHERE id = 5
// static::table() returns each subclass's own table name
?>
▶ جرّب الكود

5. الثوابت في الفئات (const)

ثوابت الفئة لا تتغير من كائن لآخر — وهي مثالية لقيم التكوين والاتفاقيات:

PHP
<?php
class HttpStatus {
    public const OK = 200;
    public const NOT_FOUND = 404;
    public const INTERNAL_ERROR = 500;
    
    public static function getMessage(int $code): string {
        return match($code) {
            self::OK            => 'OK',
            self::NOT_FOUND     => 'Not Found',
            self::INTERNAL_ERROR => 'Internal Server Error',
            default             => 'Unknown',
        };
    }
}

echo HttpStatus::OK;            // 200
echo HttpStatus::NOT_FOUND;     // 404
echo HttpStatus::getMessage(404); // Not Found

// PHP 8.1+ final const
class Config {
    final public const APP_NAME = 'MyBlog';
    // Subclasses can't override final constants
}
?>
💡 نصيحة: الثوابت الخاصة بالفئات أفضل من define() — فهي تتمتع بمساحات أسماء، وتدعم الإكمال التلقائي، ولا تلوث النطاق العام. إذا كانت مجموعة من القيم تنتمي إلى نفس المفهوم (رموز حالة HTTP، أدوار المستخدمين)، فقم بتنظيمها باستخدام الثوابت الخاصة بالفئات.

❓ أسئلة شائعة

س كيف أختار بين السمة (Trait) والفئة المجردة (abstract class)؟
ج الفئات غير المرتبطة ببعضها والتي تشترك في كود → السمة (Trait). علاقة «هو نوع من» (is-a) + خصائص مشتركة → الفئة المجردة (abstract class). يمكن للفئة أن use عدة سمات، لكنها لا يمكنها أن تمتد إلا لفئة مجردة واحدة.
س متى يكون التمييز بين self:: وstatic:: مهمًا فعليًّا؟
ج عند كتابة طرق ثابتة سيتم توريثها. على سبيل المثال، تحتاج الفئة الأساسية Model في إطار العمل إلى معرفة «أي فئة فرعية محددة تستدعي هذه الطريقة الثابتة». أما بالنسبة لفئات الأدوات المساعدة (الطرق الثابتة التي لا يتم توريثها أبدًا)، فإن self:: كافية.
س هل يمكن للـ«ترايت» (traits) تعريف الخصائص؟ وماذا عن الثوابت؟
ج يمكن للـ«ترايت» تعريف الخصائص والطرق، ولكن لا يمكنها تعريف الثوابت (لا تدعم لغة PHP ذلك حتى الآن). ولا يمكن لعدة «ترايت» تعريف خصائص تحمل الاسم نفسه — فهذا يؤدي إلى خطأ فادح.

📖 ملخص

📝 تمارين

  1. اكتب سمة Loggable مع طريقة log($message) (تقوم تلقائيًا بإضافة طابع زمني كبادئة). أنشئ فئتين غير مرتبطتين، User وProduct، ثم قم بتطبيق use عليهما.
  2. اكتب فئة أصلية Database، واستخدم static:: لتنفيذ طريقة table()، بحيث تُرجع الفئات الفرعية User/Order users/orders على التوالي. اكتب طريقة ثابتة all() تُرجع SELECT * FROM [table].
  3. أنشئ فئة AppConfig، واستخدم الثوابت الخاصة بالفئة لتعريف APP_NAME وMAX_UPLOAD_SIZE وDEFAULT_LANGUAGE. اكتب دالة ثابتة display() تعرض جميع قيم التكوين.
Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%