20 β Design patterns (GoF) idiomatik PHP 8.4¶
β¬ οΈ Oldingi: 19 β SOLID prinsiplari Β· π README Β· Keyingi: 21 β Taktik dizayn: Repository, Service, DTO β‘οΈ
Bu bobda: dizayn andozasi (design pattern) nima ekanini aniqlaymiz β bu kod emas, balki takrorlanadigan muammoga sinalgan yondashuv va jamoa uchun umumiy lug'at ("bu yerda Decorator ishlatdim" deganda hamma tushunadi). Boshlovchi kitob (5.3 Foydali dizayn andozalari) faqat uchta andozani tanishtirgan edi; bu yerda GoF ("Gang of Four") katalogini idiomatik PHP 8.4 da chuqur ochamiz. Uch oila bo'yicha boramiz: Creational (Factory Method, Abstract Factory, Builder, Singleton va uning tuzoqlari), Structural (Adapter, Decorator, Proxy, Facade, Composite), Behavioral (Strategy, Observer, Command, State, Template Method, Chain of Responsibility). Eng muhimi β zamonaviy PHP da ko'p andoza sinf ierarxiyasisiz ifodalanadi: closure-as-Strategy,
__invokehandler, enum-as-State (./05/./06), first-class callablefoo(...). Oxirida pattern abuse tanqidi: har joyga andoza tiqish β anti-pattern; soddalik birinchi. Har bir andoza haqiqiyphp8.4 bilan ishga tushirib tasdiqlandi, generic factory esa PHPStan max daraja bilan tekshirildi.
Andoza nima β va nima EMAS¶
Avval ikkita keng tarqalgan tushunmovchilikni yo'qotaylik.
Andoza β bu hozir copy-paste qiladigan kod EMAS. Bu β muayyan toifadagi muammoga yondashuvning nomi va shakli. Masalan "Strategy" deganda biz konkret sinflarni emas, "almashtiriladigan algoritmni interfeys ortiga yashir" g'oyasini nazarda tutamiz. PHP da uni interfeys bilan ham, oddiy closure bilan ham amalga oshirish mumkin β ikkalasi ham Strategy.
Andoza β bu kutubxona yoki framework EMAS. Guzzle yoki Symfony β bu tayyor kod. Andoza esa β siz o'z kodingizni qanday tuzishingiz haqidagi qaror. Andoza Composer'dan o'rnatilmaydi.
Unday bo'lsa nega kerak? Ikki sabab:
- Umumiy lug'at. Code review'da "bu yerda Adapter qo'shdim, tashqi SDK interfeysini biznikiga moslash uchun" deyish β bir abzas tushuntirishdan tezroq. Andoza nomi β jamoaning siqilgan tili.
- Sinalgan yechim. GoF kitobidagi 23 andoza o'nlab yillik tajribani umumlashtiradi. Yangi muammoga duch kelganda "buni qaysidir andoza hal qilganmi?" deb so'rash β g'ildirakni qaytadan kashf qilmaslikning yo'li.
Tarix. "GoF" β Design Patterns: Elements of Reusable Object-Oriented Software (1994) kitobining to'rt muallifiga ("Gang of Four") ishora. Misollar C++/Smalltalk'da edi; biz ularni PHP 8.4 ning enum, closure, first-class callable kabi imkoniyatlari bilan qayta o'qiymiz β ko'pi ancha qisqa chiqadi.
Andozalar maqsadiga ko'ra uch oilaga bo'linadi. Bu bobning xaritasi:
Boshlovchi bilan ko'prik. Agar
interface,abstract class,enumyoki closure (fn) sizga notanish bo'lsa, avval boshlovchi kitobning OOP boblariga va 5.3 dizayn andozalari ga qayting. Bu yerda biz ularni allaqachon bilasiz deb hisoblaymiz va idiomatik chuqurlikka boramiz.
CREATIONAL: obyektni qanday yaratamiz¶
Creational andozalar new ni bevosita chaqirishni β qachon obyekt turi o'zgaruvchan yoki qurish murakkab bo'lsa β abstraksiya ortiga olib chiqadi.
Factory Method β yaratishni sinfdan tashqariga¶
Muammo: chaqiruvchi kod qaysi konkret sinfni yaratishni bilmasligi kerak; faqat interfeys bilan ishlasin. Factory Method yaratish mantig'ini bitta joyga yig'adi.
Idiomatik PHP 8.4 da fabrikaning eng toza shakli β enum metodi. Eksport formati enum, va enum o'zi tegishli implementatsiyani qaytaradi:
<?php
declare(strict_types=1);
interface Eksport
{
public function chiqar(array $satrlar): string;
}
final class CsvEksport implements Eksport
{
public function chiqar(array $satrlar): string
{
return implode("\n", array_map(
static fn(array $r): string => implode(',', $r),
$satrlar,
));
}
}
final class JsonEksport implements Eksport
{
public function chiqar(array $satrlar): string
{
return json_encode($satrlar, JSON_THROW_ON_ERROR);
}
}
enum Format: string
{
case Csv = 'csv';
case Json = 'json';
// Factory Method: enum'ning o'zi fabrika
public function eksport(): Eksport
{
return match ($this) {
self::Csv => new CsvEksport(),
self::Json => new JsonEksport(),
};
}
}
$data = [['Oqil', 'Toshkent'], ['Aziza', 'Samarqand']];
echo Format::Csv->eksport()->chiqar($data), "\n";
echo Format::Json->eksport()->chiqar($data), "\n";
Chiqish:
Nega bu kuchli: chaqiruvchi Format::from($_GET['format'])->eksport() deydi β qaysi sinf yaratilishini bilmaydi. match ning tugallik (exhaustiveness) tekshiruvi sizni qutqaradi: yangi case qo'shsangiz-u match ga arm qo'shmasangiz, PHP UnhandledMatchError beradi. Bu β enum-asosli fabrikaning klassik switch ustidan ustunligi.
Abstract Factory β bog'liq obyektlar oilasi¶
Factory Method bitta obyekt yaratadi. Abstract Factory β bir-biriga mos keluvchi obyektlar oilasini yaratadi. Klassik misol: UI to'plami (tugma, maydon) β hammasi bir uslubda bo'lishi kerak (Bootstrap yoki Material, lekin aralashmasin).
<?php
declare(strict_types=1);
interface Tugma { public function render(): string; }
interface Maydon { public function render(): string; }
interface UiFabrika
{
public function tugma(string $matn): Tugma;
public function maydon(string $nom): Maydon;
}
final class BootstrapTugma implements Tugma
{
public function __construct(private string $matn) {}
public function render(): string
{
return "<button class=\"btn\">{$this->matn}</button>";
}
}
final class BootstrapMaydon implements Maydon
{
public function __construct(private string $nom) {}
public function render(): string
{
return "<input class=\"form-control\" name=\"{$this->nom}\">";
}
}
final class BootstrapFabrika implements UiFabrika
{
public function tugma(string $matn): Tugma { return new BootstrapTugma($matn); }
public function maydon(string $nom): Maydon { return new BootstrapMaydon($nom); }
}
function formaChiz(UiFabrika $ui): string
{
return $ui->maydon('email')->render() . "\n" . $ui->tugma('Yuborish')->render();
}
echo formaChiz(new BootstrapFabrika()), "\n";
formaChiz faqat UiFabrika ni biladi β Bootstrap'ni Material'ga almashtirsangiz, bitta new MaterialFabrika() yetadi, qolgan kod o'zgarmaydi. Bu β bir butun oilani birvarakayiga almashtirish kuchidir.
Haddan oshmang. Abstract Factory β eng "og'ir" creational andoza. Agar sizda faqat bitta oila bo'lsa (faqat Bootstrap), bu β ortiqcha. Uni real ehtiyoj (ikkinchi tema, ikkinchi DB drayveri) paydo bo'lganda qo'shing.
Builder β murakkab obyektni qadam-baqadam qurish¶
Konstruktorda 8 ta parametr bo'lsa va yarmi ixtiyoriy bo'lsa β chaqiruv o'qib bo'lmaydi (new X(null, null, true, null, 'POST', ...)). Builder qurishni o'qiladigan qadamlarga ajratadi. PHP da fluent (zanjir) interfeys bilan eng tabiiy chiqadi:
<?php
declare(strict_types=1);
final class SorovBuilder
{
private string $metod = 'GET';
private array $sarlavhalar = [];
private ?string $tana = null;
public function metod(string $m): static { $this->metod = $m; return $this; }
public function sarlavha(string $nom, string $qiymat): static
{
$this->sarlavhalar[$nom] = $qiymat;
return $this;
}
public function tana(string $t): static { $this->tana = $t; return $this; }
public function qur(string $url): string
{
$h = '';
foreach ($this->sarlavhalar as $n => $v) {
$h .= "{$n}: {$v}\n";
}
return "{$this->metod} {$url}\n{$h}\n" . ($this->tana ?? '');
}
}
$sorov = (new SorovBuilder())
->metod('POST')
->sarlavha('Content-Type', 'application/json')
->sarlavha('Accept', 'application/json')
->tana('{"ism":"Oqil"}')
->qur('https://api.test/users');
echo $sorov, "\n";
Diqqat: har bir setter static qaytaradi (return $this) β shu sabab zanjir uziladi. Qaytish turi static (self emas) β bu meros bo'lganda subklass tipini saqlaydi (kovariantlik, ./06 ga qarang).
PHP idiomi: nomlangan argumentlar Builder o'rnini bosishi mumkin. PHP 8.0+ da
new Sorov(metod: 'POST', tana: '...')ko'pincha Builder ehtiyojini yo'qotadi β agar obyekt immutable va parametrlar oddiy bo'lsa. Builder'ni faqat qurish jarayonida mantiq bo'lganda (validatsiya, hisoblash, shartli qadamlar) saqlang.
Singleton β va uning TUZOQLARI¶
Singleton "butun ilovada faqat bitta nusxa bo'lsin" deydi. Klassik shakli:
<?php
declare(strict_types=1);
final class Konfig
{
private static ?self $instance = null;
private array $qiymatlar;
private function __construct() { $this->qiymatlar = ['debug' => true]; }
private function __clone() {} // klonlashni bloklaymiz
public static function instance(): self
{
return self::$instance ??= new self(); // birinchi chaqiruvda yaratiladi
}
public function get(string $kalit): mixed
{
return $this->qiymatlar[$kalit] ?? null;
}
}
var_dump(Konfig::instance() === Konfig::instance()); // bool(true) β bitta nusxa
var_dump(Konfig::instance()->get('debug')); // bool(true)
Texnik jihatdan ishlaydi. Lekin Singleton β GoF katalogidagi eng tanqid qilingan andoza, va sabablarini bilishingiz shart:
- Yashirin global holat.
Konfig::instance()ni istalgan joyda chaqirsa bo'ladi β bog'liqlik kod imzosida ko'rinmaydi.function foo()ga qarab uning Konfig'ga tayanishini bilolmaysiz. Bu β DI ning aksi. - Test qiyinligi. Testda soxta (mock) Konfig bera olmaysiz β
instance()qattiq yopishgan. Statik$instancetestlar orasida saqlanib qoladi, bir test ikkinchisiga "oqib" o'tadi (flaky testlar). - Yashirin bog'liqliklar. Singleton ishlatuvchi sinf "halol" emas: konstruktorida hech narsa so'ramaydi, lekin aslida Konfig'ga muhtoj.
Idiomatik almashtiruv β DI konteyner bilan "bitta nusxa" (singleton lifetime). Bitta nusxa kerakligi to'g'ri bo'lishi mumkin, lekin uni konteyner boshqarsin, sinfning o'zi emas. ./13 da ko'rgan konteyner aynan shuni beradi: $container->singleton(Konfig::class). Sinf esa oddiy, test qilinadigan bo'lib qoladi:
<?php
declare(strict_types=1);
// β Singleton: global, test qilib bo'lmaydigan vaqt
// Soat::instance()->hozir() β testda soxta vaqt berolmaysiz
// β
Oddiy obyekt: vaqtni TASHQARIDAN olamiz (DI)
final class Soat
{
public function __construct(private \DateTimeImmutable $hozir) {}
public function hozir(): string { return $this->hozir->format('Y-m-d'); }
}
// Testda soxta vaqt berish OSON:
$soat = new Soat(new \DateTimeImmutable('2026-06-12'));
echo $soat->hozir(), "\n"; // 2026-06-12
Qoida: "faqat bitta nusxa kerak" β bu konfiguratsiya masalasi (konteyner hal qiladi), sinf dizayni masalasi emas. Singleton andozasini yangi kodda deyarli hech qachon qo'lda yozmang. Eski kodda (legacy) uchratsangiz β uni DI ga ko'chirishni rejalashtiring.
STRUCTURAL: obyektlarni birlashtirish¶
Structural andozalar obyektlarni moslash, o'rash yoki guruhlash orqali yangi imkoniyat beradi β ko'pincha asl sinflarga tegmasdan.
Adapter β mos kelmaydigan interfeysni moslash¶
Tashqi kutubxona sizning kodingiz kutgan interfeysga mos kelmaydi. Adapter β ikki interfeys orasidagi "tarjimon". Ilovangiz Logger ni kutadi, tashqi SDK esa boshqacha imzoga ega:
<?php
declare(strict_types=1);
// Bizning ilova shu interfeysni kutadi:
interface Logger
{
public function log(string $daraja, string $xabar): void;
}
// Tashqi kutubxona BOSHQA interfeysga ega (mos emas):
final class TashqiMonolog
{
public array $yozuvlar = [];
public function write(int $level, string $message): void
{
$this->yozuvlar[] = "[{$level}] {$message}";
}
}
// Adapter: tashqi interfeysni bizning interfeysga "tarjima" qiladi
final class MonologAdapter implements Logger
{
private const XARITA = ['info' => 1, 'warning' => 2, 'error' => 3];
public function __construct(private TashqiMonolog $tashqi) {}
public function log(string $daraja, string $xabar): void
{
$this->tashqi->write(self::XARITA[$daraja] ?? 0, $xabar);
}
}
$tashqi = new TashqiMonolog();
$logger = new MonologAdapter($tashqi);
$logger->log('error', 'Disk to\'ldi');
print_r($tashqi->yozuvlar);
// Array ( [0] => [3] Disk to'ldi )
Endi ilovangiz Logger bilan ishlaydi, tashqi kutubxona almashsa β yangi adapter yozasiz, ilova kodi o'zgarmaydi. Bu β PSR-3 (./10) kabi standart interfeyslar nega muhimligining sababi: ular tashqi paketlarni adapter ortiga olishni osonlashtiradi.
Decorator β xatti-harakat qo'shish (o'rash)¶
Decorator obyektni bir xil interfeysli o'ramga joylab, atrofiga yangi xatti-harakat qo'shadi. Meros (inheritance) dan farqi: decorator'larni dinamik va bir nechtasini ustma-ust qo'yish mumkin. Bu β bevosita middleware (./12) g'oyasi.
<?php
declare(strict_types=1);
interface Hisobot
{
public function chiqar(): string;
}
final class OddiyHisobot implements Hisobot
{
public function chiqar(): string { return 'sotuv=1000'; }
}
// Decorator: bir xil interfeysni saqlaydi, atrofiga keshni qo'shadi
final class KeshDecorator implements Hisobot
{
private ?string $kesh = null;
public function __construct(private Hisobot $ichki) {}
public function chiqar(): string
{
return $this->kesh ??= '[kesh] ' . $this->ichki->chiqar();
}
}
// Yana bir qatlam: vaqt belgisini qo'shadi
final class VaqtDecorator implements Hisobot
{
public function __construct(private Hisobot $ichki) {}
public function chiqar(): string
{
return $this->ichki->chiqar() . ' @12:00';
}
}
$h = new VaqtDecorator(new KeshDecorator(new OddiyHisobot()));
echo $h->chiqar(), "\n";
// [kesh] sotuv=1000 @12:00
Har qatlam o'zining ish-ini bajaradi va ichki ga uzatadi β xuddi piyoz qatlamlari (./12 dagi "onion" model). PSR-15 middleware β Decorator'ning HTTP so'rov/javobga moslangan ko'rinishi: har middleware so'rovni o'raydi, keyingisiga uzatadi, javobni qaytishda yana o'raydi.
Decorator vs meros. Agar
KeshliVaqtliHisobot extends OddiyHisobotdeb meros qilsangiz, kombinatsiyalar portlaydi: kesh+vaqt, faqat kesh, faqat vaqt β har biriga alohida sinf. Decorator bilan ularni ishlash vaqtida kerakligicha terib chiqasiz.
Proxy β joynishin: lazy, cache, himoya¶
Proxy ham obyektni bir xil interfeys ortida o'raydi, lekin maqsadi boshqa: kirishni boshqarish. Uch keng tarqalgan tur β lazy (kechiktirilgan yaratish), cache (natijani saqlash), protection (huquq tekshirish). Mana lazy Proxy: qimmat obyekt faqat kerak bo'lganda yaratiladi:
<?php
declare(strict_types=1);
interface OgirServis
{
public function ishla(): string;
}
final class HaqiqiyServis implements OgirServis
{
public function __construct() { echo "(qimmat ulanish yaratildi)\n"; }
public function ishla(): string { return 'natija'; }
}
final class LazyProxy implements OgirServis
{
private ?OgirServis $haqiqiy = null;
public function __construct(private \Closure $yaratuvchi) {}
public function ishla(): string
{
$this->haqiqiy ??= ($this->yaratuvchi)(); // birinchi chaqiruvda yaratiladi
return $this->haqiqiy->ishla();
}
}
$proxy = new LazyProxy(fn() => new HaqiqiyServis());
echo "proxy yasaldi, hali ulanmadi\n";
echo $proxy->ishla(), "\n"; // ENDI yaratiladi
Chiqish:
HaqiqiyServis konstruktori ishla() chaqirilguncha ishlamadi β bu lazy loading. Cache proxy esa ishla() natijasini eslab qoladi; protection proxy esa ishla() oldidan huquqni tekshiradi. Decorator "yangi xulq qo'shadi", Proxy esa "kirishni boshqaradi" β interfeys bir xil, niyat boshqacha.
Facade β murakkablikni soddalashtirish¶
Facade ko'p qism-tizimni bitta sodda interfeys ortiga yashiradi. Chaqiruvchi murakkab ichki ketma-ketlikni bilmasin:
<?php
declare(strict_types=1);
final class Disk { public function saqla(string $f, string $d): void { /* ... */ } }
final class Rasm { public function thumbnail(string $f): string { return "thumb_{$f}"; } }
final class Db { public function yoz(string $jadval, array $q): int { return 42; } }
final class YuklashFacade
{
public function __construct(
private Disk $disk,
private Rasm $rasm,
private Db $db,
) {}
public function yukla(string $fayl, string $tana): int
{
$this->disk->saqla($fayl, $tana);
$thumb = $this->rasm->thumbnail($fayl);
$this->disk->saqla($thumb, 'thumb-data');
return $this->db->yoz('rasmlar', ['fayl' => $fayl, 'thumb' => $thumb]);
}
}
$facade = new YuklashFacade(new Disk(), new Rasm(), new Db());
echo 'yuklandi, id=', $facade->yukla('rasm.jpg', 'baytlar'), "\n";
// yuklandi, id=42
Chaqiruvchi yukla() deydi β diskka saqlash, thumbnail yasash, DB ga yozishning uchta qadami yashiringan. Facade ichki murakkablikni kamaytiradi, ammo Adapter'dan farqi: Adapter bitta mos kelmagan interfeysni tarjima qiladi, Facade esa ko'p qismni soddalashtiradi.
Facade vs God-object. Facade ko'p ish qilsa, u "hammasini biluvchi" obyektga aylanib qolishi mumkin. Uni yupqa tuting: faqat qadamlarni muvofiqlashtirsin, biznes mantiq qism-tizimlarda qolsin. Laravel'dagi
Cache::,Storage::β bular ham Facade (lekin ular global statik proxy, biroz boshqacha).
Composite β daraxt tuzilmasi¶
Composite bitta obyekt va obyektlar guruhini bir xil ishlatishga imkon beradi β daraxt strukturasi uchun. Klassik misol: fayl tizimi (fayl va papka, papka ichida yana papka):
<?php
declare(strict_types=1);
interface FsTugun
{
public function olcham(): int;
public function chiz(int $chuqurlik = 0): string;
}
final class Fayl implements FsTugun
{
public function __construct(private string $nom, private int $bayt) {}
public function olcham(): int { return $this->bayt; }
public function chiz(int $chuqurlik = 0): string
{
return str_repeat(' ', $chuqurlik) . "{$this->nom} ({$this->bayt}b)\n";
}
}
final class Papka implements FsTugun
{
/** @var FsTugun[] */
private array $bolalar = [];
public function __construct(private string $nom) {}
public function qosh(FsTugun $t): static { $this->bolalar[] = $t; return $this; }
public function olcham(): int
{
// butun shoxning yig'indisi β rekursiv
return array_sum(array_map(
static fn(FsTugun $t): int => $t->olcham(),
$this->bolalar,
));
}
public function chiz(int $chuqurlik = 0): string
{
$s = str_repeat(' ', $chuqurlik) . "{$this->nom}/\n";
foreach ($this->bolalar as $b) {
$s .= $b->chiz($chuqurlik + 1);
}
return $s;
}
}
$root = (new Papka('app'))
->qosh((new Papka('src'))
->qosh(new Fayl('Kernel.php', 1200))
->qosh(new Fayl('Router.php', 800)))
->qosh(new Fayl('composer.json', 300));
echo $root->chiz();
echo 'Jami: ', $root->olcham(), " bayt\n";
Chiqish:
Sehr shundaki: olcham() ni Fayl ham, Papka ham bir xil chaqiradi. Papka esa o'z bolalariga rekursiv yuradi. Chaqiruvchi "bu yaproqmi yoki shoxmi?" deb so'ramaydi β interfeys bir xil. Bu β menyu (submenular), tashkilot tuzilmasi, HTML DOM kabi har qanday daraxtga to'g'ri keladi.
BEHAVIORAL: obyektlar muloqoti¶
Behavioral andozalar obyektlar o'rtasidagi mas'uliyat va aloqani tartibga soladi. Bu oilada zamonaviy PHP eng ko'p ulush qo'shadi β closure, enum va first-class callable ko'p sinfni almashtiradi.
Strategy β almashtiriladigan algoritm¶
Strategy bir vazifaning bir nechta usulini interfeys ortiga olib, ularni almashtiriladigan qiladi. Klassik shakl β interfeys + implementatsiyalar:
<?php
declare(strict_types=1);
interface NarxStrategiya
{
public function hisobla(float $summa): float;
}
final class VipChegirma implements NarxStrategiya
{
public function hisobla(float $summa): float { return $summa * 0.8; }
}
final class OddiyNarx implements NarxStrategiya
{
public function hisobla(float $summa): float { return $summa; }
}
final class Savatcha
{
public function __construct(private NarxStrategiya $strategiya) {}
public function jami(float $summa): float
{
return $this->strategiya->hisobla($summa);
}
}
echo (new Savatcha(new VipChegirma()))->jami(1000), "\n"; // 800
To'g'ri ishlaydi. Ammo PHP da bitta metodli interfeys uchun uchta sinf β ko'pincha ortiqcha. Idiomatik shakl β closure-as-Strategy:
<?php
declare(strict_types=1);
final class Savatcha
{
/** @param callable(float): float $strategiya */
public function __construct(private $strategiya) {}
public function jami(float $summa): float
{
return ($this->strategiya)($summa);
}
}
// Har bir "strategiya" β bitta closure:
$vip = static fn(float $s): float => $s * 0.8;
echo (new Savatcha($vip))->jami(1000), "\n"; // 800
// first-class callable bilan tayyor funksiyani strategiya qilish:
$yaxlit = new Savatcha(intval(...));
echo $yaxlit->jami(999.7), "\n"; // 999
callable(float): float β strategiyaning butun "shartnomasi". intval(...) β PHP 8.1 ning first-class callable sintaksisi: mavjud funksiyani Closure'ga aylantiradi, qo'lda o'rovchi sinf yozmaysiz. Ikki yondashuvni solishtiring:
Qachon sinf, qachon closure? Strategiyaning o'z holati (state), bir nechta metodi yoki murakkab konstruktori bo'lsa β interfeys+sinf. Agar u shunchaki "kirish-chiqish" hisoblovchi bo'lsa β closure. Birini boshqasiga osongina ko'chirasiz, chunki callable interfeys ham qabul qiladi (har bir sinf __invoke bilan callable bo'la oladi).
Observer β event/listener¶
Observer "bir narsa sodir bo'lganda, qiziquvchilarning hammasiga xabar ber" deydi. Bu β event-listener tizimining poydevori (Symfony EventDispatcher, Laravel events). Idiomatik PHP da listener'lar closure yoki first-class callable bo'ladi:
<?php
declare(strict_types=1);
final class EventDispatcher
{
/** @var array<string, list<callable>> */
private array $tinglovchilar = [];
public function tingla(string $hodisa, callable $cb): void
{
$this->tinglovchilar[$hodisa][] = $cb;
}
public function yubor(string $hodisa, array $payload): void
{
foreach ($this->tinglovchilar[$hodisa] ?? [] as $cb) {
$cb($hodisa, $payload);
}
}
}
final class EmailListener
{
public array $yuborildi = [];
public function bajar(string $hodisa, array $payload): void
{
$this->yuborildi[] = "email:{$hodisa}:{$payload['user']}";
}
}
$d = new EventDispatcher();
$email = new EmailListener();
$d->tingla('user.registered', $email->bajar(...)); // first-class callable
$d->tingla('user.registered', fn($h, $p) => print("log:{$p['user']}\n"));
$d->yubor('user.registered', ['user' => 'oqil']);
print_r($email->yuborildi);
Chiqish:
Dispatcher kim tinglayotganini bilmaydi β yangi listener qo'shish dispatcher kodiga tegmaydi. $email->bajar(...) β obyekt metodini Closure sifatida uzatishning idiomatik yo'li (eski [$email, 'bajar'] massiv-callable o'rniga). Standart interfeys β PSR-14 (Event Dispatcher), uni ./10 da ko'rgansiz.
Command β amalni obyektga aylantirish¶
Command bir amalni (va uning ma'lumotini) obyektga o'raydi. Shu sabab amalni saqlash, navbatga qo'yish, bekor qilish (undo) yoki qayta bajarish mumkin bo'ladi:
<?php
declare(strict_types=1);
interface Buyruq
{
public function bajar(): string;
public function bekor(): string;
}
final class MatnQosh implements Buyruq
{
public function __construct(private string $matn) {}
public function bajar(): string { return "qo'shildi: {$this->matn}"; }
public function bekor(): string { return "o'chirildi: {$this->matn}"; }
}
final class Tarix
{
/** @var Buyruq[] */
private array $stek = [];
public function ishlat(Buyruq $b): string
{
$this->stek[] = $b;
return $b->bajar();
}
public function orqaga(): string
{
return (array_pop($this->stek))?->bekor() ?? 'bo\'sh';
}
}
$t = new Tarix();
echo $t->ishlat(new MatnQosh('salom')), "\n"; // qo'shildi: salom
echo $t->orqaga(), "\n"; // o'chirildi: salom
Stekka buyruqlarni yig'ib, orqaga() da oxirgisini chiqarib bekor() qilamiz β bu undo/redo ning poydevori. Web ilovalarda Command ko'pincha "queue job" ko'rinishida uchraydi: amal obyekt sifatida navbatga qo'yiladi, keyin worker uni bajar() qiladi. ?-> (nullsafe) array_pop bo'sh massivda null qaytarganini xavfsiz hal qiladi.
State β holat mashinasi (enum bilan)¶
State obyekt xulqi uning holatiga qarab o'zgarishini boshqaradi β va noto'g'ri o'tishlarni (transition) bloklaydi. Eski darsliklar har holatga alohida sinf yozadi. Idiomatik PHP 8.4 da enum + match ancha tiniq:
<?php
declare(strict_types=1);
enum Buyurtma: string
{
case Yangi = 'yangi';
case Tolangan = 'tolangan';
case Yuborilgan = 'yuborilgan';
case Yetkazilgan = 'yetkazilgan';
// ruxsat etilgan o'tish β holat mashinasi enum ichida
public function keyingi(): self
{
return match ($this) {
self::Yangi => self::Tolangan,
self::Tolangan => self::Yuborilgan,
self::Yuborilgan => self::Yetkazilgan,
self::Yetkazilgan => throw new \LogicException('Yakuniy holat'),
};
}
public function bekorQilSaBoladi(): bool
{
return $this === self::Yangi || $this === self::Tolangan;
}
}
$b = Buyurtma::Yangi;
$b = $b->keyingi(); // Tolangan
$b = $b->keyingi(); // Yuborilgan
echo $b->value, ' bekor? ', $b->bekorQilSaBoladi() ? 'ha' : 'yoq', "\n";
// yuborilgan bekor? yoq
Holatlar β enum case'lari; o'tish qoidalari va har holatdagi xulq β enum metodlari. Yetkazilgan dan oldinga yurish β LogicException. Enum'ning yopiq to'plam bo'lishi sizni qutqaradi: holat faqat to'rt qiymatdan biri bo'la oladi, "noma'lum holat" mantiqan imkonsiz. Enum asoslari ./05 va Value Object bilan bog'liqligi ./06 da ochilgan.
Template Method β skelet algoritm¶
Template Method algoritmning umumiy skeletini abstrakt sinfda belgilaydi, ayrim qadamlarni subklassga qoldiradi. Tartib o'zgarmas, faqat "to'ldiriladigan joylar" har xil:
<?php
declare(strict_types=1);
abstract class HisobotShablon
{
// skelet algoritm β tartib o'zgarmas (shuning uchun final)
final public function yarat(): string
{
return $this->sarlavha() . "\n" . $this->tana() . "\n" . $this->footer();
}
abstract protected function tana(): string; // subklass MAJBUR to'ldirsin
// ixtiyoriy "hook" lar β subklass xohlasa qayta yozadi
protected function sarlavha(): string { return '=== HISOBOT ==='; }
protected function footer(): string { return '--- yakun ---'; }
}
final class SotuvHisobot extends HisobotShablon
{
protected function tana(): string { return 'Jami sotuv: 5000'; }
}
echo (new SotuvHisobot())->yarat(), "\n";
Chiqish:
yarat() β final, demak hech kim umumiy tartibni buzolmaydi (sarlavha-tana-footer doim shu ketma-ketlikda). Subklass faqat tana() ni to'ldiradi. Bu β meros asosidagi andoza; uning closure-asoslangan muqobili β Strategy. Agar faqat bitta qadam o'zgaruvchan bo'lsa, ko'pincha Template Method o'rniga "o'zgaruvchan qismni closure qilib uzating" sodda chiqadi.
Chain of Responsibility β so'rovni zanjir bo'ylab uzatish¶
Chain of Responsibility so'rovni bir nechta ishlovchidan ketma-ket o'tkazadi: har biri yo hal qiladi, yo keyingisiga uzatadi. Bu β to'g'ridan-to'g'ri middleware pipeline (./12) g'oyasi. Klassik (sinf) shakl:
<?php
declare(strict_types=1);
abstract class Tekshiruvchi
{
private ?Tekshiruvchi $keyingi = null;
public function keyin(Tekshiruvchi $t): Tekshiruvchi
{
$this->keyingi = $t;
return $t;
}
public function tekshir(array $sorov): ?string
{
$xato = $this->ozQoidam($sorov);
if ($xato !== null) {
return $xato; // shu yerda to'xtaymiz
}
return $this->keyingi?->tekshir($sorov); // yoki keyingisiga uzatamiz
}
abstract protected function ozQoidam(array $sorov): ?string;
}
final class AuthTekshir extends Tekshiruvchi
{
protected function ozQoidam(array $s): ?string
{
return isset($s['token']) ? null : '401: token yo\'q';
}
}
final class RoleTekshir extends Tekshiruvchi
{
protected function ozQoidam(array $s): ?string
{
return ($s['role'] ?? '') === 'admin' ? null : '403: ruxsat yo\'q';
}
}
$zanjir = new AuthTekshir();
$zanjir->keyin(new RoleTekshir());
var_dump($zanjir->tekshir(['token' => 'x', 'role' => 'admin'])); // NULL β o'tdi
var_dump($zanjir->tekshir(['token' => 'x', 'role' => 'user'])); // 403
Har tekshiruvchi o'z qoidasini ko'radi; muvaffaqiyatda ?-> orqali keyingisiga uzatadi, xatoda zanjirni uzadi. PSR-15 middleware β bu andozaning HTTP versiyasi: har middleware $handler->handle($request) deb keyingisiga uzatadi yoki erta javob qaytaradi (short-circuit, ./12). Mashqlar bo'limida buni closure pipeline bilan sinfsiz qayta yozamiz.
PHP idiomlari: sinfsiz andozalar¶
Bir nechta idiomni yuqorida ko'rdik; bu yerda ularni yig'ib, asosiy fikrni mustahkamlaymiz: zamonaviy PHP ko'p andozani sinf ierarxiyasisiz ifodalaydi.
- closure-as-Strategy β bitta metodli strategiya interfeysi o'rniga
callable. __invokehandler β sinfni "chaqiriladigan" qiladi, ham obyekt, ham funksiya bo'ladi.- enum-as-State β holat mashinasi enum +
matchichida. - first-class callable
foo(...),$obj->m(...)β mavjud funksiya/metodni Closure'ga aylantiradi.
__invoke handler β single-action controller va Command uchun ayniqsa idiomatik:
<?php
declare(strict_types=1);
final class SalomHandler
{
public function __invoke(string $ism): string
{
return "Salom, {$ism}!";
}
}
$handler = new SalomHandler();
echo $handler('Oqil'), "\n"; // obyektni funksiya kabi chaqiramiz
$cb = $handler(...); // first-class callable
echo array_map($cb, ['A', 'B'])[1], "\n"; // Salom, B!
enum-as-State ni tryFrom bilan birga ishlatsangiz, xom satrdan xavfsiz holat quriladi:
<?php
declare(strict_types=1);
enum Holat: string
{
case Faol = 'active';
case Bloklangan = 'blocked';
}
$h = Holat::from('active');
echo $h->name, "\n"; // Faol
var_dump(Holat::tryFrom('yoq') === null); // bool(true) β noma'lum qiymat null
@template β PHP generics MEXANIKASI (factory uchun)¶
Generic factory β bir tipni qabul qilib, aynan o'sha tipni qaytaruvchi fabrika. PHP da bu sintaksis emas, balki PHPDoc annotatsiyasi: statik tahlilchi (PHPStan/Psalm) @template ni o'qiydi va tipni kuzatadi. Generikalarning tushuncha tomoni (variance, bound) TypeScript kitobida chuqur (../typescript/README.md); bu yerda faqat PHP @template mexanikasi:
<?php
declare(strict_types=1);
/**
* @template T of object
* @param class-string<T> $sinf
* @return T
*/
function yarat(string $sinf): object
{
return new $sinf();
}
final class Servis { public string $nom = 'servis'; }
$s = yarat(Servis::class); // PHPStan: $s tipi aniq Servis (mixed/object EMAS)
echo $s->nom, "\n"; // servis
Ish vaqtida yarat() oddiy new $sinf(). Lekin PHPStan uchun @template T "kirgan tip = chiqgan tip" deydi β shuning uchun $s->nom ga kirish xavfsiz, $s object emas, Servis deb biladi. Buni PHPStan'ning eng qattiq darajasi (level max) bilan tasdiqladim:
Diqqat β generic'ni qayerda ishlatish mumkin. Yuqoridagi funksiya factory'da
@templateto'liq ishlaydi. Lekin agar obyektniarray<class-string, object>xususiyatga saqlab, keyin o'qib qaytarsangiz (registry/pool), PHPStan ko'pinchaTbog'lanishini yo'qotadi β chunki massivdan o'qilgan qiymat tipiobjectbo'lib qoladi. Bu β PHP statik tahlilining hozirgi cheklovi, sizning xatoyingiz emas. Bunday hollarda eng ishonchli yo'l β generic'ni yaratuvchi funksiya/metodga qoldirish, saqlash qatlamida emas.
Pattern abuse: andoza ham ortiqcha bo'ladi¶
Andozalarni o'rganganlar tez-tez bitta tuzoqqa tushadi: har joyga andoza tiqish. Bu β alohida anti-pattern. Ikki misol:
1) Bitta operatsiya uchun butun Strategy ierarxiyasi β ortiqcha.
<?php
declare(strict_types=1);
// β KERAKSIZ: oddiy hisob uchun interfeys + sinflar
interface Operatsiya { public function bajar(int $a, int $b): int; }
final class Qoshish implements Operatsiya
{
public function bajar(int $a, int $b): int { return $a + $b; }
}
// ... yana 5 ta shunday sinf ...
$natija1 = (new Qoshish())->bajar(2, 3);
// β
IDIOMATIK: closure massivi yetadi
$amallar = [
'+' => static fn(int $a, int $b): int => $a + $b,
'-' => static fn(int $a, int $b): int => $a - $b,
];
$natija2 = $amallar['+'](2, 3);
var_dump($natija1 === $natija2); // bool(true) β bir xil natija, ~80% kam kod
Qoida: soddalik birinchi. Andoza β muammoga javob, kun boshidan rejalashtiriladigan maqsad emas. "YAGNI" (You Aren't Gonna Need It): andozani aniq ehtiyoj paydo bo'lganda kiriting. Kodni avval to'g'ridan-to'g'ri yozing; takrorlanish yoki o'zgaruvchanlik ko'rinib qolganda mos andozaga refaktoring qiling.
Pattern abuse belgilari:
- Bitta implementatsiyasi bo'lgan interfeys/abstract (kelajak "ehtimol uchun") β hozir kerak emas.
- AbstractSingletonFactoryProxyManager kabi nomlar β andoza nomlari biriksa, dizayn buzilgan.
- "Kuchli" andoza (Abstract Factory, Visitor) ikki qatorlik muammoga β bolg'a bilan yong'oq chaqish.
Andozalarni tanish uchun o'rganing (kodni o'qiganda "bu Decorator ekan" deb tushunish), majburlash uchun emas. Eng yaxshi kod β kerakli joyda kerakli andoza, qolgan joyda esa oddiy, to'g'ridan-to'g'ri kod.
Mashqlar¶
Oson¶
- Closure registry Strategy.
ChegirmaXizmatsinfini yozing:qoidaQosh(string $nom, callable $cb)bilan nomli chegirma qoidalarini ro'yxatga oladi,hisobla(string $nom, float $summa): floatesa shu qoidani qo'llaydi. Noma'lum nom uchunInvalidArgumentExceptiontashlasin. Sinab ko'ring:vip(20% chegirma) vayangi_yil(50 so'm chegirma). - Singletonni DI ga aylantiring.
Soat::instance()->hozir()ko'rinishidagi Singletonni oddiy, vaqtni konstruktor orqali oluvchiSoatsinfiga aylantiring. Nega bu test uchun yaxshiroq β bir jumlada izohlang.
O'rta¶
- Decorator zanjiri.
Servisinterfeysi (ishla(string): string) vaAsosiyServis(matnni katta harfga aylantiradi) bering.LogDecoratoryozing: u kirish va chiqishnilogmassiviga yozsin, lekin asl natijani o'zgartirmasin. Decorator'ni asl servisga o'rab, ishlatib, log'ni ko'rsating. - State enum guard.
Eshikenum (Ochiq,Yopiq,Qulflangan) yozing.harakat(string $amal): selfmetodi faqat ruxsat etilgan o'tishlarni bajarsin (och: YopiqβOchiq,yop: OchiqβYopiq,qulfla: YopiqβQulflangan), boshqasigaLogicExceptiontashlasin. Ochiq eshikni qulflashga urinib, bloklanishini ko'rsating.
Qiyin¶
- Chain of Responsibility β closure middleware pipeline. Sinflarsiz, faqat closure'lar bilan middleware pipeline yozing:
pipeline(array $bosqichlar, callable $yadro): callable. Har bosqich(array $sorov, callable $next): arrayimzosiga ega bo'lsin. Auth middleware (token yo'q bo'lsa['status' => 401]qaytarib zanjirni uzsin) va logging middleware (javobgalogged => trueqo'shsin) bilan sinang. (Maslahat:array_reduce+array_reverse.)
Yechim β 1
<?php
declare(strict_types=1);
final class ChegirmaXizmat
{
/** @var array<string, callable(float): float> */
private array $qoidalar = [];
public function qoidaQosh(string $nom, callable $cb): void
{
$this->qoidalar[$nom] = $cb;
}
public function hisobla(string $nom, float $summa): float
{
$cb = $this->qoidalar[$nom]
?? throw new \InvalidArgumentException("Noma'lum qoida: {$nom}");
return $cb($summa);
}
}
$x = new ChegirmaXizmat();
$x->qoidaQosh('vip', static fn(float $s): float => $s * 0.8);
$x->qoidaQosh('yangi_yil', static fn(float $s): float => $s - 50);
echo $x->hisobla('vip', 1000), "\n"; // 800
echo $x->hisobla('yangi_yil', 1000), "\n"; // 950
Har bir chegirma β bitta closure, alohida sinf shart emas. ?? throw β PHP 8.0 ning throw-expression idiomi: yo'q kalit uchun darhol istisno.
Yechim β 2
<?php
declare(strict_types=1);
final class Soat
{
public function __construct(private \DateTimeImmutable $hozir) {}
public function hozir(): string { return $this->hozir->format('Y-m-d'); }
}
// Ishlatish (production):
$soat = new Soat(new \DateTimeImmutable('now'));
// Test (soxta vaqt):
$test = new Soat(new \DateTimeImmutable('2026-06-12'));
echo $test->hozir(), "\n"; // 2026-06-12
Nega yaxshiroq: vaqt tashqaridan beriladi, shuning uchun testda istalgan sanani qo'yib, natijani aniq tekshira olasiz. Singleton'da instance() qattiq yopishgan bo'lib, soxta vaqt berib bo'lmaydi va statik holat testlar orasida saqlanib qoladi (flaky test).
Yechim β 3
<?php
declare(strict_types=1);
interface Servis
{
public function ishla(string $kirish): string;
}
final class AsosiyServis implements Servis
{
public function ishla(string $kirish): string { return strtoupper($kirish); }
}
final class LogDecorator implements Servis
{
public array $log = [];
public function __construct(private Servis $ichki) {}
public function ishla(string $kirish): string
{
$this->log[] = "kirish={$kirish}";
$natija = $this->ichki->ishla($kirish);
$this->log[] = "chiqish={$natija}";
return $natija;
}
}
$dek = new LogDecorator(new AsosiyServis());
echo $dek->ishla('salom'), "\n"; // SALOM
print_r($dek->log);
// Array ( [0] => kirish=salom [1] => chiqish=SALOM )
LogDecorator bir xil Servis interfeysni saqlaydi β chaqiruvchi o'ralganini sezmaydi. Asl natija o'zgarmaydi, faqat yon-ta'sir (log) qo'shiladi.
Yechim β 4
<?php
declare(strict_types=1);
enum Eshik
{
case Ochiq;
case Yopiq;
case Qulflangan;
public function harakat(string $amal): self
{
return match (true) {
$amal === 'och' && $this === self::Yopiq => self::Ochiq,
$amal === 'yop' && $this === self::Ochiq => self::Yopiq,
$amal === 'qulfla' && $this === self::Yopiq => self::Qulflangan,
default => throw new \LogicException(
"'{$amal}' mumkin emas holatda: " . $this->name,
),
};
}
}
$e = Eshik::Yopiq;
$e = $e->harakat('och');
echo $e->name, "\n"; // Ochiq
try {
$e->harakat('qulfla'); // ochiq eshikni qulflab bo'lmaydi
} catch (\LogicException $ex) {
echo 'Bloklandi: ', $ex->getMessage(), "\n";
// Bloklandi: 'qulfla' mumkin emas holatda: Ochiq
}
match (true) β bir nechta shartni tekshirishning idiomatik yo'li (./05 da type narrowing'da ham ishlatgan edik). Faqat ruxsat etilgan uchta o'tish β qolgan hamma narsa default orqali istisno. Enum yopiq to'plam bo'lgani uchun "noma'lum holat" mantiqan imkonsiz.
Yechim β 5
<?php
declare(strict_types=1);
/**
* Closure asosli middleware pipeline (PSR-15 g'oyasi, sinfsiz).
*
* @param list<callable(array, callable): array> $bosqichlar
* @param callable(array): array $yadro
* @return callable(array): array
*/
function pipeline(array $bosqichlar, callable $yadro): callable
{
return array_reduce(
array_reverse($bosqichlar),
static fn(callable $keyingi, callable $bosqich): callable
=> static fn(array $sorov): array => $bosqich($sorov, $keyingi),
$yadro,
);
}
$app = pipeline(
[
// auth middleware β token yo'q bo'lsa zanjirni uzadi
static function (array $s, callable $next): array {
if (!isset($s['token'])) {
return ['status' => 401];
}
return $next($s);
},
// logging middleware β javobni o'rab, belgi qo'shadi
static function (array $s, callable $next): array {
$javob = $next($s);
$javob['logged'] = true;
return $javob;
},
],
static fn(array $s): array => ['status' => 200, 'user' => $s['token']],
);
print_r($app(['token' => 'oqil'])); // status=200, user=oqil, logged=1
print_r($app([])); // status=401 (logging'ga yetib bormaydi)
array_reduce + array_reverse middleware'larni teskari yig'ib, "piyoz" tuzadi: tashqi bosqich avval ishlaydi. Auth token bermasa 401 qaytarib $next ni chaqirmaydi β short-circuit (./12). Diqqat: ikkinchi chaqiruvda logged yo'q, chunki auth zanjirni logging'gacha uzdi. Bu β Chain of Responsibility ning sinfsiz, idiomatik PHP ko'rinishi.
Xulosa. Andozalar β kod emas, umumiy lug'at va sinalgan yondashuv. Creational (Factory, Builder, va ehtiyotkor Singleton) obyekt yaratishni, Structural (Adapter, Decorator, Proxy, Facade, Composite) obyektlarni birlashtirishni, Behavioral (Strategy, Observer, Command, State, Template Method, Chain of Responsibility) ularning muloqotini boshqaradi. Zamonaviy PHP 8.4 da ko'pi sinfsiz ifodalanadi: closure, enum,
__invoke, first-class callable. Eng muhim qoida β soddalik birinchi: andoza muammoga javob, maqsad emas. Keyingi bobda taktik dizaynga β Repository, Service, DTO qatlamlariga o'tamiz.
β¬ οΈ Oldingi: 19 β SOLID prinsiplari Β· π README Β· Keyingi: 21 β Taktik dizayn: Repository, Service, DTO β‘οΈ