Kodokon kodokon.com

بنية MVC مصنوعة يدويًا

اكتب إطار عمل MVC مصغّرًا وكاملًا - مُوجِّه قائم على التعابير النمطية، ومتحكّمات مُحقَّنة، وعروض مُخزَّنة مؤقتًا - لتفهم ما تفعله أطر العمل نيابةً عنك.

11 دقيقة · 3 أسئلة

افتح هذا الدرس في Kodokon

فهم إطار عمل يعني معرفة كيفية كتابة إطار صغير. تقوم كل بنية MVC حديثة على متحكّم أمامي (front controller): يستقبل ملف index.php واحد كل الطلبات (يعيد الخادم كتابة كل عنوان URL نحوه)، ثم يطابق مُوجِّه (router) طريقة HTTP والمسار مع معالِج. ويحوّل مُوجِّهنا أنماطًا مثل /posts/{id} إلى تعابير نمطية ذات مجموعات التقاط مُسمّاة: فتملأ عندئذٍ preg_match القيمة $found['id'] مباشرةً، دون أيّ تقسيم يدوي لعنوان URL.

PHP
<?php
declare(strict_types=1);

final class Router
{
    private array $routes = [];

    public function add(
        string $method,
        string $pattern,
        callable $handler
    ): void {
        $regex = preg_replace(
            '#\{(\w+)\}#',
            '(?P<$1>[^/]+)',
            $pattern
        );
        $this->routes[] = [
            $method, "#^$regex$#", $handler
        ];
    }

    public function dispatch(
        string $method,
        string $uri
    ): mixed {
        foreach ($this->routes as $route) {
            [$m, $re, $handler] = $route;
            if ($m !== $method) {
                continue;
            }
            if (preg_match($re, $uri, $found) === 1) {
                return $handler($found);
            }
        }
        http_response_code(404);
        return 'Page not found';
    }
}
يصبح النمط /posts/{id} على الشكل #^/posts/(?P<id>[^/]+)$#.

ينظّم المتحكّم العمل: فهو يقرأ الوسائط، ويستعلم النموذج - الطبقة الوحيدة المسموح لها بالتحدّث إلى قاعدة البيانات - ثم يختار عرضًا. تدخل التبعيات عبر الباني (constructor): هذا هو حقن التبعيات دون حاوية، ويضمن ترقية الخصائص إلى readonly أنها لن تتغير أبدًا بعد الإنشاء. ويُرجِع النموذج القيمة null صراحةً عندما لا يكون المورد موجودًا، بدلًا من false: فيمكن للمتحكّم عندئذٍ أن يقرّر بشأن رمز 404.

PHP
<?php
declare(strict_types=1);

final class PostModel
{
    public function __construct(
        private readonly PDO $pdo
    ) {}

    public function find(int $id): ?array
    {
        $stmt = $this->pdo->prepare(
            'SELECT * FROM posts WHERE id = :id'
        );
        $stmt->execute(['id' => $id]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        return $row === false ? null : $row;
    }
}

final class PostController
{
    public function __construct(
        private readonly PostModel $posts
    ) {}

    public function show(array $params): string
    {
        $id = (int) $params['id'];
        $post = $this->posts->find($id);
        if ($post === null) {
            http_response_code(404);
            return render('not-found');
        }
        return render('post', ['post' => $post]);
    }
}
دالة render مُعرَّفة في الكتلة التالية.

العرض ملف PHP بسيط لا يعرض سوى متغيرات جُهِّزت له مسبقًا. وتجعل ذلك ممكنًا آليتان داخليتان: تحوّل extract مصفوفة البيانات إلى متغيرات محلية، ويلتقط التخزين المؤقت للإخراج (ob_start ثم ob_get_clean) الإخراجَ لإرجاعه كسلسلة بدل إرساله فورًا - وهو أمر لا غنى عنه لتركيب قوالب متداخلة أو لتحديد الترويسات بعد العرض.

PHP
<?php
declare(strict_types=1);

function render(
    string $view,
    array $data = []
): string {
    extract($data, EXTR_SKIP);
    ob_start();
    require __DIR__ . "/views/$view.php";
    return (string) ob_get_clean();
}

$pdo = new PDO('sqlite:blog.db');
$controller = new PostController(
    new PostModel($pdo)
);

$router = new Router();
$router->add(
    'GET',
    '/posts/{id}',
    fn (array $p): string => $controller->show($p)
);

$path = parse_url(
    $_SERVER['REQUEST_URI'],
    PHP_URL_PATH
);

echo $router->dispatch(
    $_SERVER['REQUEST_METHOD'],
    (string) $path
);
المتحكّم الأمامي الكامل: العرض والربط والتوزيع.

اختبار المعرفة

تأكّد من أنك تذكّرت النقاط الأساسية في هذا الدرس.

  1. في هذا المُوجِّه، ماذا يُنتِج تحويل النمط /posts/{id}؟
    • تعبير نمطي بمجموعة التقاط مُسمّاة، يمكن الوصول إليها بعدئذٍ عبر $found['id']
    • مصفوفة ترابطية لأجزاء عنوان URL
    • مقارنة نصية بسيطة، جزءًا جزءًا
  2. لماذا تستخدم دالة render التعليمة ob_start؟
    • لضغط إخراج HTML قبل إرساله
    • لالتقاط إخراج العرض وإرجاعه كسلسلة بدل إرساله فورًا إلى المتصفح
    • لمنع العرض من تنفيذ كود PHP
    • لتخزين ناتج عرض القالب على القرص
  3. ما دور المتحكّم الأمامي؟
    • خدمة الملفات الساكنة بشكل أسرع
    • التحقق من أذونات المستخدم قبل كل صفحة
    • أن يكون نقطة الدخول الوحيدة للتطبيق: تُعاد كتابة كل طلب نحوه، ثم يُفوَّض إلى المُوجِّه