MVC arxitektura — oddiy router va controller yozish

~30 daq · PHP · Zamonaviy PHP

Muammodan boshlaylik

37-darsdagi todo ilovasini eslang. Har bir faylda bir xil narsa aralash:

index.php:   session_start + require + validatsiya + SQL + HTML + yana SQL + HTML

10 ta sahifa bo'lsa — 10 ta shunday fayl. Muammolar:

  • SQL so'rovlar sochilib ketgan: jadval nomini o'zgartirsangiz — hamma fayllarni qidirasiz
  • Dizayner HTML ni o'zgartirmoqchi — SQL va PHP mantiq orasida adashadi
  • URL lar tahrirlash.php?id=5 — chiroyli emas, fayl tuzilmasini oshkor qiladi
  • Bitta mantiqni test qilib bo'lmaydi — u HTML bilan chambarchas bog'langan

MVC (Model-View-Controller) — kodni vazifasiga qarab uch qismga ajratish usuli. Laravel, Symfony, CodeIgniter, Yii — hammasi shu g'oyaga qurilgan.


Uch qism

Brauzer → /vazifalar/5
              │
              ▼
        ┌──────────┐
        │  Router  │  "bu URL qaysi controllerga?"
        └────┬─────┘
             ▼
      ┌────────────┐     ┌─────────┐
      │ Controller │ ──→ │  Model  │ ←→ Baza
      └─────┬──────┘ ←── └─────────┘
            ▼
       ┌────────┐
       │  View  │  → HTML → Brauzer
       └────────┘
Qism Vazifasi Nimani bilmaydi
Model Ma'lumot va biznes qoidalari: bazadan o'qish, saqlash, tekshirish HTML, URL, $_POST
View Ko'rinish: ma'lumotni HTML ga aylantirish Baza, SQL
Controller Dirijyor: so'rovni qabul qiladi, Model dan ma'lumot oladi, View ga beradi SQL tafsilotlari, HTML tafsilotlari

Hayotiy o'xshatma: restoran. Ofitsiant (controller) buyurtmani qabul qiladi. Oshxona (model) taomni tayyorlaydi — ofitsiant retseptni bilmaydi. Likopchaga chiroyli qo'yish (view) — oshxona mijoz qaysi stolda o'tirganini bilmaydi.


Papka tuzilmasi

mvc-todo/
├── composer.json
├── config.php
├── public/
│   └── index.php              ← YAGONA kirish nuqtasi
├── src/
│   ├── Router.php
│   ├── Database.php
│   ├── Controller/
│   │   └── VazifaController.php
│   └── Model/
│       └── VazifaRepository.php
└── views/
    ├── layout.php
    └── vazifalar/
        ├── royxat.php
        └── tahrirlash.php

composer.json:

{
    "autoload": { "psr-4": { "App\\": "src/" } }
}
composer dump-autoload
php -S localhost:8000 -t public

1. Front Controller — public/index.php

Endi barcha so'rovlar bitta faylga keladi:

<?php
declare(strict_types=1);

require __DIR__ . '/../vendor/autoload.php';
session_start();

use App\Router;
use App\Controller\VazifaController;

$router = new Router();

$router->get('/',                    [VazifaController::class, 'royxat']);
$router->post('/vazifalar',          [VazifaController::class, 'saqlash']);
$router->get('/vazifalar/{id}',      [VazifaController::class, 'tahrirlash']);
$router->post('/vazifalar/{id}',     [VazifaController::class, 'yangilash']);
$router->post('/vazifalar/{id}/ochir', [VazifaController::class, 'ochirish']);

$router->bajar($_SERVER['REQUEST_METHOD'], parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH));

Barcha URL lar bir joyda — sayt xaritasi ko'z oldingizda.

[!INFO] php -S bilan -t public berilganda, mavjud bo'lmagan har qanday yo'l index.php ga yo'naltiriladi. Apache uchun .htaccess, Nginx uchun try_files $uri /index.php?$query_string; sozlanadi.


2. Router — src/Router.php

<?php
declare(strict_types=1);

