13 β PSR-11 DI konteyner qurish (autowiring)¶
β¬ οΈ Oldingi: 12 β PSR-15 middleware pipeline Β· π README Β· Keyingi: 14 β Routing va front controller β‘οΈ
Bu bobda: Framework yadrosining yuragi β Dependency Injection konteynerini qo'lda quramiz. Avval DI (bog'liqlikni tashqaridan berish) va DI konteyner (bog'liqliklarni avtomatik yig'uvchi mexanizm) o'rtasidagi farqni aniqlaymiz. So'ng service location anti-pattern nega yomonligini ko'rsatamiz. PSR-11
ContainerInterfacekontraktini (get/has) inline yozamiz. Keyin asosiy "sehr" β autowiring: konstruktor parametrlariniReflectionClass/ReflectionParameter/ReflectionNamedTypebilan o'qib (bu 08-bob ga tayanadi), bog'liqliklarni rekursiv ravishda avtomatik quramiz. Undan keyin binding (interfeysni konkret implementatsiyaga bog'lash β "interfeysga dasturlash"), lifetime (singleton/shared vs transient/factory), aylanma bog'liqlik (AβBβA) ni aniqlash va aniq xato berish, hamda compiled/cached konteyner g'oyasini (Reflection sekin β production'da bir marta hisoblab kesh) o'rganamiz. Oxirida hamma narsani birlashtirgan, haqiqatan ishlaydigan kichik konteyner quramiz va in-process sinab ko'ramiz. Boshlovchi DI/MVC asoslari uchun../php/37-mvc-loyihani-tartibga-solish.mdva../php/38-foydali-dizayn-andozalari.mdga, Reflection uchun./08-reflection-attributes.mdga, tip tizimi uchun./05-tip-tizimi.mdga tayanamiz.
DI vs DI konteyner: farqni aniqlaymiz¶
Boshlovchi kitobda (../php/38-foydali-dizayn-andozalari.md) Dependency Injection g'oyasini ko'rib o'tgansiz: sinf o'ziga kerakli obyektni ichkarida new qilib yaratmaydi, balki uni tashqaridan (odatda konstruktor orqali) qabul qiladi. Bu β toza "DI" tamoyili va u hech qanday konteynersiz ham mavjud.
Quyida konteyner umuman yo'q β bu shunchaki qo'lda DI:
<?php
declare(strict_types=1);
interface Mailer { public function send(string $to): string; }
final class SmtpMailer implements Mailer
{
public function __construct(private string $host) {}
public function send(string $to): string { return "{$this->host} -> {$to}"; }
}
final class Notifier
{
// Bog'liqlik (Mailer) tashqaridan beriladi -> bu DI
public function __construct(private Mailer $mailer) {}
public function ping(): string { return $this->mailer->send('a@b.uz'); }
}
// Qo'lda yig'amiz: avval Mailer, keyin uni Notifier ga uzatamiz
$mailer = new SmtpMailer('smtp.local');
$notifier = new Notifier($mailer);
echo $notifier->ping() . "\n"; // smtp.local -> a@b.uz
Bu kod ajoyib: Notifier Mailer ga bog'liq, lekin uni o'zi yaratmaydi β shuning uchun testda soxta (mock) mailer berish oson, implementatsiyani almashtirish oson. Muammo boshqa joyda: loyiha o'sgani sari new A(new B(new C(new D(...)))) zanjiri uzayib ketadi. Har bir kontroller, har bir servis uchun shu "yig'ish kodi"ni qo'lda yozish β zerikarli, takrorlanuvchi va xatoga moyil.
Aynan shu yerda DI konteyner kiradi. Konteyner β bu "obyektlar fabrikasi" bo'lib, undan "menga Notifier ber" deb so'rasangiz, u:
Notifierning konstruktoriga qarab, ungaMailerkerakligini avtomatik aniqlaydi,Mailerni (yoki uning konkret implementatsiyasini) o'zi yaratadi,- uni
Notifierga uzatib, tayyor obyektni qaytaradi.
| DI (toza tamoyil) | DI konteyner | |
|---|---|---|
| Bog'liqlik | tashqaridan beriladi | tashqaridan beriladi |
| Kim yig'adi? | siz, qo'lda new bilan |
konteyner, avtomatik (autowiring) |
| Kerakli kod | har joyda yig'ish zanjiri | bitta get($id) chaqiruvi |
| Asos | dizayn andozasi | Reflection + bog'lanishlar jadvali |
Asosiy fikr: DI β bu tamoyil ("yaratma β qabul qil"). DI konteyner esa shu tamoyilni avtomatlashtiruvchi vosita. Konteyner DI ni majburlamaydi, lekin uni miqyoslanadigan (scalable) qiladi. Laravel ning
app(), Symfony ning service container β aynan shu.
Service location: nega bu anti-pattern?¶
Konteyner bilan ishlashning ikki yo'li bor va ular tubdan farq qiladi. Birinchisi β dependency injection (yaxshi): bog'liqliklar konstruktorga aniq tip sifatida keladi. Ikkinchisi β service location (yomon): sinf konteynerning o'zini qabul qiladi va kerak bo'lganda undan get('...') bilan xizmat tortib oladi.
<?php
declare(strict_types=1);
interface Mailer { public function send(string $to): string; }
final class SmtpMailer implements Mailer
{
public function __construct(private string $host) {}
public function send(string $to): string { return "{$this->host} -> {$to}"; }
}
// Oddiy "service locator"
final class ServiceLocator
{
/** @var array<string,object> */
private array $services = [];
public function set(string $id, object $s): void { $this->services[$id] = $s; }
public function get(string $id): object { return $this->services[$id]; }
}
final class OrderServiceBad
{
// β Locator ni qabul qilib, ICHKARIDA get() chaqiramiz
public function __construct(private ServiceLocator $locator) {}
public function place(): string
{
// β Imzo (signature) Mailer ga bog'liqligini YASHIRADI
$mailer = $this->locator->get('mailer');
return $mailer->send('x@y.uz');
}
}
$loc = new ServiceLocator();
$loc->set('mailer', new SmtpMailer('smtp.bad'));
$bad = new OrderServiceBad($loc);
echo $bad->place() . "\n"; // smtp.bad -> x@y.uz
Kod ishlaydi, lekin u anti-pattern. Nima uchun?
- Yashirin bog'liqlik.
OrderServiceBadning konstruktoriga qarasangiz, uServiceLocatorga bog'liqdek ko'rinadi. Aslida esa uMailerga bog'liq β lekin buni faqat metod ichini o'qib bilasiz. Imzo yolg'on gapiradi. - Test qiyin. Bu sinfni testlash uchun butun boshli locator yasab, unga
'mailer'kalitini to'g'ri nom bilan qo'yishingiz kerak. KonstruktorMailerni so'raganida esa testda shunchaki mock berasiz. - Kompilyatsiya vaqtida tekshirilmaydi.
get('mailer')β bu satr.'maler'deb adashsangiz, xato faqat ish vaqtida portlaydi. Tip esa (05-bob) statik tahlilchi bilan oldindan ushlanadi. - Konteynerga bog'lanish. Sinfingiz endi konteyner mavjudligini biladi va unga "yopishadi". Boshqa loyihaga ko'chirish qiyin.
Qoida: Konteynerdan
get()ni faqat eng tashqi nuqtada (front controller / router) chaqiring β u yerda "ildiz" obyektni so'raysiz. Undan keyin hamma narsa konstruktor injeksiyasi orqali oqib o'tsin. Servislaringiz ichida$container->get(...)ko'rsangiz β bu signal: siz service locator yozyapsiz.
PSR-11: ContainerInterface kontrakti¶
Ekotizimdagi konteynerlar bir-biriga moslashishi uchun PHP-FIG PSR-11 ni chiqargan (10-bob da ko'rib o'tgan edik). Bu β psr/container paketidagi juda kichik interfeys. Kontraktni o'z ko'zingiz bilan ko'rishingiz uchun uni inline yozamiz:
<?php
declare(strict_types=1);
namespace Psr\Container;
// Bazaviy istisno (marker) interfeyslari
interface ContainerExceptionInterface extends \Throwable {}
interface NotFoundExceptionInterface extends ContainerExceptionInterface {}
interface ContainerInterface
{
/**
* Berilgan id bo'yicha kirish (entry) ni topib qaytaradi.
* @throws NotFoundExceptionInterface id topilmasa
* @throws ContainerExceptionInterface boshqa xatolik bo'lsa
*/
public function get(string $id): mixed;
/** id mavjudligini tekshiradi (yaratmasdan). */
public function has(string $id): bool;
}
Hammasi shu β atigi ikki metod. get($id) identifikator bo'yicha xizmatni qaytaradi (yo'q bo'lsa NotFoundExceptionInterface otadi), has($id) esa uni yaratmasdan mavjudligini aytadi. PSR-11 ataylab minimal: u konteyner qanday qurilishini (autowiring, config, ...) belgilamaydi β faqat iste'mol qilish tomonini standartlashtiradi.
Diqqat: PSR-11 β bu read-only kontrakt. Unda
bind()/set()yo'q. Sababi: kontrakt iste'molchi (masalan, router) uchun. Qanday ro'yxatdan o'tkazish β har bir konteynerning o'z ishi. Bizbind()/singleton()metodlarini qo'shamiz, lekinget/hasPSR-11 ga aniq mos bo'ladi.
Amalda esa interfeysni qo'lda yozmaysiz β uni Composer bilan o'rnatasiz (paket juda kichik):
Keyin use Psr\Container\ContainerInterface; qilib, konteyneringiz shu interfeysni implements qiladi β natijada uni har qanday PSR-11 mos kutubxonaga ula olasiz.
Autowiring: Reflection bilan avtomatik yig'ish¶
Endi yadroga keldik. Autowiring β konteynerning bir sinfni so'raganingizda uning konstruktorini o'qib, kerakli bog'liqliklarni o'zi aniqlab, ularni rekursiv yaratib berishi. Buning uchun 08-bobdagi Reflection API kerak.
Avval g'oyani bitta parametrda ko'raylik: bir sinfning konstruktor parametrining tipini Reflection bilan o'qiymiz.
<?php
declare(strict_types=1);
interface Mailer { public function send(string $to): string; }
final class Notifier
{
public function __construct(private Mailer $mailer) {}
}
$rc = new ReflectionClass(Notifier::class);
$param = $rc->getConstructor()->getParameters()[0];
$type = $param->getType();
echo "param nomi: {$param->getName()}\n"; // mailer
echo "tip: {$type->getName()}\n"; // Mailer
echo "builtin: " . var_export($type->isBuiltin(), true) . "\n"; // false
Mana avtomatik wiring uchun zarur bo'lgan barcha ma'lumot:
getConstructor()βReflectionMethodyoki konstruktor bo'lmasanull.getParameters()βReflectionParametermassivi.$param->getType()βReflectionNamedType(yoki union/null). Undan:getName()β tip nomi (masalanMailer).isBuiltin()β bu ichki tipmi (int,string,bool,array...) yoki sinf/interfeysmi? Aynan shu β autowiring qarorining ayrigichi:falsebo'lsa, biz uni rekursivget()bilan yaratamiz.
Mana butun autowiring oqimi diagrammada:
Algoritm aniq:
get($id)β avval keshni va bog'lanishlarni tekshir; topilmasa, sinfni quramiz.ReflectionClass($id)β konstruktorni o'qi. Konstruktor yo'q bo'lsa, shunchakinew $id().- Har bir parametr uchun tipni ko'r:
- Sinf/interfeys (
isBuiltin() === false) β$this->get($type->getName())β rekursiv. - Skalyar (
isBuiltin() === true) β standart qiymat bormi (isDefaultValueAvailable()), yokinullruxsatmi, yoki xato. - Yig'ilgan argumentlar bilan
newInstanceArgs($args)β tayyor obyekt.
Binding: interfeysni implementatsiyaga bog'lash¶
Autowiring bitta jiddiy muammoga duch keladi: interfeysni new qilib bo'lmaydi. Notifier Mailer interfeysini so'rasa, konteyner new Mailer() qila olmaydi β qaysi konkret klassni? Aynan shu yerda binding kerak: biz konteynerga "Mailer so'ralganda SmtpMailer ber" deb aytamiz. Bu β "interfeysga dasturlash" tamoyilining konteyner darajasidagi ifodasi.
Diagrammaning yuqori qismi β binding: bind(Logger::class, FileLogger::class). Endi Logger so'ralganda konteyner FileLogger ni quradi. Pastki qismi β lifetime, uni keyingi bo'limda ochamiz.
Binding turlari:
- Interfeys β konkret sinf:
bind(Mailer::class, SmtpMailer::class). - Factory closure:
bind(Mailer::class, fn($c) => new SmtpMailer('smtp.local'))β yaratish mantig'i murakkab bo'lsa (config'dan host olish kabi). - Tayyor instance:
instance(Config::class, $config)β allaqachon yasalgan obyektni ro'yxatga qo'yish.
Lifetime: singleton vs transient¶
Konteyner obyektni necha marta yaratishi kerak? Ikki asosiy rejim bor:
- Singleton (shared): obyekt bir marta yaratiladi, keshlanadi va har keyingi
get()da o'sha obyekt qaytadi. Mantiqan bitta nusxa bo'lishi kerak bo'lgan, holatsiz (stateless) yoki "qimmat" servislar uchun: ulanish hovuzi (connection pool), config, logger. - Transient (factory): har
get()chaqiruvida yangi obyekt yaratiladi. Har safar toza holat kerak bo'lganda: masalan, bir martalik buyruq (command) obyekti, request-ga bog'liq holat.
Diagrammaning pastki qismida farq aniq: singleton da get() #1 va get() #2 bir xil obyektni (#A1) qaytaradi; transient da esa ikki xil obyekt (#B1, #B2).
Tanlash qoidasi: Servis ichida o'zgaruvchan holat bo'lmasa (faqat metodlar) β singleton (xotira tejaladi). Har chaqiruvda mustaqil, toza nusxa kerak bo'lsa β transient. Shubha bo'lsa, framework'lar odatda servislarni singleton qiladi.
Aylanma bog'liqlik (circular dependency)¶
Tasavvur qiling: A konstruktorida B ni so'raydi, B esa konstruktorida A ni. Konteyner A ni qurish uchun B ni so'raydi, B ni qurish uchun yana A ni so'raydi... cheksiz rekursiya va dasturning qulashi. Bu β aylanma bog'liqlik, va yaxshi konteyner buni aniq xato bilan ushlashi shart, "Maximum function nesting" kabi tushunarsiz halokat bilan emas.
Yechim oddiy: hozir qurilayotgan sinflarni "building" ro'yxatida saqlaymiz. Agar allaqachon ro'yxatda turgan sinfni qayta qurishga harakat qilsak β zanjir aylangan, demak xato.
// instantiate() ning boshida (to'liq kontekst keyingi bo'limda):
if (isset($this->building[$class])) {
$zanjir = implode(' -> ', array_keys($this->building)) . " -> {$class}";
throw new ContainerException("Aylanma bog'liqlik aniqlandi: {$zanjir}");
}
$this->building[$class] = true; // qurishni boshladik
// ... bog'liqliklarni resolve qilamiz ...
unset($this->building[$class]); // qurib bo'ldik (finally bilan)
finally bloki muhim: bog'liqlikni resolve qilishda xato chiqsa ham, sinfni building dan olib tashlash kerak, aks holda keyingi (haqiqiy) get() chaqiruvi yolg'on "aylanma" xatosi beradi.
To'liq konteyner: hammasini birlashtiramiz¶
Endi yuqoridagi barcha g'oyalarni β PSR-11, autowiring, binding, lifetime, aylanma aniqlash β bitta ishlaydigan konteynerga yig'amiz. Avval PSR-11 interfeysini (yuqoridagidek) bering yoki composer require psr/container qiling. Quyidagi blok shu interfeys mavjud deb yozilgan.
<?php
declare(strict_types=1);
use Psr\Container\ContainerInterface;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
// --- Istisnolar (PSR-11 marker interfeyslarini implement qiladi) ---
final class NotFoundException extends \Exception implements NotFoundExceptionInterface {}
final class ContainerException extends \Exception implements ContainerExceptionInterface {}
// --- Lifetime turi ---
enum Lifetime
{
case Singleton; // bir marta yaratiladi, keyin qayta ishlatiladi
case Transient; // har safar yangi obyekt
}
final class Binding
{
/** @param \Closure|string|null $concrete Factory closure YOKI konkret sinf nomi */
public function __construct(
public readonly \Closure|string|null $concrete,
public readonly Lifetime $lifetime,
) {}
}
final class Container implements ContainerInterface
{
/** @var array<string, Binding> id => binding */
private array $bindings = [];
/** @var array<string, mixed> singleton keshlangan obyektlar */
private array $instances = [];
/** @var array<string, true> hozir resolve qilinayotgan id lar (circular aniqlash) */
private array $building = [];
/** Interfeys/sinf -> konkret sinf yoki factory bog'lash (transient). */
public function bind(string $id, \Closure|string|null $concrete = null): void
{
$this->bindings[$id] = new Binding($concrete ?? $id, Lifetime::Transient);
}
/** Singleton bog'lash: bir marta yaratilib, keyin qayta ishlatiladi. */
public function singleton(string $id, \Closure|string|null $concrete = null): void
{
$this->bindings[$id] = new Binding($concrete ?? $id, Lifetime::Singleton);
}
/** Tayyor qiymatni to'g'ridan-to'g'ri ro'yxatdan o'tkazish (config va h.k.). */
public function instance(string $id, mixed $object): void
{
$this->instances[$id] = $object;
$this->bindings[$id] = new Binding(null, Lifetime::Singleton);
}
public function has(string $id): bool
{
return isset($this->bindings[$id])
|| isset($this->instances[$id])
|| class_exists($id)
|| interface_exists($id);
}
public function get(string $id): mixed
{
// 1) Singleton keshda bormi?
if (isset($this->instances[$id])) {
return $this->instances[$id];
}
// 2) Aniq bog'lanish bormi?
$binding = $this->bindings[$id] ?? null;
// 3) Bog'lanish ham, sinf ham yo'q -> NotFound
if ($binding === null && !class_exists($id)) {
throw new NotFoundException("Konteynerda '{$id}' uchun bog'lanish topilmadi.");
}
$object = $this->build($id, $binding);
// 4) Singleton bo'lsa keshlaymiz
if ($binding !== null && $binding->lifetime === Lifetime::Singleton) {
$this->instances[$id] = $object;
}
return $object;
}
private function build(string $id, ?Binding $binding): mixed
{
$concrete = $binding?->concrete ?? $id;
// Factory closure bo'lsa: o'zini chaqiramiz, konteynerni beramiz
if ($concrete instanceof \Closure) {
return $concrete($this);
}
// Aks holda $concrete - sinf nomi: uni autowiring bilan quramiz
return $this->instantiate($concrete);
}
/** Reflection bilan sinfni va uning bog'liqliklarini avtomatik quradi. */
private function instantiate(string $class): object
{
// Aylanma: A -> B -> A bo'lsa, A allaqachon "building" da turibdi
if (isset($this->building[$class])) {
$zanjir = implode(' -> ', array_keys($this->building)) . " -> {$class}";
throw new ContainerException("Aylanma bog'liqlik aniqlandi: {$zanjir}");
}
try {
$reflector = new \ReflectionClass($class);
} catch (\ReflectionException $e) {
throw new NotFoundException("'{$class}' sinfini topib bo'lmadi.", 0, $e);
}
if (!$reflector->isInstantiable()) {
throw new ContainerException(
"'{$class}' ni yaratib bo'lmaydi (interfeys yoki abstrakt). "
. "Avval bind() bilan konkret sinfga bog'lang."
);
}
$constructor = $reflector->getConstructor();
// Konstruktor yo'q -> shunchaki yangi obyekt
if ($constructor === null) {
return new $class();
}
$this->building[$class] = true;
try {
$args = array_map(
fn (\ReflectionParameter $p) => $this->resolveParameter($p, $class),
$constructor->getParameters(),
);
} finally {
// Xato chiqsa ham building dan olib tashlash SHART
unset($this->building[$class]);
}
return $reflector->newInstanceArgs($args);
}
/** Bitta konstruktor parametrini hal qiladi. */
private function resolveParameter(\ReflectionParameter $param, string $owner): mixed
{
$type = $param->getType();
// Sinf/interfeys tipidagi parametr -> rekursiv get()
if ($type instanceof \ReflectionNamedType && !$type->isBuiltin()) {
return $this->get($type->getName());
}
// Skalyar/ichki tur: standart qiymat bormi?
if ($param->isDefaultValueAvailable()) {
return $param->getDefaultValue();
}
// null qabul qiladimi?
if ($type !== null && $type->allowsNull()) {
return null;
}
throw new ContainerException(
"'{$owner}' sinfining \${$param->getName()} parametrini hal qilib bo'lmadi "
. "(tip yo'q yoki ichki tur, standart qiymat ham yo'q)."
);
}
}
Diqqat qiling: resolveParameter() da getType() ReflectionNamedType ekanini va isBuiltin() ni tekshiramiz. Union/intersection tiplar uchun bu yetarli emas (ular ReflectionUnionType qaytaradi) β ularni qo'shimcha mantiq bilan qo'llab-quvvatlash mumkin, lekin asosiy g'oya shu. Bu β 05-bobdagi tip tizimi va 08-bobdagi Reflection bilimining to'g'ridan-to'g'ri qo'llanishi.
Konteynerni sinab ko'ramiz (in-process)¶
Endi konteynerni haqiqiy domen sinflarida ishga tushiramiz. Quyidagi test butunlay o'zini tutadi (yuqoridagi Container mavjud deb hisoblaydi) va assert() lar bilan har bir xususiyatni tekshiradi.
<?php
declare(strict_types=1);
// (Yuqoridagi Container va PSR-11 interfeyslari shu yerda mavjud deb faraz qilamiz)
interface Logger { public function log(string $m): void; }
final class EchoLogger implements Logger
{
public array $messages = [];
public function log(string $m): void { $this->messages[] = $m; }
}
final class Database
{
public function __construct(public readonly string $dsn = 'sqlite::memory:') {}
}
final class UserRepository
{
public function __construct(
public readonly Database $db,
public readonly Logger $logger,
) {}
}
final class UserService
{
public function __construct(public readonly UserRepository $repo) {}
public function dsn(): string { return $this->repo->db->dsn; }
}
$c = new Container();
// Logger interfeysini konkret implementatsiyaga bog'laymiz
$c->bind(Logger::class, EchoLogger::class);
// AUTOWIRING: UserService -> UserRepository -> (Database + Logger) avtomatik
$service = $c->get(UserService::class);
assert($service instanceof UserService);
assert($service->repo instanceof UserRepository);
assert($service->repo->db instanceof Database);
assert($service->repo->logger instanceof EchoLogger); // interfeys -> impl
assert($service->dsn() === 'sqlite::memory:'); // skalyar default ishladi
echo "Autowiring + binding: OK\n";
get(UserService::class) bitta chaqiruvda butun grafni qurdi: UserService UserRepository ni, u esa Database (skalyar $dsn uchun standart qiymat) va Logger (interfeys β EchoLogger) ni so'radi. Hech qaysisini biz qo'lda new qilmadik.
Endi lifetime ni tekshiramiz:
<?php
declare(strict_types=1);
// (Container, Logger, EchoLogger, Database yuqorida berilgan)
$c = new Container();
$c->bind(Logger::class, EchoLogger::class); // transient
$c->singleton(Database::class); // singleton
// SINGLETON: ikki get() bir xil obyekt
$a = $c->get(Database::class);
$b = $c->get(Database::class);
assert($a === $b, 'singleton bir xil bo\'lishi kerak');
// TRANSIENT: ikki get() har xil obyekt
$la = $c->get(Logger::class);
$lb = $c->get(Logger::class);
assert($la !== $lb, 'transient har xil bo\'lishi kerak');
echo "Singleton vs transient: OK\n";
// FACTORY closure: yaratish mantig'ini o'zimiz beramiz
$c2 = new Container();
$c2->singleton(Database::class, fn (Container $ct) => new Database('mysql:host=localhost'));
assert($c2->get(Database::class)->dsn === 'mysql:host=localhost');
echo "Factory closure: OK\n";
// AYLANMA bog'liqlik aniqlanadi
final class AlfaServis { public function __construct(public BetaServis $b) {} }
final class BetaServis { public function __construct(public AlfaServis $a) {} }
$c3 = new Container();
try {
$c3->get(AlfaServis::class);
echo "XATO: aylanma aniqlanmadi\n";
} catch (ContainerException $e) {
assert(str_contains($e->getMessage(), 'Aylanma'));
echo "Circular: OK -> {$e->getMessage()}\n";
}
Ishga tushirilganda chiqish:
Singleton vs transient: OK
Factory closure: OK
Circular: OK -> Aylanma bog'liqlik aniqlandi: AlfaServis -> BetaServis -> AlfaServis
Aylanma xatosi zanjirni aniq ko'rsatadi: AlfaServis -> BetaServis -> AlfaServis. Bu β yaxshi konteyner va yomon konteyner farqi. Yomon konteyner shunchaki "stack overflow" beradi; bizniki muammoning aynan qayerdaligini aytadi.
Compiled/cached konteyner: Reflection sekin¶
Bizning konteyner toza, lekin bir kamchiligi bor: har get() da Reflection ishlaydi. ReflectionClass yaratish, konstruktorni o'qish, parametrlarni tahlil qilish β bularning hammasi nisbatan sekin. Bitta so'rovda o'nlab servis quriladigan real ilovada bu sezilarli sekinlikka aylanadi. 08-bobda ham aytganimizdek: Reflection β kuchli, lekin uni har so'rovda qaytadan ishlatish β eng keng tarqalgan performance xatosi.
Yechim β compiled (kompilyatsiya qilingan) konteyner. G'oya: bog'liqlik grafi development paytida o'zgaradi, lekin production'da o'zgarmaydi. Demak, Reflection ni faqat bir marta (deploy/build paytida) ishlatib, natijani β ya'ni "qaysi sinfni qanday yig'ish" rejasini β oddiy PHP fayl sifatida yozib qo'yamiz. Production'da shu fayl yuklanadi, Reflection umuman ishlamaydi β faqat to'g'ridan-to'g'ri new chaqiruvlari qoladi. Bu β aynan Symfony ning yondashuvi (var/cache/.../Container.php β minglab qatorlik avtogeneratsiya qilingan fayl) va Laravel ning config:cache/optimizatsiya g'oyasiga yaqin.
Quyida soddalashtirilgan namuna β graf bo'yicha factory massivini PHP fayl sifatida generatsiya qilamiz va keyin Reflection'siz yuklaymiz:
<?php
declare(strict_types=1);
interface Cache { public function get(string $k): ?string; }
final class ArrayCache implements Cache
{
public function get(string $k): ?string { return null; }
}
final class Repo
{
public function __construct(public Cache $cache) {}
}
/**
* COMPILE bosqichi: graf bo'yicha PHP kod-string generatsiya qiladi.
* Real konteynerda bu reja Reflection bilan AVTOMATIK chiqariladi;
* bu yerda natijani soddalashtirib ko'rsatamiz.
*/
function compileContainer(): string
{
$code = "<?php\nreturn [\n";
$code .= " Cache::class => fn() => new ArrayCache(),\n";
$code .= " Repo::class => fn(\$c) => new Repo(\$c[Cache::class]()),\n";
$code .= "];\n";
return $code;
}
$file = sys_get_temp_dir() . '/cache_container.php';
file_put_contents($file, compileContainer());
// PRODUCTION yuklash: Reflection YO'Q, faqat tayyor factory massivi
$factories = require $file;
$repo = $factories[Repo::class]($factories);
assert($repo instanceof Repo);
assert($repo->cache instanceof ArrayCache);
echo "Compiled konteyner: OK (Reflection ishlatilmadi)\n";
@unlink($file);
Bu yerda muhim kontseptsiya, kod emas: development'da qulaylik (autowiring, "shunchaki ishlaydi"), production'da tezlik (oldindan hisoblangan, keshlangan plan). Ikki dunyoning ikkala yaxshi tomonini olamiz.
Amaliy maslahat: O'zingiz konteyner yozmang β Symfony DI, Laravel container, yoki yengil
PHP-DIdan foydalaning. Ammo bu bobni o'qiganingizdan keyin ular endi "sehr" emas. Siz autowiring, binding, lifetime va compile g'oyasining ostidagi mexanizmni bilasiz β bu xatolarni tushunish va to'g'ri konfiguratsiya qilishda hal qiluvchi ahamiyatga ega.
Mashqlar¶
Oson¶
Container::get()nitry/catchichida chaqirib, bog'lanmagan interfeys (masalan, hech qachonbind()qilinmaganLogger::class) so'rang. Qaysi istisno otiladi βNotFoundExceptionmi yokiContainerExceptionmi? Nega? (Maslahat:class_exists('Logger')interfeys uchun nima qaytaradi?)- Konteynerga
instance(Config::class, $config)bilan tayyor obyekt qo'shing vaget(Config::class)aynan o'sha obyektni (===) qaytarishiniassertbilan tasdiqlang. has()metodini sinab ko'ring: mavjud konkret sinf, mavjud interfeys va umuman yo'q nom ('YoqMutlaqo') uchun navbati bilantrue,true,falseqaytishini tekshiring.
O'rta¶
- Konstruktorida
?Logger $logger = null(nullable, standartnull) parametri bo'lgan sinf yarating. UniLoggerbog'lanmagan konteynerdanget()qiling βresolveParameter()nullqaytarishini va xato otilmasligini tasdiqlang. Nega bu xavfli bo'lishi mumkin? Containergamake(string $class, array $params = [])metodini qo'shing: u autowiring qiladi, lekin$paramsda berilgan nomli argumentlar (masalan['dsn' => 'pgsql:...']) skalyar parametrlar uchun standart qiymat o'rniga ishlatilsin. Test bilan tekshiring.- Singleton va transient farqini statistika bilan ko'rsating: bir interfeysni
bind()(transient) qiling, uni 3 martaget()qiling va har safarspl_object_id()har xil ekanini tasdiqlang. So'ngsingleton()ga o'zgartiring vaspl_object_id()bir xil ekanini ko'rsating.
Qiyin¶
#[Inject]atributi bilan skalyar injeksiya. Konteynerni shunday kengaytiring: agar konstruktor parametri ichki tur (int,string) bo'lsa va unga#[Inject('kalit')]atributi qo'yilgan bo'lsa, qiymatni konteynerning sozlamalar jadvalidan oling; atribut bo'lmasa standart qiymat; u ham bo'lmasa istisno. (Bu autowiring'ni config injeksiyasi bilan birlashtiradi β Symfony ning#[Autowire]ga o'xshash.)- Aylanma bog'liqlikni setter injeksiyasi bilan yeching. Konstruktor injeksiyasi aylanma grafni qura olmaydi.
AvaBo'zaro bog'liq bo'lganda, birini konstruktorda, ikkinchisinisetB(B $b)setter metodi orqali keyin in'ektsiya qiluvchi mexanizm qo'shing. Konteyner avvalAni (proxy/qisman) yaratib, keyin setterni chaqirishi kerak.
Yechim β 7
Asosiy g'oya: resolveParameter() da skalyar parametr uchun avval #[Inject] atributini tekshiramiz. Quyidagi yechim alohida, o'zini-tutgan konteyner sifatida (asosiy g'oyani izolyatsiyada ko'rsatish uchun) yozilgan va ishlaydi:
<?php
declare(strict_types=1);
use Psr\Container\ContainerInterface; // psr/container yoki inline interfeys
#[Attribute(Attribute::TARGET_PARAMETER)]
final class Inject
{
public function __construct(public string $key) {}
}
final class InjectAwareContainer implements ContainerInterface
{
private array $config = [];
private array $bindings = [];
public function setConfig(string $key, mixed $val): void { $this->config[$key] = $val; }
public function bind(string $id, string $concrete): void { $this->bindings[$id] = $concrete; }
public function has(string $id): bool
{
return isset($this->bindings[$id]) || class_exists($id);
}
public function get(string $id): mixed
{
$class = $this->bindings[$id] ?? $id;
$rc = new \ReflectionClass($class);
$ctor = $rc->getConstructor();
if ($ctor === null) {
return new $class();
}
$args = [];
foreach ($ctor->getParameters() as $p) {
$type = $p->getType();
// 1) Sinf/interfeys tipi -> rekursiv
if ($type instanceof \ReflectionNamedType && !$type->isBuiltin()) {
$args[] = $this->get($type->getName());
continue;
}
// 2) #[Inject('kalit')] bormi?
$attrs = $p->getAttributes(Inject::class);
if ($attrs !== []) {
$key = $attrs[0]->newInstance()->key;
if (!array_key_exists($key, $this->config)) {
throw new \RuntimeException("Inject kaliti yo'q: {$key}");
}
$args[] = $this->config[$key];
continue;
}
// 3) Standart qiymat
if ($p->isDefaultValueAvailable()) {
$args[] = $p->getDefaultValue();
continue;
}
throw new \RuntimeException("\${$p->getName()} ni hal qilib bo'lmadi");
}
return $rc->newInstanceArgs($args);
}
}
final class ApiKlient
{
public function __construct(
#[Inject('api.base_url')] public string $baseUrl,
#[Inject('api.timeout')] public int $timeout = 30,
) {}
}
$c = new InjectAwareContainer();
$c->setConfig('api.base_url', 'https://api.example.uz');
$c->setConfig('api.timeout', 10);
$klient = $c->get(ApiKlient::class);
assert($klient->baseUrl === 'https://api.example.uz');
assert($klient->timeout === 10);
echo "#[Inject] skalyar injeksiya: OK -> {$klient->baseUrl}, t={$klient->timeout}\n";
Mantiq tartibi muhim: avval sinf tipi (rekursiv), keyin #[Inject] (config'dan), keyin standart qiymat, oxirida xato. getAttributes(Inject::class) (08-bob) atributni o'qiydi, newInstance() esa undan Inject obyektini yaratib key ni beradi. Bu β getType()->isBuiltin() tekshiruvi (skalyarni sinfdan ajratish) va atribut o'qishning birlashmasi.
Yechim β 8
Konstruktor injeksiyasi aylanma uchun fundamental ishlamaydi: A ni qurish uchun B to'liq tayyor bo'lishi kerak, B ni qurish uchun esa A β bu hal qilib bo'lmaydigan tovuq-tuxum muammosi. Yagona yechim β bir bog'liqlikni konstruktordan tashqariga (setter yoki property injeksiyaga) chiqarish, shunda A "yarim tayyor" holatda yaratilib, keyin to'ldiriladi.
<?php
declare(strict_types=1);
// A B ga konstruktorda bog'liq; B esa A ga SETTER orqali (aylanmani uzadi)
final class ServiceA
{
public function __construct(public ServiceB $b) {}
public function nom(): string { return 'A(' . $this->b->nom() . ')'; }
}
final class ServiceB
{
private ?ServiceA $a = null;
public function setA(ServiceA $a): void { $this->a = $a; }
public function nom(): string { return 'B'; }
public function hasA(): bool { return $this->a !== null; }
}
final class SetterContainer
{
private array $instances = [];
public function get(string $id): object
{
if (isset($this->instances[$id])) {
return $this->instances[$id];
}
// ServiceB ni AVVAL (A'siz) yaratamiz va DARHOL keshlaymiz,
// shunda ServiceA uni konstruktorda olganda aynan shu nusxa keladi.
if ($id === ServiceB::class) {
$b = new ServiceB();
$this->instances[ServiceB::class] = $b; // keshga qo'yamiz
return $b;
}
if ($id === ServiceA::class) {
$b = $this->get(ServiceB::class); // tayyor (yoki yangi) B
$a = new ServiceA($b);
$this->instances[ServiceA::class] = $a;
$b->setA($a); // SETTER bilan aylanmani yopamiz
return $a;
}
throw new \RuntimeException("Noma'lum: {$id}");
}
}
$c = new SetterContainer();
$a = $c->get(ServiceA::class);
$b = $c->get(ServiceB::class);
assert($a instanceof ServiceA);
assert($b->hasA() === true); // B ga A in'ektsiya qilindi
assert($a->b === $b); // bir xil B nusxasi
assert($b === $c->get(ServiceB::class)); // keshlangan
echo "Setter bilan aylanma yechildi: {$a->nom()}, B.hasA=" . var_export($b->hasA(), true) . "\n";
Kalit g'oyalar:
- Erta keshlash: ServiceB ni yaratgach, uni ServiceA qurishidan oldin keshga qo'yamiz. Shunda ServiceA konstruktorida olingan B va keyin setterga uzatilgan A bir-biriga to'g'ri "tugun" hosil qiladi.
- Setter injeksiya: aylanmaning bitta yo'nalishini konstruktordan setterga ko'chirish β bu aylanmani "kechiktirilgan" (lazy) bog'lashga aylantiradi.
- Eng yaxshi yechim β qochish: real loyihada aylanma bog'liqlik ko'pincha dizayn xatosi signali. Uni setter bilan "yamashdan" oldin, sinflarni qayta loyihalashtirish (umumiy mantiqni uchinchi sinfga chiqarish) ko'pincha to'g'riroq yo'l.
β¬ οΈ Oldingi: 12 β PSR-15 middleware pipeline Β· π README Β· Keyingi: 14 β Routing va front controller β‘οΈ