12 β PSR-15 middleware pipeline¶
β¬ οΈ Oldingi: 11 β HTTP xabarlari: PSR-7 va PSR-17 Β· π README Β· Keyingi: 13 β PSR-11 DI konteyner qurish β‘οΈ
Bu bobda: har bir zamonaviy PHP framework (Laravel, Symfony, Slim, Mezzio) yuragida yotgan tushuncha β middleware pipeline ni noldan quramiz. Avval middleware nima va nega kerakligini ko'ramiz: auth, CORS, CSRF, logging, rate-limit β har biri so'rov bilan biznes-mantiq orasidagi alohida, qayta ishlatiladigan qatlam. So'ng eng muhim aqliy model β "piyoz" (onion) ni o'rganamiz: har middleware so'rovni ichkariga uzatadi va javobni tashqariga qaytaradi (
before/aftermantiq). Keyin PSR-15 ning ikki interfeysini βMiddlewareInterface::process()vaRequestHandlerInterface::handle()β inline yozib, ulardan pipeline/dispatcher quramiz: middleware steki + yakuniy handler, har middleware$handler->handle()ni chaqirib keyingisiga o'tadi. Short-circuit ni ko'rsatamiz: auth muvaffaqiyatsiz bo'lsa darrov 401 qaytariladi, keyingi qatlamlarga umuman o'tilmaydi. Amalda 3 ta middleware (logging, auth, CORS) + final handler qurib,ServerRequestni o'tkazibResponseolamiz vabefore/aftertartibini chiqishda jonli ko'rsatamiz. Bu bob 11 β PSR-7 HTTP xabarlari ga tayanadi (immutable so'rov/javob obyektlari) va 10 β PSR standartlari dagi "kontraktga dasturlash" g'oyasini davom ettiradi. Boshlovchi MVC tushunchasi uchun 37 β MVC loyihani tartibga solish ga qarang.
Middleware nima va nega kerak?¶
Tasavvur qiling: sizda o'nlab marshrut (route) bor β /profil, /buyurtmalar, /admin/hisobot va hokazo. Endi talab keldi: har bir so'rovni logga yozish kerak. Yana: /admin/* marshrutlari faqat tizimga kirgan foydalanuvchiga ochiq bo'lsin (auth). Yana: brauzerdan AJAX so'rovlari kelishi uchun CORS sarlavhalari qo'shilsin. Yana: bitta IP daqiqada 100 martadan ko'p so'rov yubormasin (rate-limit).
Bu mantiqni har bir kontroller ichiga ko'chirib yozish β falokat. Auth tekshiruvi 50 ta metodda takrorlanadi; logga yozish hamma joyda; bittasini o'zgartirsangiz qolganlari eskirib qoladi. Bu β kesib o'tuvchi tashvish (cross-cutting concern): u bitta marshrutga emas, hammasiga taalluqli.
Middleware β aynan shu muammoning yechimi. Middleware β bu so'rov (ServerRequest) framework ga kelganidan keyin, lekin yakuniy biznes-mantiq (kontroller/handler) ga yetib bormasdan oldin turadigan qatlam. Har bir tashvish β alohida middleware:
| Middleware | Vazifasi | before mantiq |
after mantiq |
|---|---|---|---|
| Logging | So'rov/javobni qayd etish | so'rovni log qiladi | javob statusini log qiladi |
| Auth | Kirishni tekshirish | token/sessiyani tekshiradi, xato bo'lsa to'xtatadi | β |
| CORS | Brauzer ruxsatlari | β | javobga Access-Control-* qo'shadi |
| CSRF | Forma soxtalashtirilishidan himoya | tokenni solishtiradi | β |
| Rate-limit | So'rovlar sonini cheklash | hisoblagichni tekshiradi, oshsa to'xtatadi | β |
Har biri β mustaqil, qayta ishlatiladigan birlik. Bir loyihada yozgan AuthMiddleware ni boshqasiga ko'chirasiz. Tartibni almashtirasiz, birini o'chirasiz, yangisini qo'shasiz β biznes-mantiqqa tegmasdan. Bu β 10-bobdagi "kontraktga dasturlash" falsafasining HTTP qatlamiga tatbiqi.
Asosiy g'oya: middleware so'rov bilan javob orasidagi quvur (pipeline) ni qatlamlarga ajratadi. Har qatlam β bitta vazifa. Quvurni yig'ish (qaysi middleware qaysi tartibda) β markazlashtirilgan joyda, biznes-mantiqdan ajratilgan holda bo'ladi.
"Piyoz" (onion) modeli: ichkari va tashqari¶
Middleware ni tushunishning eng muhim aqliy modeli β bu piyoz (onion). Tasavvur qiling: yakuniy handler β piyozning markazi. Har middleware β uning atrofidagi qatlam. So'rov tashqi qatlamdan markazga kiradi, javob esa markazdan tashqariga chiqadi.
Har middleware ikki "qism" ga ega:
beforeβ so'rovni keyingi qatlamga uzatishdan oldin bajariladigan kod (masalan: tokenni tekshirish, log yozish, so'rovga atribut qo'shish).afterβ keyingi qatlam javob qaytargandan keyin bajariladigan kod (masalan: javobga sarlavha qo'shish, javob statusini log qilish).
Bu ikkalasi bitta funksiya ichida joylashadi β orada $handler->handle($request) chaqiruvi turadi. O'sha chaqiruv β "ichkariga o'tish" momenti:
public function process($request, $handler)
{
// === before: ichkariga kirishdan OLDIN ===
$response = $handler->handle($request); // ICHKARIGA o'tamiz (keyingi qatlam)
// === after: javob qaytgandan KEYIN ===
return $response;
}
Eng muhim natija β tartib. Agar quvurda Logging β Auth β CORS β Handler bo'lsa, bajarilish ketma-ketligi shunday:
before lar kirish tartibida (tashqaridan ichkariga), after lar esa teskari tartibda (ichkaridan tashqariga) ishlaydi β xuddi stek kabi. Logging eng tashqarida bo'lgani uchun u birinchi boshlanadi va oxirgi tugaydi β shuning uchun u butun so'rov-javob siklini "o'rab oladi" (masalan, umumiy vaqtni o'lchash uchun ideal).
Nega aynan piyoz? Chunki har middleware keyingisini o'z ichiga oladi:
Loggingningprocess()iAuthni chaqiradi,Authningprocess()iCORSni,CORSesa handler ni. Chaqiruvlar bir-birining ichida β funksiya stek kabi.aftermantiq β bu funksiyaningreturndan oldingi qismi, shuning uchun u tabiiy ravishda teskari tartibda ishlaydi.
PSR-15 ning ikki interfeysi¶
10-bobda ko'rganimizdek, PHP ekotizimi bu pattern ni standartlashtirgan β bu PSR-15 (HTTP Server Request Handlers). U atigi ikki kichik interfeysdan iborat. Shularni inline yozib, ma'nosini ko'raylik (PSR-7 o'rniga vaqtincha oddiy string ishlatamiz β toza mantiqni ko'rsatish uchun):
<?php
declare(strict_types=1);
// PSR-15 ning IKKI interfeysi (soddalashtirilgan, PSR-7 o'rniga string)
interface RequestHandlerInterface
{
// So'rovni qabul qiladi, javob qaytaradi. "Oxirgi nuqta" yoki "keyingi qatlam".
public function handle(string $request): string;
}
interface MiddlewareInterface
{
// So'rovni oladi VA keyingi qatlamga ($handler) murojaat qila oladi.
public function process(string $request, RequestHandlerInterface $handler): string;
}
// Bitta middleware: before mantiq -> keyingisini chaqiradi -> after mantiq
final class UpperMiddleware implements MiddlewareInterface
{
public function process(string $request, RequestHandlerInterface $handler): string
{
$request = strtoupper($request); // BEFORE: so'rovni o'zgartiramiz
$response = $handler->handle($request); // ICHKARIGA uzatamiz
return "[$response]"; // AFTER: javobni o'raymiz
}
}
// Yakuniy handler: shunchaki javob yasaydi
final class EchoHandler implements RequestHandlerInterface
{
public function handle(string $request): string
{
return "javob:$request";
}
}
// Middleware ni handler atrofiga "o'rab" chaqiramiz
$mw = new UpperMiddleware();
$final = new EchoHandler();
// $mw->process ichida $final->handle chaqiriladi
$result = $mw->process('salom', $final);
echo $result . "\n"; // kutiladi: [javob:SALOM]
Ishga tushiramiz:
Mana butun PSR-15 ning mohiyati shu uchta satrda:
RequestHandlerInterface::handle(request): responseβ "menga so'rov ber, men javob qaytaraman". Bu β yakuniy handler ham, "keyingi qatlam" ham bo'lishi mumkin. Bitta interfeys, ikki rol.MiddlewareInterface::process(request, handler): responseβ "menga so'rov va keyingi qatlam ber. Men so'rovni ishlab, kerak bo'lsa keyingi qatlamni chaqiraman, javobni qaytaraman".$handlerβ bu "ichkariga o'tish eshigi".process()ichidagi$handler->handle($request)β bu piyozning bir qatlamga ichkariga kirishi.
Nozik nuqta: middleware
$handler->handle()ni chaqirish majburiyati yo'q. Agar u o'zi javob qaytarsa (chaqirmasa), pipeline shu yerda to'xtaydi β bu short-circuit (keyinroq ko'ramiz). Aynan shu erkinlik auth/rate-limit ni mumkin qiladi.
Haqiqiy PSR-15 da string o'rniga PSR-7 obyektlari turadi (Psr\Http\Message\ServerRequestInterface va ResponseInterface), lekin interfeyslarning shakli aynan shu. Composer bilan o'rnatsangiz (composer require psr/http-server-middleware), aynan shu ikki interfeys keladi.
PSR-7 asosida: ServerRequest va Response¶
Endi string o'rniga 11-bobdagi PSR-7 uslubidagi immutable obyektlarga o'tamiz. Bu blok keyingi barcha misollarning poydevori β uni o'qib chiqing, qolganlari shunga tayanadi. (PSR-7 ning to'liq interfeysi katta; bu β minimal, lekin haqiqiy uslubdagi implementatsiya; production da nyholm/psr7 yoki guzzlehttp/psr7 ishlatiladi.)
<?php
declare(strict_types=1);
// === PSR-7 minimal (11-bobdan) ===
interface MessageInterface
{
public function getHeaderLine(string $name): string;
public function withHeader(string $name, string $value): static;
public function getBody(): string;
}
final class ServerRequest implements MessageInterface
{
/** @param array<string,string> $headers @param array<string,mixed> $attributes */
public function __construct(
public readonly string $method = 'GET',
public readonly string $path = '/',
private array $headers = [],
private array $attributes = [],
private string $body = '',
) {}
public function getHeaderLine(string $name): string
{
return $this->headers[strtolower($name)] ?? '';
}
public function withHeader(string $name, string $value): static
{
$clone = clone $this;
$clone->headers[strtolower($name)] = $value;
return $clone;
}
public function getBody(): string { return $this->body; }
public function getAttribute(string $name, mixed $default = null): mixed
{
return $this->attributes[$name] ?? $default;
}
public function withAttribute(string $name, mixed $value): static
{
$clone = clone $this;
$clone->attributes[$name] = $value;
return $clone;
}
}
final class Response implements MessageInterface
{
/** @param array<string,string> $headers */
public function __construct(
public readonly int $status = 200,
private array $headers = [],
private string $body = '',
) {}
public function getHeaderLine(string $name): string
{
return $this->headers[strtolower($name)] ?? '';
}
public function withHeader(string $name, string $value): static
{
$clone = clone $this;
$clone->headers[strtolower($name)] = $value;
return $clone;
}
public function getBody(): string { return $this->body; }
public function getStatus(): int { return $this->status; }
}
Nega
withAttribute()muhim? PSR-7 so'rovi immutable (o'zgarmas). Lekin middleware ko'pincha keyingi qatlamlarga ma'lumot uzatishi kerak β masalan, Auth middleware "bu so'rov foydalanuvchisioqil" deb belgilashi kerak. BuniwithAttribute()qiladi: u yangi so'rov nusxasini qaytaradi (eskisi o'zgarmaydi), unga atribut qo'shilgan. Handler keyingetAttribute('user')bilan o'qiydi. Bu β middleware lar orasidagi rasmiy ma'lumot kanali. Immutability nima vacloneqanday ishlashini 11-bobda batafsil ko'rgansiz.
PSR-15 kontraktlari va Pipeline (dispatcher)¶
Endi string o'rniga ServerRequest/Response ishlatadigan haqiqiy PSR-15 interfeyslarini va eng muhim qism β Pipeline (dispatcher) ni yozamiz. Pipeline β middleware steki ustida turuvchi mexanizm; uning vazifasi: stekni navbatma-navbat aylanib chiqish va oxirida final handler ga yetish.
// === PSR-15 kontraktlari (INLINE) ===
interface RequestHandlerInterface
{
public function handle(ServerRequest $request): Response;
}
interface MiddlewareInterface
{
public function process(ServerRequest $request, RequestHandlerInterface $handler): Response;
}
// === Pipeline / Dispatcher ===
final class Pipeline implements RequestHandlerInterface
{
/** @var list<MiddlewareInterface> */
private array $queue = [];
public function __construct(private RequestHandlerInterface $finalHandler) {}
public function pipe(MiddlewareInterface $mw): self
{
$this->queue[] = $mw;
return $this;
}
public function handle(ServerRequest $request): Response
{
// Stekdan keyingi middleware ni olamiz; tugasa - final handler.
$mw = array_shift($this->queue);
if ($mw === null) {
return $this->finalHandler->handle($request);
}
// Hozirgi middleware ga "qolgan pipeline" ni handler sifatida beramiz.
return $mw->process($request, $this);
}
}
Bu β butun bobning eng muhim 15 satri. Diqqat bilan tahlil qilaylik:
Pipelinening o'ziRequestHandlerInterfaceni implement qiladi. Ya'ni pipeline ham bitta handler. Bu β fokusning siri.handle()chaqirilganda stekdan bitta middleware niarray_shiftbilan oladi.- Agar middleware qolmagan bo'lsa (
null) β demak markazga yetdik, final handler ni chaqiramiz. - Aks holda β middleware ning
process()iga so'rov va$this(ya'ni pipeline ning o'zi) ni "keyingi handler" sifatida beramiz. - Middleware ichida
$handler->handle()ni chaqirsa, u yana shuhandle()metodiga qaytadi β bu safar stekda bittaga kam middleware bor. Shunday qilib rekursiya orqali qatlamlar birin-ketin ochiladi.
Bu nega piyozni hosil qiladi? Har
process()chaqiruvi PHP stekida yangi kadr ochadi.Logging.processichidaAuth.process, uning ichidaCORS.process, uning ichidaHandler.handleβ barchasi biri ikkinchisining ichida. Final handlerreturnqilganda, stek teskari ochiladi:CORSningafteri, keyinAuthning, keyinLoggingning. Piyoz aynan shu β chaqiruv stekining tabiiy shakli.
array_shifthaqida ogohlantirish: u$queueni o'zgartiradi (element olib tashlaydi). Demak buPipelineobyekti bir martalik β bitta so'rovga ishlatiladi. Ko'p so'rov uchun har safar yangiPipelineyarating (yoki indeks bilan ishlovchi immutable variantni β Mashq 3 da). Haqiqiy framework lar (Slim, Mezzio) indeksli, qayta ishlatiladigan dispatcher ishlatadi.
Amaliy: 3 middleware + final handler¶
Endi to'liq misol β uchta haqiqiy middleware (Logging, Auth, CORS) va final handler. Har biri echo bilan qachon ishlaganini chiqaradi, shunda piyoz tartibini o'z ko'zimiz bilan ko'ramiz. Bu blok yuqoridagi ikki blok (PSR-7 + PSR-15/Pipeline) bilan birga ishlaydi.
// === Final handler ===
final class HelloHandler implements RequestHandlerInterface
{
public function handle(ServerRequest $request): Response
{
$user = $request->getAttribute('user', 'mehmon');
echo " [HANDLER] ish bajarildi (user={$user})\n";
return new Response(200, ['content-type' => 'text/plain'], "Salom, {$user}!");
}
}
// === 1) Logging middleware (before + after) ===
final class LoggingMiddleware implements MiddlewareInterface
{
public function process(ServerRequest $request, RequestHandlerInterface $handler): Response
{
echo "[LOG before] {$request->method} {$request->path}\n";
$response = $handler->handle($request); // ICHKARIGA uzatamiz
echo "[LOG after] status={$response->getStatus()}\n"; // javob qaytdi
return $response;
}
}
// === 2) Auth middleware (short-circuit) ===
final class AuthMiddleware implements MiddlewareInterface
{
public function __construct(private string $token = 'sirli') {}
public function process(ServerRequest $request, RequestHandlerInterface $handler): Response
{
echo " [AUTH before] tokenni tekshiramiz\n";
if ($request->getHeaderLine('Authorization') !== "Bearer {$this->token}") {
echo " [AUTH] RAD ETILDI -> 401 (keyingilarga O'TMAYDI)\n";
return new Response(401, ['content-type' => 'text/plain'], 'Ruxsat yo\'q');
}
$request = $request->withAttribute('user', 'oqil'); // keyingi qatlamlarga uzatamiz
$response = $handler->handle($request);
echo " [AUTH after] o'tdi\n";
return $response;
}
}
// === 3) CORS middleware (faqat after - javobga header qo'shadi) ===
final class CorsMiddleware implements MiddlewareInterface
{
public function process(ServerRequest $request, RequestHandlerInterface $handler): Response
{
echo " [CORS before] (hech narsa)\n";
$response = $handler->handle($request);
echo " [CORS after] Access-Control-Allow-Origin qo'shildi\n";
return $response->withHeader('Access-Control-Allow-Origin', '*');
}
}
// === In-process run: MUVAFFAQIYATLI so'rov ===
echo "===== 1-SO'ROV: to'g'ri token =====\n";
$pipeline = new Pipeline(new HelloHandler());
$pipeline->pipe(new LoggingMiddleware())
->pipe(new AuthMiddleware('sirli'))
->pipe(new CorsMiddleware());
$req = new ServerRequest('GET', '/profil', ['authorization' => 'Bearer sirli']);
$res = $pipeline->handle($req);
echo "NATIJA: status={$res->getStatus()}, body=\"{$res->getBody()}\", "
. "CORS=\"{$res->getHeaderLine('Access-Control-Allow-Origin')}\"\n";
Bu kodni in-process ishga tushirganda (jonli server shart emas β biz ServerRequest ni qo'lda qurib pipeline'dan o'tkazyapmiz) quyidagi chiqishni olamiz:
===== 1-SO'ROV: to'g'ri token =====
[LOG before] GET /profil
[AUTH before] tokenni tekshiramiz
[CORS before] (hech narsa)
[HANDLER] ish bajarildi (user=oqil)
[CORS after] Access-Control-Allow-Origin qo'shildi
[AUTH after] o'tdi
[LOG after] status=200
NATIJA: status=200, body="Salom, oqil!", CORS="*"
Mana piyoz isboti! Chekinishga (otstup) qarang β u qatlam chuqurligini ko'rsatadi:
beforelar yuqoridan pastga (LOG β AUTH β CORS) β tashqaridan ichkariga.- O'rtada
[HANDLER]β eng chuqur nuqta. afterlar teskari (CORS β AUTH β LOG) β ichkaridan tashqariga.
Yana e'tibor bering: [HANDLER] chiqishida user=oqil β bu Auth middleware withAttribute('user', 'oqil') bilan qo'ygan atribut handler ga yetib bordi. Va yakuniy javobda CORS="*" β CORS ning after qismi javobga sarlavha qo'shgani isboti.
Short-circuit: auth muvaffaqiyatsiz β darrov 401¶
Endi eng kuchli imkoniyat β short-circuit (qisqa tutashuv). Agar middleware $handler->handle() ni chaqirmasdan o'zi javob qaytarsa, pipeline shu yerda to'xtaydi: keyingi qatlamlar va final handler umuman ishlamaydi. Aynan shu narsa auth/rate-limit ni mumkin qiladi.
Bir xil pipeline ga tokensiz so'rov yuboraylik. AuthMiddleware ichidagi if ni eslang: token mos kelmasa, u return new Response(401, ...) qiladi β $handler->handle() ga umuman yetmaydi.
// === In-process run: SHORT-CIRCUIT (noto'g'ri token) ===
echo "===== 2-SO'ROV: token yo'q (short-circuit) =====\n";
$pipeline2 = new Pipeline(new HelloHandler());
$pipeline2->pipe(new LoggingMiddleware())
->pipe(new AuthMiddleware('sirli'))
->pipe(new CorsMiddleware());
$req2 = new ServerRequest('GET', '/profil', []); // Authorization header yo'q
$res2 = $pipeline2->handle($req2);
echo "NATIJA: status={$res2->getStatus()}, body=\"{$res2->getBody()}\"\n";
Chiqish:
===== 2-SO'ROV: token yo'q (short-circuit) =====
[LOG before] GET /profil
[AUTH before] tokenni tekshiramiz
[AUTH] RAD ETILDI -> 401 (keyingilarga O'TMAYDI)
[LOG after] status=401
NATIJA: status=401, body="Ruxsat yo'q"
Diqqat bilan solishtiring birinchi so'rov bilan:
[CORS before],[HANDLER],[CORS after]β butunlay yo'q. Auth$handler->handle()ni chaqirmagani uchun ichkari qatlamlar ochilmadi.- Lekin
[LOG after]baribir ishladi! Nega? ChunkiLoggingAuthdan tashqarida β uAuth.process()ni chaqirgan,Authesa unga 401 javobni qaytargan. Logging uchun bu shunchaki "qaytgan javob" β u uni log qiladi (status=401) va qaytaradi.
Bu nega kuchli? Auth tekshiruvi markazlashtirilgan: handler hech qachon "foydalanuvchi kim?" deb tashvishlanmaydi β agar so'rov handler ga yetib kelgan bo'lsa, demak u allaqachon auth dan o'tgan. Va Logging tashqarida bo'lgani uchun rad etilgan so'rovlar ham logga tushadi. Tartib muhim: agar Logging ni Auth dan ichkariga qo'ysangiz, 401 bilan tugagan so'rovlar logga tushmasdi. Shuning uchun framework lar middleware tartibini jiddiy hujjatlashtiradi.
Real hayotda: xuddi shu pattern bilan rate-limit (limitdan oshsa darrov
429 Too Many Requests), maintenance rejimi (sayt yopiq bo'lsa darrov503), CSRF (token mos kelmasa419) ishlaydi. Hammasi β$handler->handle()ni chaqirmasdan javob qaytarish. Keyingi bo'limda rate-limit ni ko'ramiz.
Short-circuit yana: rate-limit va closure middleware¶
Yana ikki amaliy texnikani ko'rsataylik: rate-limit middleware (holatni saqlovchi, short-circuit qiluvchi) va closure adapter β har safar to'liq sinf yozmasdan, oddiy funksiyani middleware ga aylantirish. Bu blok o'zini-tutgan (kerakli minimal sinflar shu yerda).
<?php
declare(strict_types=1);
// Minimal qayta-ishlatiladigan asoslar
final class Req
{
/** @param array<string,mixed> $attrs */
public function __construct(public array $attrs = []) {}
public function with(string $k, mixed $v): static
{
$c = clone $this; $c->attrs[$k] = $v; return $c;
}
public function get(string $k, mixed $d = null): mixed { return $this->attrs[$k] ?? $d; }
}
final class Resp
{
public function __construct(public int $status = 200, public string $body = '') {}
}
interface Handler { public function handle(Req $r): Resp; }
interface Middleware { public function process(Req $r, Handler $h): Resp; }
final class Pipe implements Handler
{
/** @var list<Middleware> */ private array $q = [];
public function __construct(private Handler $final) {}
public function pipe(Middleware $m): self { $this->q[] = $m; return $this; }
public function handle(Req $r): Resp
{
$m = array_shift($this->q);
return $m === null ? $this->final->handle($r) : $m->process($r, $this);
}
}
// === Closure adapter: oddiy funksiyani middleware ga aylantiradi ===
final class CallableMiddleware implements Middleware
{
/** @param callable(Req, Handler): Resp $fn */
public function __construct(private mixed $fn) {}
public function process(Req $r, Handler $h): Resp { return ($this->fn)($r, $h); }
}
// === Rate-limit middleware (short-circuit 429) ===
final class RateLimit implements Middleware
{
private int $hits = 0;
public function __construct(private int $max = 2) {}
public function process(Req $r, Handler $h): Resp
{
if (++$this->hits > $this->max) {
return new Resp(429, 'Juda ko\'p so\'rov'); // SHORT-CIRCUIT
}
return $h->handle($r);
}
}
$final = new class implements Handler {
public function handle(Req $r): Resp { return new Resp(200, 'OK#' . $r->get('n')); }
};
// Closure middleware: so'rovga atribut qo'shadi (to'liq sinf yozmasdan)
$timer = new CallableMiddleware(function (Req $r, Handler $h): Resp {
$r = $r->with('t', 'belgilandi');
return $h->handle($r);
});
$rl = new RateLimit(max: 2); // bitta obyekt - holat (hits) so'rovlar orasida saqlanadi
foreach ([1, 2, 3] as $n) {
$pipe = (new Pipe($final))->pipe($timer)->pipe($rl);
$res = $pipe->handle(new Req(['n' => $n]));
echo "so'rov #{$n}: status={$res->status}, body=\"{$res->body}\"\n";
}
Chiqish:
so'rov #1: status=200, body="OK#1"
so'rov #2: status=200, body="OK#2"
so'rov #3: status=429, body="Juda ko'p so'rov"
Birinchi ikki so'rov o'tdi (limit max=2), uchinchisi β short-circuit bilan 429. RateLimit obyekti bitta bo'lib, $hits holatini so'rovlar orasida saqladi. CallableMiddleware esa to'liq sinf yozish o'rniga closure ni middleware ga o'rashning qulay yo'lini ko'rsatadi β kichik, bir martalik mantiq uchun ideal.
Ko'prik: bu pattern Slim Framework ning
$app->add(function($req, $handler) {...})chaqiruviga to'g'ridan-to'g'ri mos keladi. Laravel da middleware βhandle($request, Closure $next)(bu yerda$next= bizning$handler), Symfony da β event listener'lar. Nomi har xil, mohiyati bir xil: so'rov bilan handler orasidagi o'raluvchi qatlamlar.
Tartib muhim: keng tarqalgan xatolar¶
Middleware tartibi β bu logik to'g'rilik masalasi, did emas. Ba'zi keng tarqalgan xatolar:
// β XATO: Auth ni Logging dan ICHKARIGA qo'ygan
$pipeline->pipe(new AuthMiddleware()) // tashqarida
->pipe(new LoggingMiddleware()); // ichkarida
// Natija: 401 bilan rad etilgan so'rovlar LOGGA TUSHMAYDI,
// chunki Auth short-circuit qilganda Logging ga umuman yetmaydi.
// β
TO'G'RI: Logging eng tashqarida - hamma narsani (rad etilganni ham) ko'radi
$pipeline->pipe(new LoggingMiddleware()) // tashqarida
->pipe(new AuthMiddleware()); // ichkarida
Yana bir tipik xato β CORS ni Auth dan keyin qo'yish: agar Auth 401 bilan short-circuit qilsa, CORS sarlavhalari javobga qo'shilmaydi, va brauzer 401 ni "CORS xatosi" deb noto'g'ri ko'rsatadi. Shuning uchun CORS odatda eng tashqi qatlamlardan biri bo'ladi.
Umumiy qoida sifatida tartib (tashqaridan ichkariga): CORS β Logging β Rate-limit β Auth β CSRF β ... β Handler. Lekin bu loyihaga bog'liq β muhimi, har bir qatlamning short-circuit holatida tashqi qatlamlar baribir ishlashini hisobga olish.
Xulosa va keyingisi¶
Bu bobda zamonaviy PHP framework larining yuragi β PSR-15 middleware pipeline ni noldan qurdik. Asosiy xulosalar:
- Middleware β so'rov bilan biznes-mantiq orasidagi alohida, qayta ishlatiladigan qatlam. Auth, CORS, CSRF, logging, rate-limit kabi kesib o'tuvchi tashvishlar ni biznes-mantiqdan ajratadi.
- "Piyoz" (onion) modeli β har middleware keyingisini o'z ichiga oladi. So'rov ichkariga (
before), javob tashqariga (after) βafterlar teskari tartibda, xuddi stek kabi. - PSR-15 atigi ikki interfeysdan iborat:
RequestHandlerInterface::handle()("menga so'rov ber β javob beraman") vaMiddlewareInterface::process()("menga so'rov va keyingi qatlam ber"). - Pipeline/dispatcher β
RequestHandlerInterfaceni o'zi implement qiladi;array_shiftbilan stekdan middleware larni navbatma-navbat olib, oxirida final handler ga yetadi. Rekursiya piyozni hosil qiladi. - Short-circuit β middleware
$handler->handle()ni chaqirmay javob qaytarsa, pipeline to'xtaydi (auth β 401, rate-limit β 429). Tashqi qatlamlarningafterqismi baribir ishlaydi. - Tartib muhim: Logging va CORS odatda eng tashqarida (rad etilgan so'rovlarni ham ko'rishi uchun).
So'rov bilan ishlaydigan pipeline tayyor. Lekin bitta savol qoldi: middleware lar va handler larga ularning bog'liqliklarini (logger, repozitoriy, ma'lumotlar bazasi ulanishi) kim beradi? Hozircha biz hammasini qo'lda new bilan yasayapmiz. Keyingi bobda β 13 β PSR-11 DI konteyner qurish β bu muammoni hal qilamiz: Reflection (08-bob) yordamida bog'liqliklarni avtomatik bog'laydigan (autowiring) konteyner quramiz. Pipeline + konteyner + router β bu uchlik birga sizning shaxsiy mini-framework ingiz ni tashkil etadi.
β¬ οΈ Oldingi: 11 β HTTP xabarlari: PSR-7 va PSR-17 Β· π README Β· Keyingi: 13 β PSR-11 DI konteyner qurish β‘οΈ
Mashqlar¶
Oson¶
1. TimerMiddleware yozing: u so'rov boshlanishida vaqtni qayd etadi (hrtime(true)), $handler->handle() ni chaqiradi, javob qaytgach o'tgan vaqtni millisekundda hisoblab echo qiladi. Bu qaysi "qism" (before/after) ga tegishli? Nega bu middleware eng tashqi qatlamda bo'lishi mantiqiy?
2. Yuqoridagi CorsMiddleware ni o'zgartiring: u faqat Access-Control-Allow-Origin emas, balki Access-Control-Allow-Methods: GET, POST sarlavhasini ham qo'shsin. Bu o'zgarish before ga ta'sir qiladimi yoki faqat after ga?
O'rta¶
3. Joriy Pipeline array_shift bilan o'zini "iste'mol qiladi" (bir martalik). Immutable variant yozing: IndexedPipeline $index maydoni va array $middleware saqlasin; handle() da $this->middleware[$index] ni olib, keyingi qatlam sifatida yangi IndexedPipeline($middleware, $index + 1) bersin. Shunda bitta pipeline ni ko'p marta ishlatish mumkin bo'ladi. Ikki so'rov o'tkazib, ikkalasi ham to'g'ri ishlashini ko'rsating.
4. MaintenanceMiddleware yozing: konstruktor bool $yopiq qabul qiladi. Agar $yopiq === true bo'lsa, har qanday so'rovga darrov 503 "Sayt texnik ishlar uchun yopiq" qaytarsin (short-circuit). Aks holda so'rovni o'tkazib yuborsin. Buni pipeline ga eng tashqi qatlam qilib qo'shing va ikki holatni (yopiq/ochiq) sinab ko'ring.
Qiyin¶
5. To'liq mini-router + middleware integratsiyasini yozing. Router (final handler vazifasini bajaradi) ServerRequest->path ga qarab ikki marshrutni ajratsin: /ommaviy (auth talab qilmaydi) va /maxfiy (auth talab qiladi). Buni marshrutga-bog'liq middleware orqali yeching: /maxfiy so'rovi AuthMiddleware dan o'tsin, /ommaviy esa o'tmasin. Maslahat: ikki alohida pipeline quring (biri auth bilan, biri auths) va Router so'rov path iga qarab tegishlisiga yo'naltirsin. To'rt so'rov bilan sinang: /ommaviy (tokensiz β 200), /maxfiy (tokensiz β 401), /maxfiy (token bilan β 200), /noma'lum (β 404).
Yechim β 1
TimerMiddleware ham before, ham after qismidan foydalanadi: boshlanishni before da, hisoblashni after da qiladi. U eng tashqi qatlamda bo'lishi mantiqiy, chunki shunda u butun pipeline (barcha boshqa middleware + handler) sarflagan vaqtni o'lchaydi β agar ichkarida bo'lsa, faqat o'zidan ichkaridagi vaqtni ko'rardi.
<?php
declare(strict_types=1);
// (Req, Resp, Handler, Middleware, Pipe sinflari oldingi bloklardan)
final class TimerMiddleware implements MiddlewareInterface
{
public function process(ServerRequest $request, RequestHandlerInterface $handler): Response
{
$boshlandi = hrtime(true); // BEFORE: vaqtni qayd etamiz
$response = $handler->handle($request); // ichkariga
$msek = (hrtime(true) - $boshlandi) / 1_000_000; // AFTER: o'tgan vaqt
echo "Bajarilish vaqti: " . number_format($msek, 3) . " ms\n";
return $response;
}
}
Yechim β 2
Faqat after ga ta'sir qiladi β chunki sarlavhalar javobga qo'shiladi, javob esa $handler->handle() qaytgandan keyin mavjud bo'ladi. before (so'rov) o'zgarmaydi:
final class CorsMiddleware implements MiddlewareInterface
{
public function process(ServerRequest $request, RequestHandlerInterface $handler): Response
{
$response = $handler->handle($request); // before da hech narsa
// after: ikki sarlavhani ketma-ket qo'shamiz (har withHeader yangi nusxa qaytaradi)
return $response
->withHeader('Access-Control-Allow-Origin', '*')
->withHeader('Access-Control-Allow-Methods', 'GET, POST');
}
}
withHeader immutable bo'lgani uchun zanjir (chaining) bilan ketma-ket qo'shamiz β har biri yangi Response nusxasi qaytaradi.
Yechim β 3
Asosiy farq: array_shift o'rniga $index ishlatamiz va keyingi qatlamni yangi obyekt qilib uzatamiz β joriy obyekt o'zgarmaydi, shuning uchun qayta ishlatish mumkin.
<?php
declare(strict_types=1);
// (ServerRequest, Response, RequestHandlerInterface, MiddlewareInterface oldingi bloklardan)
final class IndexedPipeline implements RequestHandlerInterface
{
/** @param list<MiddlewareInterface> $middleware */
public function __construct(
private array $middleware,
private RequestHandlerInterface $finalHandler,
private int $index = 0,
) {}
public function handle(ServerRequest $request): Response
{
// Shu indeksda middleware bormi?
if (!isset($this->middleware[$this->index])) {
return $this->finalHandler->handle($request); // tugadi - final
}
$mw = $this->middleware[$this->index];
// Keyingi qatlam - YANGI pipeline (index+1). Joriy obyekt o'zgarmaydi.
$next = new self($this->middleware, $this->finalHandler, $this->index + 1);
return $mw->process($request, $next);
}
}
// Sinab ko'ramiz: bitta pipeline ni IKKI marta ishlatamiz
$mws = [new LoggingMiddleware(), new AuthMiddleware('sirli')];
$pipeline = new IndexedPipeline($mws, new HelloHandler());
$r1 = $pipeline->handle(new ServerRequest('GET', '/a', ['authorization' => 'Bearer sirli']));
echo "1: {$r1->getStatus()} {$r1->getBody()}\n";
// O'SHA pipeline obyekti - ikkinchi so'rov (chunki immutable, $index o'zgarmagan)
$r2 = $pipeline->handle(new ServerRequest('GET', '/b', []));
echo "2: {$r2->getStatus()} {$r2->getBody()}\n";
Kutilgan chiqish (loglardan tashqari):
Ikkala so'rov ham to'g'ri ishladi, chunki IndexedPipeline hech qachon o'z $middleware yoki $index ini o'zgartirmaydi β har chaqiruvda $index lokal, keyingi qatlam esa yangi obyekt. Bu β Slim/Mezzio uslubidagi production-tayyor yondashuv.
Yechim β 4
<?php
declare(strict_types=1);
// (ServerRequest, Response, interfeyslar, Pipeline oldingi bloklardan)
final class MaintenanceMiddleware implements MiddlewareInterface
{
public function __construct(private bool $yopiq) {}
public function process(ServerRequest $request, RequestHandlerInterface $handler): Response
{
if ($this->yopiq) {
// SHORT-CIRCUIT: handler ga umuman o'tmaymiz
return new Response(503, ['retry-after' => '3600'], 'Sayt texnik ishlar uchun yopiq');
}
return $handler->handle($request); // ochiq - odatdagidek o'tkazamiz
}
}
// 1-holat: sayt YOPIQ
$p1 = new Pipeline(new HelloHandler());
$p1->pipe(new MaintenanceMiddleware(yopiq: true))->pipe(new LoggingMiddleware());
$r1 = $p1->handle(new ServerRequest('GET', '/', []));
echo "Yopiq: {$r1->getStatus()} {$r1->getBody()}\n";
// 2-holat: sayt OCHIQ
$p2 = new Pipeline(new HelloHandler());
$p2->pipe(new MaintenanceMiddleware(yopiq: false))->pipe(new LoggingMiddleware());
$r2 = $p2->handle(new ServerRequest('GET', '/', []));
echo "Ochiq: {$r2->getStatus()} {$r2->getBody()}\n";
Kutilgan chiqish (loglardan tashqari):
MaintenanceMiddleware eng tashqi qatlam bo'lgani uchun, yopiq holatda boshqa hech narsa (hatto logging ham) ishlamaydi β sayt to'liq qulflangan.
Yechim β 5
To'liq yechim: Router so'rov path iga qarab tegishli pipeline'ga (yoki to'g'ridan-to'g'ri 404 ga) yo'naltiradi. /maxfiy uchun auth'li pipeline, /ommaviy uchun auths pipeline ishlatamiz. Bu β marshrutga-bog'liq middleware ning eng sodda, ishonchli implementatsiyasi.
<?php
declare(strict_types=1);
// === PSR-7 minimal + PSR-15 (oldingi bloklardan) ===
final class ServerRequest
{
/** @param array<string,string> $headers */
public function __construct(
public readonly string $method = 'GET',
public readonly string $path = '/',
private array $headers = [],
private array $attributes = [],
) {}
public function getHeaderLine(string $n): string { return $this->headers[strtolower($n)] ?? ''; }
public function getAttribute(string $n, mixed $d = null): mixed { return $this->attributes[$n] ?? $d; }
public function withAttribute(string $n, mixed $v): static
{
$c = clone $this; $c->attributes[$n] = $v; return $c;
}
}
final class Response
{
public function __construct(public readonly int $status = 200, public string $body = '') {}
public function getStatus(): int { return $this->status; }
public function getBody(): string { return $this->body; }
}
interface RequestHandlerInterface { public function handle(ServerRequest $r): Response; }
interface MiddlewareInterface
{
public function process(ServerRequest $r, RequestHandlerInterface $h): Response;
}
final class Pipeline implements RequestHandlerInterface
{
/** @var list<MiddlewareInterface> */ private array $queue = [];
public function __construct(private RequestHandlerInterface $final) {}
public function pipe(MiddlewareInterface $m): self { $this->queue[] = $m; return $this; }
public function handle(ServerRequest $r): Response
{
$m = array_shift($this->queue);
return $m === null ? $this->final->handle($r) : $m->process($r, $this);
}
}
// === Auth middleware ===
final class AuthMiddleware implements MiddlewareInterface
{
public function __construct(private string $token = 'sirli') {}
public function process(ServerRequest $r, RequestHandlerInterface $h): Response
{
if ($r->getHeaderLine('Authorization') !== "Bearer {$this->token}") {
return new Response(401, 'Ruxsat yo\'q');
}
return $h->handle($r->withAttribute('user', 'oqil'));
}
}
// === Yakuniy handler: kontent qaytaradi ===
final class PageHandler implements RequestHandlerInterface
{
public function handle(ServerRequest $r): Response
{
$user = $r->getAttribute('user', 'mehmon');
return new Response(200, "{$r->path} sahifasi (user={$user})");
}
}
// === Router: path ga qarab tegishli pipeline ni tanlaydi ===
final class Router implements RequestHandlerInterface
{
public function handle(ServerRequest $r): Response
{
return match ($r->path) {
// ommaviy: auths pipeline (faqat handler)
'/ommaviy' => (new Pipeline(new PageHandler()))->handle($r),
// maxfiy: auth'li pipeline (avval AuthMiddleware, keyin handler)
'/maxfiy' => (new Pipeline(new PageHandler()))
->pipe(new AuthMiddleware('sirli'))
->handle($r),
// mos marshrut yo'q
default => new Response(404, 'Topilmadi'),
};
}
}
// === Sinov: 4 so'rov ===
$router = new Router();
$cases = [
['GET', '/ommaviy', []], // tokensiz -> 200
['GET', '/maxfiy', []], // tokensiz -> 401
['GET', '/maxfiy', ['authorization' => 'Bearer sirli']], // token -> 200
['GET', '/nomalum', []], // -> 404
];
foreach ($cases as [$m, $path, $h]) {
$res = $router->handle(new ServerRequest($m, $path, $h));
echo str_pad($path, 12) . " -> {$res->getStatus()} | {$res->getBody()}\n";
}
Kutilgan chiqish:
/ommaviy -> 200 | /ommaviy sahifasi (user=mehmon)
/maxfiy -> 401 | Ruxsat yo'q
/maxfiy -> 200 | /maxfiy sahifasi (user=oqil)
/nomalum -> 404 | Topilmadi
Tahlil: Router ham RequestHandlerInterface ni implement qiladi β u "yakuniy handler" rolida, lekin ichida path ga qarab turli pipeline larni tanlaydi. /ommaviy da AuthMiddleware umuman yo'q (shuning uchun tokensiz ham 200, user=mehmon). /maxfiy da Auth bor β tokensiz 401 (short-circuit, PageHandler ga yetmaydi), token bilan esa Auth withAttribute('user', 'oqil') qo'yib o'tkazadi (user=oqil). default esa pipeline ga umuman kirmasdan 404 qaytaradi. Bu β Slim/Laravel dagi "route middleware" tushunchasining yadrosi: turli marshrutlarga turli middleware steki. Keyingi 13-bobda bu marshrutlar va middleware larga bog'liqliklarni avtomatik beradigan DI konteyner ni qo'shamiz.