namespace App;

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

    public function get(string $yol, array $amal): void
    {
        $this->qosh('GET', $yol, $amal);
    }

    public function post(string $yol, array $amal): void
    {
        $this->qosh('POST', $yol, $amal);
    }

    private function qosh(string $metod, string $yol, array $amal): void
    {
        // "/vazifalar/{id}" → "#^/vazifalar/(?P<id>[^/]+)$#"
        $regex = preg_replace('#\{(\w+)\}#', '(?P<$1>[^/]+)', $yol);
        $this->yollar[] = [$metod, "#^{$regex}$#", $amal];
    }

    public function bajar(string $metod, string $uri): void
    {
        foreach ($this->yollar as [$m, $regex, [$klass, $funksiya]]) {
            if ($m === $metod && preg_match($regex, $uri, $moslik)) {
                $parametrlar = array_filter($moslik, 'is_string', ARRAY_FILTER_USE_KEY);
                (new $klass())->$funksiya(...$parametrlar);
                return;
            }
        }

        http_response_code(404);
        echo 'Sahifa topilmadi';
    }
}

Router URL ni shablonlar bilan solishtiradi. {id} qismi ajratib olinib, controller metodiga nomlangan argument sifatida beriladi (...$parametrlar).


3. Model — src/Model/VazifaRepository.php

Barcha SQL — faqat shu yerda:

<?php
declare(strict_types=1);

namespace App\Model;

use App\Database;

final class VazifaRepository
{
    private \PDO $pdo;

    public function __construct()
    {
        $this->pdo = Database::ulanish();
    }

    public function hammasi(): array
    {
        return $this->pdo
            ->query('SELECT * FROM vazifalar ORDER BY bajarildi, yaratilgan DESC')
            ->fetchAll();
    }

    public function top(int $id): ?array
    {
        $stmt = $this->pdo->prepare('SELECT * FROM vazifalar WHERE id = ?');
        $stmt->execute([$id]);
        return $stmt->fetch() ?: null;
    }

    public function qosh(string $matn): int
    {
        $this->pdo->prepare('INSERT INTO vazifalar (matn) VALUES (?)')->execute([$matn]);
        return (int) $this->pdo->lastInsertId();
    }

    public function yangila(int $id, string $matn): void
    {
        $this->pdo->prepare('UPDATE vazifalar SET matn = ? WHERE id = ?')->execute([$matn, $id]);
    }

    public function ochir(int $id): void
    {
        $this->pdo->prepare('DELETE FROM vazifalar WHERE id = ?')->execute([$id]);
    }

    public static function tekshir(string $matn): array
    {
        $xatolar = [];
        if ($matn === '' || mb_strlen($matn) > 255) {
            $xatolar[] = "Vazifa matni 1-255 belgi bo'lsin";
        }
        return $xatolar;
    }
}

src/Database.php:

<?php
declare(strict_types=1);

namespace App;

final class Database
{
    private static ?\PDO $pdo = null;

    public static function ulanish(): \PDO
    {
        if (self::$pdo === null) {
            $c = require __DIR__ . '/../config.php';
            self::$pdo = new \PDO($c['dsn'], $c['user'], $c['parol'], [
                \PDO::ATTR_ERRMODE            => \PDO::ERRMODE_EXCEPTION,
                \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
                \PDO::ATTR_EMULATE_PREPARES   => false,
            ]);
        }
        return self::$pdo;
    }
}

4. Controller — src/Controller/VazifaController.php

<?php
declare(strict_types=1);

namespace App\Controller;

use App\Model\VazifaRepository;

final class VazifaController
{
    private VazifaRepository $vazifalar;

    public function __construct()
    {
        $this->vazifalar = new VazifaRepository();
    }

    public function royxat(): void
    {
        $this->korsat('vazifalar/royxat', [
            'vazifalar' => $this->vazifalar->hammasi(),
        ]);
    }

    public function saqlash(): void
    {
        $matn = trim($_POST['matn'] ?? '');
        $xatolar = VazifaRepository::tekshir($matn);

        if ($xatolar) {
            $this->korsat('vazifalar/royxat', [
                'vazifalar' => $this->vazifalar->hammasi(),
                'xatolar'   => $xatolar,
                'matn'      => $matn,
            ]);
            return;
        }

        $this->vazifalar->qosh($matn);
        $this->yonaltir('/');
    }

    public function tahrirlash(string $id): void
    {
        $vazifa = $this->vazifalar->top((int) $id) ?? $this->topilmadi();
        $this->korsat('vazifalar/tahrirlash', ['vazifa' => $vazifa]);
    }

    public function yangilash(string $id): void
    {
        $vazifa = $this->vazifalar->top((int) $id) ?? $this->topilmadi();
        $matn = trim($_POST['matn'] ?? '');

        if ($xatolar = VazifaRepository::tekshir($matn)) {
            $this->korsat('vazifalar/tahrirlash', ['vazifa' => [...$vazifa, 'matn' => $matn], 'xatolar' => $xatolar]);
            return;
        }

        $this->vazifalar->yangila((int) $id, $matn);
        $this->yonaltir('/');
    }

