MVC arxitektura — oddiy router va controller yozish
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 -Sbilan-t publicberilganda, mavjud bo'lmagan har qanday yo'lindex.phpga yo'naltiriladi. Apache uchun.htaccess, Nginx uchuntry_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.phpga 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