    public function ochirish(string $id): void
    {
        $this->vazifalar->ochir((int) $id);
        $this->yonaltir('/');
    }

    // ===== yordamchilar =====

    private function korsat(string $view, array $data = []): void
    {
        extract($data);   // ['vazifalar' => ...] → $vazifalar
        ob_start();
        require __DIR__ . "/../../views/$view.php";
        $kontent = ob_get_clean();
        require __DIR__ . '/../../views/layout.php';
    }

    private function yonaltir(string $url): never
    {
        header("Location: $url", true, 303);
        exit;
    }

    private function topilmadi(): never
    {
        http_response_code(404);
        exit('Vazifa topilmadi');
    }
}

Controller qisqa va "toza": SQL yo'q, HTML yo'q. Faqat "nima olish, kimga berish".

ob_start() / ob_get_clean() — chiqishni ekranga emas, o'zgaruvchiga yig'ish. View natijasi $kontent ga tushadi, keyin layout ichiga joylanadi.


5. View

views/layout.php:

<!DOCTYPE html>
<html lang="uz">
<head>
    <meta charset="UTF-8">
    <title>Vazifalar</title>
</head>
<body style="font-family:system-ui;max-width:640px;margin:40px auto;padding:0 16px">
    <h1><a href="/">Vazifalar</a></h1>
    <?= $kontent ?>
</body>
</html>

views/vazifalar/royxat.php:

<?php foreach ($xatolar ?? [] as $x): ?>
    <p style="color:red"><?= htmlspecialchars($x) ?></p>
<?php endforeach; ?>

<form method="post" action="/vazifalar">
    <input name="matn" value="<?= htmlspecialchars($matn ?? '') ?>" placeholder="Yangi vazifa">
    <button>Qo'shish</button>
</form>

<ul>
<?php foreach ($vazifalar as $v): ?>
    <li>
        <a href="/vazifalar/<?= (int) $v['id'] ?>"><?= htmlspecialchars($v['matn']) ?></a>
        <form method="post" action="/vazifalar/<?= (int) $v['id'] ?>/ochir" style="display:inline">
            <button>✕</button>
        </form>
    </li>
<?php endforeach; ?>
</ul>

views/vazifalar/tahrirlash.php:

<?php foreach ($xatolar ?? [] as $x): ?>
    <p style="color:red"><?= htmlspecialchars($x) ?></p>
<?php endforeach; ?>

<form method="post" action="/vazifalar/<?= (int) $vazifa['id'] ?>">
    <input name="matn" value="<?= htmlspecialchars($vazifa['matn']) ?>">
    <button>Saqlash</button>
</form>

View da faqat chiqarish va oddiy sikl. Dizayner bemalol ishlashi mumkin.


Nima o'zgardi?

Oldin MVC bilan
tahrirlash.php?id=5 /vazifalar/5
SQL har bir faylda SQL faqat VazifaRepository da
HTML va mantiq aralash View faqat ko'rsatadi
Validatsiya ikki joyda takrorlangan VazifaRepository::tekshir() — bitta joy
URL lar fayllar bo'ylab sochilgan index.php da hammasi ko'rinib turibdi

Nima uchun shunday?

Nima uchun framework ishlatmasdan o'z routerimizni yozdik?

Chunki Laravel ni ochganingizda Route::get('/vazifalar/{id}', [VazifaController::class, 'tahrirlash']) ni ko'rasiz — va u sehr bo'lib tuyulmaydi. Siz ichida nima bo'layotganini bilasiz: regex, front controller, extract va output buffering. Framework — bu shu g'oyalarning xavfsiz, sinovdan o'tgan, kengaytirilgan versiyasi. Haqiqiy loyihada o'z routeringizni emas, frameworkni ishlating (44-dars). Lekin uni bir marta o'zingiz yozish — eng yaxshi tushunish usuli.


Xulosa

Eslab qoling:

  • Model — ma'lumot va qoidalar; View — HTML; Controller — ular orasidagi bog'lovchi
  • Front controller: barcha so'rovlar public/index.php ga keladi
  • Router URL ni controller metodiga moslaydi; {id} — parametr
  • SQL faqat Model (Repository) da, HTML faqat View da
  • ob_start() + require + ob_get_clean() — view ni layout ga joylash
  • Bu Laravel va Symfony ning asosiy g'oyasi

Keyingi dars

42-dars: REST API — JSON qaytaruvchi oddiy API