[{"data":1,"prerenderedAt":6611},["ShallowReactive",2],{"article-pl-bridge-pattern":3,"articles-pl-sidebar":972},{"id":4,"title":5,"articleId":6,"body":7,"category":61,"codeLang":61,"date":947,"deploys":68,"description":948,"excerpt":949,"extension":950,"lang":951,"meta":952,"navigation":101,"path":953,"pos":954,"readMin":116,"related":958,"seo":961,"service":962,"stem":963,"tags":964,"version":970,"__hash__":971},"articles_pl\u002Fpl\u002Farticles\u002Fbridge-pattern.md","Wzorzec Bridge: oddzielenie tego, co wysyłasz, od tego, jak to wysyłasz","bridge-pattern",{"type":8,"value":9,"toc":939},"minimark",[10,23,26,31,34,37,47,50,54,57,319,503,506,510,513,579,586,656,660,663,731,742,753,757,760,914,917,921,935],[11,12,13,14,18,19,22],"p",{},"Wzorzec Bridge jest wyjaśniany w większości podręczników na przykładzie kształtów i API renderowania. Hierarchia ",[15,16,17],"code",{},"Shape"," i hierarchia ",[15,20,21],{},"Renderer",", połączone mostem. Przykłady są poprawne i całkowicie bezużyteczne jako wskazówka projektowa, bo żaden produkcyjny system nikogo nie rysuje kształtów.",[11,24,25],{},"Wzorzec rozwiązuje konkretny, rozpoznawalny problem: masz dwa niezależnie zmienne wymiary i musisz je łączyć bez tworzenia klasy dla każdej kombinacji. Najczęściej spotykałem ten problem w systemach notyfikacji i na tym przykładzie opieram ten artykuł.",[27,28,30],"h2",{"id":29},"problem-m-n-klas","Problem: M × N klas",[11,32,33],{},"Masz system notyfikacji. Wysyła notyfikacje. Wysyła je przez trzy kanały: Email, SMS i Slack. Wysyła cztery typy notyfikacji: PaymentConfirmation, LowInventoryAlert, AccountSuspended i WeeklyReport.",[11,35,36],{},"Bez żadnej struktury:",[38,39,44],"pre",{"className":40,"code":42,"language":43},[41],"language-text","PaymentConfirmationEmail\nPaymentConfirmationSMS\nPaymentConfirmationSlack\nLowInventoryAlertEmail\nLowInventoryAlertSMS\nLowInventoryAlertSlack\nAccountSuspendedEmail\nAccountSuspendedSMS\nAccountSuspendedSlack\nWeeklyReportEmail\nWeeklyReportSMS\nWeeklyReportSlack\n","text",[15,45,42],{"__ignoreMap":46},"",[11,48,49],{},"12 klas. Dodaj czwarty kanał (push notifications): 16 klas. Dodaj piąty typ notyfikacji (PasswordReset): 20 klas. Struktura skaluje się jako M × N, a każda klasa zawiera logikę jednego typu notyfikacji sformatowanego dla jednego kanału.",[27,51,53],{"id":52},"wyekstrahowany-bridge","Wyekstrahowany Bridge",[11,55,56],{},"Bridge oddziela dwie hierarchie i łączy je przez kompozycję zamiast dziedziczenia:",[38,58,62],{"className":59,"code":60,"language":61,"meta":46,"style":46},"language-php shiki shiki-themes github-light github-dark","\u002F\u002F The \"implementor\" side: how to send\ninterface NotificationChannel\n{\n    public function send(string $recipient, string $subject, string $body): void;\n}\n\nfinal class EmailChannel implements NotificationChannel\n{\n    public function __construct(private readonly Mailer $mailer) {}\n\n    public function send(string $recipient, string $subject, string $body): void\n    {\n        $this->mailer->send(\n            to:      $recipient,\n            subject: $subject,\n            html:    $body,\n        );\n    }\n}\n\nfinal class SlackChannel implements NotificationChannel\n{\n    public function __construct(private readonly SlackClient $slack) {}\n\n    public function send(string $recipient, string $subject, string $body): void\n    {\n        \u002F\u002F Slack does not have a subject — prepend it to the body\n        $this->slack->postMessage(\n            channel: $recipient,\n            text:    \"*{$subject}*\\n{$body}\",\n        );\n    }\n}\n\nfinal class SmsChannel implements NotificationChannel\n{\n    public function __construct(private readonly SmsProvider $sms) {}\n\n    public function send(string $recipient, string $subject, string $body): void\n    {\n        \u002F\u002F SMS is length-constrained — truncate body to 160 chars\n        $text = \"{$subject}: \" . substr(strip_tags($body), 0, 140);\n        $this->sms->send(phone: $recipient, message: $text);\n    }\n}\n","php",[15,63,64,72,78,84,90,96,103,109,114,120,125,131,137,143,149,155,161,167,173,178,183,189,194,200,205,210,215,221,227,233,239,244,249,254,259,265,270,276,281,286,291,297,303,309,314],{"__ignoreMap":46},[65,66,69],"span",{"class":67,"line":68},"line",1,[65,70,71],{},"\u002F\u002F The \"implementor\" side: how to send\n",[65,73,75],{"class":67,"line":74},2,[65,76,77],{},"interface NotificationChannel\n",[65,79,81],{"class":67,"line":80},3,[65,82,83],{},"{\n",[65,85,87],{"class":67,"line":86},4,[65,88,89],{},"    public function send(string $recipient, string $subject, string $body): void;\n",[65,91,93],{"class":67,"line":92},5,[65,94,95],{},"}\n",[65,97,99],{"class":67,"line":98},6,[65,100,102],{"emptyLinePlaceholder":101},true,"\n",[65,104,106],{"class":67,"line":105},7,[65,107,108],{},"final class EmailChannel implements NotificationChannel\n",[65,110,112],{"class":67,"line":111},8,[65,113,83],{},[65,115,117],{"class":67,"line":116},9,[65,118,119],{},"    public function __construct(private readonly Mailer $mailer) {}\n",[65,121,123],{"class":67,"line":122},10,[65,124,102],{"emptyLinePlaceholder":101},[65,126,128],{"class":67,"line":127},11,[65,129,130],{},"    public function send(string $recipient, string $subject, string $body): void\n",[65,132,134],{"class":67,"line":133},12,[65,135,136],{},"    {\n",[65,138,140],{"class":67,"line":139},13,[65,141,142],{},"        $this->mailer->send(\n",[65,144,146],{"class":67,"line":145},14,[65,147,148],{},"            to:      $recipient,\n",[65,150,152],{"class":67,"line":151},15,[65,153,154],{},"            subject: $subject,\n",[65,156,158],{"class":67,"line":157},16,[65,159,160],{},"            html:    $body,\n",[65,162,164],{"class":67,"line":163},17,[65,165,166],{},"        );\n",[65,168,170],{"class":67,"line":169},18,[65,171,172],{},"    }\n",[65,174,176],{"class":67,"line":175},19,[65,177,95],{},[65,179,181],{"class":67,"line":180},20,[65,182,102],{"emptyLinePlaceholder":101},[65,184,186],{"class":67,"line":185},21,[65,187,188],{},"final class SlackChannel implements NotificationChannel\n",[65,190,192],{"class":67,"line":191},22,[65,193,83],{},[65,195,197],{"class":67,"line":196},23,[65,198,199],{},"    public function __construct(private readonly SlackClient $slack) {}\n",[65,201,203],{"class":67,"line":202},24,[65,204,102],{"emptyLinePlaceholder":101},[65,206,208],{"class":67,"line":207},25,[65,209,130],{},[65,211,213],{"class":67,"line":212},26,[65,214,136],{},[65,216,218],{"class":67,"line":217},27,[65,219,220],{},"        \u002F\u002F Slack does not have a subject — prepend it to the body\n",[65,222,224],{"class":67,"line":223},28,[65,225,226],{},"        $this->slack->postMessage(\n",[65,228,230],{"class":67,"line":229},29,[65,231,232],{},"            channel: $recipient,\n",[65,234,236],{"class":67,"line":235},30,[65,237,238],{},"            text:    \"*{$subject}*\\n{$body}\",\n",[65,240,242],{"class":67,"line":241},31,[65,243,166],{},[65,245,247],{"class":67,"line":246},32,[65,248,172],{},[65,250,252],{"class":67,"line":251},33,[65,253,95],{},[65,255,257],{"class":67,"line":256},34,[65,258,102],{"emptyLinePlaceholder":101},[65,260,262],{"class":67,"line":261},35,[65,263,264],{},"final class SmsChannel implements NotificationChannel\n",[65,266,268],{"class":67,"line":267},36,[65,269,83],{},[65,271,273],{"class":67,"line":272},37,[65,274,275],{},"    public function __construct(private readonly SmsProvider $sms) {}\n",[65,277,279],{"class":67,"line":278},38,[65,280,102],{"emptyLinePlaceholder":101},[65,282,284],{"class":67,"line":283},39,[65,285,130],{},[65,287,289],{"class":67,"line":288},40,[65,290,136],{},[65,292,294],{"class":67,"line":293},41,[65,295,296],{},"        \u002F\u002F SMS is length-constrained — truncate body to 160 chars\n",[65,298,300],{"class":67,"line":299},42,[65,301,302],{},"        $text = \"{$subject}: \" . substr(strip_tags($body), 0, 140);\n",[65,304,306],{"class":67,"line":305},43,[65,307,308],{},"        $this->sms->send(phone: $recipient, message: $text);\n",[65,310,312],{"class":67,"line":311},44,[65,313,172],{},[65,315,317],{"class":67,"line":316},45,[65,318,95],{},[38,320,322],{"className":59,"code":321,"language":61,"meta":46,"style":46},"\u002F\u002F The \"abstraction\" side: what to send\nabstract class Notification\n{\n    public function __construct(\n        protected readonly NotificationChannel $channel,\n    ) {}\n\n    abstract public function send(string $recipient): void;\n}\n\nfinal class PaymentConfirmationNotification extends Notification\n{\n    public function __construct(\n        NotificationChannel $channel,\n        private readonly Payment $payment,\n    ) {\n        parent::__construct($channel);\n    }\n\n    public function send(string $recipient): void\n    {\n        $this->channel->send(\n            recipient: $recipient,\n            subject:   \"Payment confirmed — {$this->payment->reference}\",\n            body:      $this->renderBody(),\n        );\n    }\n\n    private function renderBody(): string\n    {\n        return sprintf(\n            \"Your payment of %s %s has been confirmed.\\nReference: %s\\nDate: %s\",\n            number_format($this->payment->amount \u002F 100, 2),\n            $this->payment->currency,\n            $this->payment->reference,\n            $this->payment->created_at->format('Y-m-d H:i'),\n        );\n    }\n}\n",[15,323,324,329,334,338,343,348,353,357,362,366,370,375,379,383,388,393,398,403,407,411,416,420,425,430,435,440,444,448,452,457,461,466,471,476,481,486,491,495,499],{"__ignoreMap":46},[65,325,326],{"class":67,"line":68},[65,327,328],{},"\u002F\u002F The \"abstraction\" side: what to send\n",[65,330,331],{"class":67,"line":74},[65,332,333],{},"abstract class Notification\n",[65,335,336],{"class":67,"line":80},[65,337,83],{},[65,339,340],{"class":67,"line":86},[65,341,342],{},"    public function __construct(\n",[65,344,345],{"class":67,"line":92},[65,346,347],{},"        protected readonly NotificationChannel $channel,\n",[65,349,350],{"class":67,"line":98},[65,351,352],{},"    ) {}\n",[65,354,355],{"class":67,"line":105},[65,356,102],{"emptyLinePlaceholder":101},[65,358,359],{"class":67,"line":111},[65,360,361],{},"    abstract public function send(string $recipient): void;\n",[65,363,364],{"class":67,"line":116},[65,365,95],{},[65,367,368],{"class":67,"line":122},[65,369,102],{"emptyLinePlaceholder":101},[65,371,372],{"class":67,"line":127},[65,373,374],{},"final class PaymentConfirmationNotification extends Notification\n",[65,376,377],{"class":67,"line":133},[65,378,83],{},[65,380,381],{"class":67,"line":139},[65,382,342],{},[65,384,385],{"class":67,"line":145},[65,386,387],{},"        NotificationChannel $channel,\n",[65,389,390],{"class":67,"line":151},[65,391,392],{},"        private readonly Payment $payment,\n",[65,394,395],{"class":67,"line":157},[65,396,397],{},"    ) {\n",[65,399,400],{"class":67,"line":163},[65,401,402],{},"        parent::__construct($channel);\n",[65,404,405],{"class":67,"line":169},[65,406,172],{},[65,408,409],{"class":67,"line":175},[65,410,102],{"emptyLinePlaceholder":101},[65,412,413],{"class":67,"line":180},[65,414,415],{},"    public function send(string $recipient): void\n",[65,417,418],{"class":67,"line":185},[65,419,136],{},[65,421,422],{"class":67,"line":191},[65,423,424],{},"        $this->channel->send(\n",[65,426,427],{"class":67,"line":196},[65,428,429],{},"            recipient: $recipient,\n",[65,431,432],{"class":67,"line":202},[65,433,434],{},"            subject:   \"Payment confirmed — {$this->payment->reference}\",\n",[65,436,437],{"class":67,"line":207},[65,438,439],{},"            body:      $this->renderBody(),\n",[65,441,442],{"class":67,"line":212},[65,443,166],{},[65,445,446],{"class":67,"line":217},[65,447,172],{},[65,449,450],{"class":67,"line":223},[65,451,102],{"emptyLinePlaceholder":101},[65,453,454],{"class":67,"line":229},[65,455,456],{},"    private function renderBody(): string\n",[65,458,459],{"class":67,"line":235},[65,460,136],{},[65,462,463],{"class":67,"line":241},[65,464,465],{},"        return sprintf(\n",[65,467,468],{"class":67,"line":246},[65,469,470],{},"            \"Your payment of %s %s has been confirmed.\\nReference: %s\\nDate: %s\",\n",[65,472,473],{"class":67,"line":251},[65,474,475],{},"            number_format($this->payment->amount \u002F 100, 2),\n",[65,477,478],{"class":67,"line":256},[65,479,480],{},"            $this->payment->currency,\n",[65,482,483],{"class":67,"line":261},[65,484,485],{},"            $this->payment->reference,\n",[65,487,488],{"class":67,"line":267},[65,489,490],{},"            $this->payment->created_at->format('Y-m-d H:i'),\n",[65,492,493],{"class":67,"line":272},[65,494,166],{},[65,496,497],{"class":67,"line":278},[65,498,172],{},[65,500,501],{"class":67,"line":283},[65,502,95],{},[11,504,505],{},"Teraz struktura to M + N: 4 klasy notyfikacji plus 3 klasy kanałów. Dodanie czwartego kanału (PushNotification) to jedna nowa klasa. Dodanie piątego typu notyfikacji (PasswordReset) to jedna nowa klasa. Nic innego się nie zmienia.",[27,507,509],{"id":508},"punkt-kompozycji","Punkt kompozycji",[11,511,512],{},"Bridge następuje przy konstrukcji: kompoyzujesz typ notyfikacji z kanałem:",[38,514,516],{"className":59,"code":515,"language":61,"meta":46,"style":46},"\u002F\u002F Wired by the application layer — typically a notification dispatcher\n$notification = new PaymentConfirmationNotification(\n    channel: new EmailChannel($mailer),\n    payment: $payment,\n);\n$notification->send(recipient: $user->email);\n\n\u002F\u002F Same notification type, different channel\n$notification = new PaymentConfirmationNotification(\n    channel: new SmsChannel($smsProvider),\n    payment: $payment,\n);\n$notification->send(recipient: $user->phone);\n",[15,517,518,523,528,533,538,543,548,552,557,561,566,570,574],{"__ignoreMap":46},[65,519,520],{"class":67,"line":68},[65,521,522],{},"\u002F\u002F Wired by the application layer — typically a notification dispatcher\n",[65,524,525],{"class":67,"line":74},[65,526,527],{},"$notification = new PaymentConfirmationNotification(\n",[65,529,530],{"class":67,"line":80},[65,531,532],{},"    channel: new EmailChannel($mailer),\n",[65,534,535],{"class":67,"line":86},[65,536,537],{},"    payment: $payment,\n",[65,539,540],{"class":67,"line":92},[65,541,542],{},");\n",[65,544,545],{"class":67,"line":98},[65,546,547],{},"$notification->send(recipient: $user->email);\n",[65,549,550],{"class":67,"line":105},[65,551,102],{"emptyLinePlaceholder":101},[65,553,554],{"class":67,"line":111},[65,555,556],{},"\u002F\u002F Same notification type, different channel\n",[65,558,559],{"class":67,"line":116},[65,560,527],{},[65,562,563],{"class":67,"line":122},[65,564,565],{},"    channel: new SmsChannel($smsProvider),\n",[65,567,568],{"class":67,"line":127},[65,569,537],{},[65,571,572],{"class":67,"line":133},[65,573,542],{},[65,575,576],{"class":67,"line":139},[65,577,578],{},"$notification->send(recipient: $user->phone);\n",[11,580,581,582,585],{},"W praktyce tą kompozycją zajmuje się ",[15,583,584],{},"NotificationDispatcher",", który sprawdza, które kanały dany użytkownik włączył:",[38,587,589],{"className":59,"code":588,"language":61,"meta":46,"style":46},"final class NotificationDispatcher\n{\n    \u002F** @param NotificationChannel[] $channels *\u002F\n    public function __construct(private readonly array $channels) {}\n\n    public function dispatch(string $notificationType, array $payload, User $user): void\n    {\n        foreach ($user->enabledChannels() as $channelName) {\n            $channel      = $this->channels[$channelName] ?? throw new UnknownChannelException($channelName);\n            $notification = $this->buildNotification($notificationType, $channel, $payload);\n            $notification->send($user->contactFor($channelName));\n        }\n    }\n}\n",[15,590,591,596,600,605,610,614,619,623,628,633,638,643,648,652],{"__ignoreMap":46},[65,592,593],{"class":67,"line":68},[65,594,595],{},"final class NotificationDispatcher\n",[65,597,598],{"class":67,"line":74},[65,599,83],{},[65,601,602],{"class":67,"line":80},[65,603,604],{},"    \u002F** @param NotificationChannel[] $channels *\u002F\n",[65,606,607],{"class":67,"line":86},[65,608,609],{},"    public function __construct(private readonly array $channels) {}\n",[65,611,612],{"class":67,"line":92},[65,613,102],{"emptyLinePlaceholder":101},[65,615,616],{"class":67,"line":98},[65,617,618],{},"    public function dispatch(string $notificationType, array $payload, User $user): void\n",[65,620,621],{"class":67,"line":105},[65,622,136],{},[65,624,625],{"class":67,"line":111},[65,626,627],{},"        foreach ($user->enabledChannels() as $channelName) {\n",[65,629,630],{"class":67,"line":116},[65,631,632],{},"            $channel      = $this->channels[$channelName] ?? throw new UnknownChannelException($channelName);\n",[65,634,635],{"class":67,"line":122},[65,636,637],{},"            $notification = $this->buildNotification($notificationType, $channel, $payload);\n",[65,639,640],{"class":67,"line":127},[65,641,642],{},"            $notification->send($user->contactFor($channelName));\n",[65,644,645],{"class":67,"line":133},[65,646,647],{},"        }\n",[65,649,650],{"class":67,"line":139},[65,651,172],{},[65,653,654],{"class":67,"line":145},[65,655,95],{},[27,657,659],{"id":658},"gdzie-abstrakcja-zaczyna-przeciekać","Gdzie abstrakcja zaczyna przeciekać",[11,661,662],{},"Bridge działa czysto wtedy, gdy dwa wymiary są naprawdę niezależne. Przestają być niezależne, gdy typ notyfikacji ma treść, która ma sens tylko w jednym kanale, szczegółowy raport HTML dla emaila, bez znaczącego odpowiednika w SMS.",[38,664,666],{"className":59,"code":665,"language":61,"meta":46,"style":46},"\u002F\u002F This is the first sign of a leaking abstraction\nfinal class WeeklyReportNotification extends Notification\n{\n    public function send(string $recipient): void\n    {\n        if ($this->channel instanceof SmsChannel) {\n            \u002F\u002F What do we send? A summary? A link? Nothing?\n            $this->channel->send($recipient, 'Weekly report', 'See your email for the full report.');\n            return;\n        }\n\n        $this->channel->send($recipient, 'Weekly Report', $this->renderFullHtmlReport());\n    }\n}\n",[15,667,668,673,678,682,686,690,695,700,705,710,714,718,723,727],{"__ignoreMap":46},[65,669,670],{"class":67,"line":68},[65,671,672],{},"\u002F\u002F This is the first sign of a leaking abstraction\n",[65,674,675],{"class":67,"line":74},[65,676,677],{},"final class WeeklyReportNotification extends Notification\n",[65,679,680],{"class":67,"line":80},[65,681,83],{},[65,683,684],{"class":67,"line":86},[65,685,415],{},[65,687,688],{"class":67,"line":92},[65,689,136],{},[65,691,692],{"class":67,"line":98},[65,693,694],{},"        if ($this->channel instanceof SmsChannel) {\n",[65,696,697],{"class":67,"line":105},[65,698,699],{},"            \u002F\u002F What do we send? A summary? A link? Nothing?\n",[65,701,702],{"class":67,"line":111},[65,703,704],{},"            $this->channel->send($recipient, 'Weekly report', 'See your email for the full report.');\n",[65,706,707],{"class":67,"line":116},[65,708,709],{},"            return;\n",[65,711,712],{"class":67,"line":122},[65,713,647],{},[65,715,716],{"class":67,"line":127},[65,717,102],{"emptyLinePlaceholder":101},[65,719,720],{"class":67,"line":133},[65,721,722],{},"        $this->channel->send($recipient, 'Weekly Report', $this->renderFullHtmlReport());\n",[65,724,725],{"class":67,"line":139},[65,726,172],{},[65,728,729],{"class":67,"line":145},[65,730,95],{},[11,732,733,734,737,738,741],{},"Sprawdzenie ",[15,735,736],{},"instanceof"," to rozpadający się wzorzec Bridge. Notyfikacja ",[15,739,740],{},"WeeklyReport"," nie jest niezależna od kanału, ma inne zachowanie dla różnych kanałów, co oznacza, że abstrakcja już nie trzyma.",[11,743,744,745,748,749,752],{},"Uczciwa naprawa to niezmuszanie wzorca. Niektóre notyfikacje mają implementacje specyficzne dla kanału. Utwórz ",[15,746,747],{},"WeeklyReportEmailNotification"," i ",[15,750,751],{},"WeeklyReportSmsSummaryNotification"," jako oddzielne klasy. Wymuszanie M + N tam, gdzie problem jest naprawdę M × N, produkuje gorszy kod niż po prostu napisanie klas M × N w czytelny sposób.",[27,754,756],{"id":755},"testowanie-wzorca","Testowanie wzorca",[11,758,759],{},"Wartość Bridge dla testowania polega na tym, że każdy wymiar jest testowalny niezależnie:",[38,761,763],{"className":59,"code":762,"language":61,"meta":46,"style":46},"\u002F\u002F Test the channel independently\nclass EmailChannelTest extends TestCase\n{\n    public function testSendsDelegatesCorrectlyToMailer(): void\n    {\n        $mailer = $this->createMock(Mailer::class);\n        $mailer->expects($this->once())\n               ->method('send')\n               ->with(to: 'alice@example.com', subject: 'Test', html: '\u003Cp>Body\u003C\u002Fp>');\n\n        (new EmailChannel($mailer))->send('alice@example.com', 'Test', '\u003Cp>Body\u003C\u002Fp>');\n    }\n}\n\n\u002F\u002F Test the notification independently using a mock channel\nclass PaymentConfirmationNotificationTest extends TestCase\n{\n    public function testSendsCorrectSubjectAndBody(): void\n    {\n        $channel = $this->createMock(NotificationChannel::class);\n        $channel->expects($this->once())\n                ->method('send')\n                ->with(\n                    recipient: 'alice@example.com',\n                    subject:   $this->stringContains('PAY-2024-001'),\n                    body:      $this->stringContains('100.00'),\n                );\n\n        $payment = new Payment(id: 1, reference: 'PAY-2024-001', amount: 10000, currency: 'PLN', ...);\n        (new PaymentConfirmationNotification($channel, $payment))->send('alice@example.com');\n    }\n}\n",[15,764,765,770,775,779,784,788,793,798,803,808,812,817,821,825,829,834,839,843,848,852,857,862,867,872,877,882,887,892,896,901,906,910],{"__ignoreMap":46},[65,766,767],{"class":67,"line":68},[65,768,769],{},"\u002F\u002F Test the channel independently\n",[65,771,772],{"class":67,"line":74},[65,773,774],{},"class EmailChannelTest extends TestCase\n",[65,776,777],{"class":67,"line":80},[65,778,83],{},[65,780,781],{"class":67,"line":86},[65,782,783],{},"    public function testSendsDelegatesCorrectlyToMailer(): void\n",[65,785,786],{"class":67,"line":92},[65,787,136],{},[65,789,790],{"class":67,"line":98},[65,791,792],{},"        $mailer = $this->createMock(Mailer::class);\n",[65,794,795],{"class":67,"line":105},[65,796,797],{},"        $mailer->expects($this->once())\n",[65,799,800],{"class":67,"line":111},[65,801,802],{},"               ->method('send')\n",[65,804,805],{"class":67,"line":116},[65,806,807],{},"               ->with(to: 'alice@example.com', subject: 'Test', html: '\u003Cp>Body\u003C\u002Fp>');\n",[65,809,810],{"class":67,"line":122},[65,811,102],{"emptyLinePlaceholder":101},[65,813,814],{"class":67,"line":127},[65,815,816],{},"        (new EmailChannel($mailer))->send('alice@example.com', 'Test', '\u003Cp>Body\u003C\u002Fp>');\n",[65,818,819],{"class":67,"line":133},[65,820,172],{},[65,822,823],{"class":67,"line":139},[65,824,95],{},[65,826,827],{"class":67,"line":145},[65,828,102],{"emptyLinePlaceholder":101},[65,830,831],{"class":67,"line":151},[65,832,833],{},"\u002F\u002F Test the notification independently using a mock channel\n",[65,835,836],{"class":67,"line":157},[65,837,838],{},"class PaymentConfirmationNotificationTest extends TestCase\n",[65,840,841],{"class":67,"line":163},[65,842,83],{},[65,844,845],{"class":67,"line":169},[65,846,847],{},"    public function testSendsCorrectSubjectAndBody(): void\n",[65,849,850],{"class":67,"line":175},[65,851,136],{},[65,853,854],{"class":67,"line":180},[65,855,856],{},"        $channel = $this->createMock(NotificationChannel::class);\n",[65,858,859],{"class":67,"line":185},[65,860,861],{},"        $channel->expects($this->once())\n",[65,863,864],{"class":67,"line":191},[65,865,866],{},"                ->method('send')\n",[65,868,869],{"class":67,"line":196},[65,870,871],{},"                ->with(\n",[65,873,874],{"class":67,"line":202},[65,875,876],{},"                    recipient: 'alice@example.com',\n",[65,878,879],{"class":67,"line":207},[65,880,881],{},"                    subject:   $this->stringContains('PAY-2024-001'),\n",[65,883,884],{"class":67,"line":212},[65,885,886],{},"                    body:      $this->stringContains('100.00'),\n",[65,888,889],{"class":67,"line":217},[65,890,891],{},"                );\n",[65,893,894],{"class":67,"line":223},[65,895,102],{"emptyLinePlaceholder":101},[65,897,898],{"class":67,"line":229},[65,899,900],{},"        $payment = new Payment(id: 1, reference: 'PAY-2024-001', amount: 10000, currency: 'PLN', ...);\n",[65,902,903],{"class":67,"line":235},[65,904,905],{},"        (new PaymentConfirmationNotification($channel, $payment))->send('alice@example.com');\n",[65,907,908],{"class":67,"line":241},[65,909,172],{},[65,911,912],{"class":67,"line":246},[65,913,95],{},[11,915,916],{},"Dwanaście kombinacji notyfikacja–kanał, zero testów uruchamiających prawdziwy mailer ani Slack API. Każdy test jest szybki, izolowany i pokrywa dokładnie jedną jednostkę zachowania.",[27,918,920],{"id":919},"kiedy-sięgać-po-bridge","Kiedy sięgać po Bridge",[11,922,923,924,927,928,927,931,934],{},"Jest jeden wyraźny sygnał: zaraz napiszesz klasę, której nazwa to dwa pojęcia złączone razem, ",[15,925,926],{},"PaymentEmailNotification",", ",[15,929,930],{},"PdfReportExporter",[15,932,933],{},"CsvLogFormatter",". Nazwa sama mówi ci, że dwa niezależne wymiary zostały połączone w jedną klasę. To jest moment, żeby zapytać, czy można je rozdzielić i skomponować zamiast tego.",[936,937,938],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":46,"searchDepth":74,"depth":74,"links":940},[941,942,943,944,945,946],{"id":29,"depth":74,"text":30},{"id":52,"depth":74,"text":53},{"id":508,"depth":74,"text":509},{"id":658,"depth":74,"text":659},{"id":755,"depth":74,"text":756},{"id":919,"depth":74,"text":920},"2023-10-19","Wzorzec Bridge jest wyjaśniany w większości podręczników na przykładzie kształtów i API renderowania. Hierarchia Shape i hierarchia Renderer, połączone mostem. Przykłady są poprawne i całkowicie bezużyteczne jako wskazówka projektowa, bo żaden produkcyjny system nikogo nie rysuje kształtów.",null,"md","pl",{},"\u002Fpl\u002Farticles\u002Fbridge-pattern",{"x":955,"y":956,"depth":957,"size":950},0.86,0.18,0.9,[959,960],"factory-method","design-patterns-production",{"title":5,"description":948},"notification-hub","pl\u002Farticles\u002Fbridge-pattern",[61,965,966,967,968,969],"design-patterns","bridge","architecture","notifications","abstraction","v1.0.0","rP6ywP5UVmnJjpKSZKyhuX9o30lrWk-yu_5GC_aBvuw",[973,1166,2078,2815,3252,3840,4453,4590,5017,5201,6019],{"id":974,"title":975,"articleId":976,"body":977,"category":1144,"codeLang":1010,"date":1145,"deploys":246,"description":981,"excerpt":949,"extension":950,"lang":951,"meta":1146,"navigation":101,"path":1147,"pos":1148,"readMin":169,"related":1153,"seo":1156,"service":1157,"stem":1158,"tags":1159,"version":1164,"__hash__":1165},"articles_pl\u002Fpl\u002Farticles\u002Fagent-graphs.md","Budowanie agentów AI w produkcji ze stanowym grafem orkiestracji","agent-graphs",{"type":8,"value":978,"toc":1137},[979,982,985,988,992,995,1002,1006,1095,1099,1110,1113,1117,1124,1128,1135],[11,980,981],{},"Gdy agent ma więcej niż dwa narzędzia, pętla ReAct przestaje być architekturą i zaczyna być zobowiązaniem. Latencja się kumuluje, tryby awarii mnożą, i nie ma uczciwego miejsca, gdzie można narysować granicę dla retry.",[11,983,984],{},"Zmiana, którą wprowadziliśmy (i którą polecam każdemu zespołowi) to odejście od modelowania agenta jako pętli na rzecz modelowania go jako maszyny stanów, gdzie każde przejście ma nazwę, każdy stan można checkpointować, a każde wywołanie narzędzia jest węzłem, nie efektem ubocznym.",[11,986,987],{},"W terminologii LangGraph: graf jest kontraktem. Model jest uczestnikiem grafu, nie jego właścicielem. To pojedyncze odwrócenie pozwala robić wszystko, czego agent w produkcji faktycznie potrzebuje, pauzować, wznawiać, fan-outować, przekazywać do człowieka, odtwarzać wczorajszą sesję w regression suite.",[27,989,991],{"id":990},"dlaczego-pętle-zawodzą-na-skali","Dlaczego pętle zawodzą na skali",[11,993,994],{},"Kanoniczna pętla ReAct jest elegancka w demach: obserwuj, myśl, działaj, powtarzaj. W produkcji akumuluje tryby awarii, które nasilają się z każdym kolejnym narzędziem.",[11,996,997,998,1001],{},"Gdy wywołanie narzędzia się nie powiedzie, pętla nie ma naturalnego miejsca na wyznaczenie granicy. Dodajesz guard ",[15,999,1000],{},"max_steps",". Potem ktoś z zespołu go podwyższy \"tylko ten jeden raz\". Trzy miesiące później twoje p99 latency to 45 sekund i nikt nie wie dlaczego, rzetelne testy na produkcji mają swoje ograniczenia. Gdy klient zgłasza buga, chcesz odtworzyć dokładną sekwencję decyzji, a pętla bez nazwanych checkpointów daje ci co najwyżej log. Graf daje ci wznawialny snapshot. Pętla nie potrafi też zatrzymać się i czekać na człowieka: graf może przerwać w dowolnym nazwanym węźle i czekać w nieskończoność na input, a potem wznowić dokładnie z tego miejsca.",[27,1003,1005],{"id":1004},"topologia-grafu","Topologia grafu",[38,1007,1011],{"className":1008,"code":1009,"language":1010,"meta":46,"style":46},"language-python shiki shiki-themes github-light github-dark","# jedyna pętla należy do runtim'u. agent jest grafem.\ngraph = StateGraph(AgentState)\n\ngraph.add_node(\"plan\",     planner)\ngraph.add_node(\"retrieve\", retriever)\ngraph.add_node(\"act\",      tool_runner)\ngraph.add_node(\"reflect\",  critic)\n\ngraph.add_edge(START, \"plan\")\ngraph.add_conditional_edges(\"plan\",\n    route=lambda s: \"retrieve\" if s.needs_context else \"act\")\ngraph.add_edge(\"retrieve\", \"act\")\ngraph.add_conditional_edges(\"act\",\n    route=lambda s: END if s.done else \"reflect\")\ngraph.add_edge(\"reflect\", \"plan\")\n\napp = graph.compile(checkpointer=PostgresSaver(dsn))\n","python",[15,1012,1013,1018,1023,1027,1032,1037,1042,1047,1051,1056,1061,1066,1071,1076,1081,1086,1090],{"__ignoreMap":46},[65,1014,1015],{"class":67,"line":68},[65,1016,1017],{},"# jedyna pętla należy do runtim'u. agent jest grafem.\n",[65,1019,1020],{"class":67,"line":74},[65,1021,1022],{},"graph = StateGraph(AgentState)\n",[65,1024,1025],{"class":67,"line":80},[65,1026,102],{"emptyLinePlaceholder":101},[65,1028,1029],{"class":67,"line":86},[65,1030,1031],{},"graph.add_node(\"plan\",     planner)\n",[65,1033,1034],{"class":67,"line":92},[65,1035,1036],{},"graph.add_node(\"retrieve\", retriever)\n",[65,1038,1039],{"class":67,"line":98},[65,1040,1041],{},"graph.add_node(\"act\",      tool_runner)\n",[65,1043,1044],{"class":67,"line":105},[65,1045,1046],{},"graph.add_node(\"reflect\",  critic)\n",[65,1048,1049],{"class":67,"line":111},[65,1050,102],{"emptyLinePlaceholder":101},[65,1052,1053],{"class":67,"line":116},[65,1054,1055],{},"graph.add_edge(START, \"plan\")\n",[65,1057,1058],{"class":67,"line":122},[65,1059,1060],{},"graph.add_conditional_edges(\"plan\",\n",[65,1062,1063],{"class":67,"line":127},[65,1064,1065],{},"    route=lambda s: \"retrieve\" if s.needs_context else \"act\")\n",[65,1067,1068],{"class":67,"line":133},[65,1069,1070],{},"graph.add_edge(\"retrieve\", \"act\")\n",[65,1072,1073],{"class":67,"line":139},[65,1074,1075],{},"graph.add_conditional_edges(\"act\",\n",[65,1077,1078],{"class":67,"line":145},[65,1079,1080],{},"    route=lambda s: END if s.done else \"reflect\")\n",[65,1082,1083],{"class":67,"line":151},[65,1084,1085],{},"graph.add_edge(\"reflect\", \"plan\")\n",[65,1087,1088],{"class":67,"line":157},[65,1089,102],{"emptyLinePlaceholder":101},[65,1091,1092],{"class":67,"line":163},[65,1093,1094],{},"app = graph.compile(checkpointer=PostgresSaver(dsn))\n",[27,1096,1098],{"id":1097},"checkpointing-z-postgres","Checkpointing z Postgres",[11,1100,1101,1102,1105,1106,1109],{},"Używamy ",[15,1103,1104],{},"PostgresSaver"," jako checkpointera. Każde przejście zapisuje wiersz do ",[15,1107,1108],{},"agent_checkpoints(thread_id, step, state_json, created_at)",". Po crashu runner podejmuje od ostatniego ukończonego kroku bez powtarzania czegokolwiek przed nim. Każdy pośredni stan można inspekcjonować lub odtworzyć z zamockowanymi narzędziami do testów regresyjnych, a każdy wiersz kroku zawiera liczniki tokenów, więc atrybucja kosztów jest dokładna zamiast Chłopskiego Rozumu as a Service na koniec miesiąca.",[11,1111,1112],{},"Zapis do Postgres dodaje ~3ms na krok. Dla agenta z 20 krokami to 60ms. Akceptowalne.",[27,1114,1116],{"id":1115},"czego-nie-można-checkpointować","Czego nie można checkpointować",[11,1118,1119,1120,1123],{},"Streamingowych outputów narzędzi. Jeśli twoje narzędzie streamuje dane do użytkownika w trakcie grafu, nie możesz bezpiecznie powtórzyć tego przejścia bez ponownego wyzwolenia streamu. Rozwiązaliśmy to oznaczając węzły streamingowe jako ",[15,1121,1122],{},"non_resumable"," i uruchamiając je od nowa przy każdym replayu.",[27,1125,1127],{"id":1126},"obserwowalność","Obserwowalność",[11,1129,1130,1131,1134],{},"Każde wykonanie grafu emituje ustrukturyzowane zdarzenie per krok: ",[15,1132,1133],{},"{thread_id, step_name, input_tokens, output_tokens, latency_ms, tool_calls, error}",". Wysyłamy je do time-series store i budujemy dashboardy per agent per tydzień. Gdy nowe narzędzie spowalnia medianę czasu kroku, widzimy to w jeden dzień, nie w jeden sprint.",[936,1136,938],{},{"title":46,"searchDepth":74,"depth":74,"links":1138},[1139,1140,1141,1142,1143],{"id":990,"depth":74,"text":991},{"id":1004,"depth":74,"text":1005},{"id":1097,"depth":74,"text":1098},{"id":1115,"depth":74,"text":1116},{"id":1126,"depth":74,"text":1127},"agents","2026-05-09",{},"\u002Fpl\u002Farticles\u002Fagent-graphs",{"x":1149,"y":1150,"depth":1151,"size":1152},0.66,0.27,1.5,"xl",[1154,1155],"microservice-cost","postgres-edge",{"title":975,"description":981},"orchestrator-graphs","pl\u002Farticles\u002Fagent-graphs",[1160,1161,1162,1163],"ai-agents","langgraph","state-machines","observability","v0.4.7","oosMwaZ0Pcx5kfnw8W9HfTLBPZoL7sGlsbdhjTp1q7c",{"id":1167,"title":1168,"articleId":1169,"body":1170,"category":2058,"codeLang":1196,"date":2059,"deploys":68,"description":2060,"excerpt":949,"extension":950,"lang":951,"meta":2061,"navigation":101,"path":2062,"pos":2063,"readMin":139,"related":2066,"seo":2067,"service":2068,"stem":2069,"tags":2070,"version":970,"__hash__":2077},"articles_pl\u002Fpl\u002Farticles\u002Fansible-production.md","Ansible poza tutorialem: idempotencja, wykrywanie dryftu i playbook, który uratował incydent o 3 w nocy","ansible-production",{"type":8,"value":1171,"toc":2051},[1172,1179,1182,1186,1189,1192,1261,1276,1279,1421,1424,1428,1431,1434,1818,1827,1831,1838,1920,1929,1933,1936,1994,2011,2015,2048],[11,1173,1174,1175,1178],{},"Demo playbook instaluje nginx i go uruchamia. Działa raz na czystej VM i wszyscy na demie kiwają głowami. To, czego nikt nie demonstruje, to uruchomienie tego samego playbooka sześć miesięcy później na serwerze, gdzie inżynier ręcznie edytował ",[15,1176,1177],{},"\u002Fetc\u002Fnginx\u002Fnginx.conf"," żeby tymczasowo naprawić problem produkcyjny i potem zapomniał to udokumentować. Albo po tym, jak pakiet nginx został zaktualizowany przez niezauważony apt cron job. Albo na serwerze, który nigdy nie był właściwie skonwergowany, bo ktoś anulował playbook w połowie.",[11,1180,1181],{},"Produkcyjny Ansible to nie uruchamianie playbooków. To niezawodna konwergencja infrastruktury do znanych stanów, w tym infrastruktury, która odpłynęła od tego, co Ansible ostatnio skonfigurował.",[27,1183,1185],{"id":1184},"idempotencja-to-kontrakt-nie-feature","Idempotencja to kontrakt, nie feature",[11,1187,1188],{},"Moduły Ansible są dokumentowane jako idempotentne i większość taka jest. Ale \"idempotentny\" w Ansible oznacza \"uruchomienie tego modułu dwa razy z tymi samymi argumentami daje ten sam wynik\", nie oznacza \"ten moduł jest bezpieczny do uruchomienia na systemie w nieznanym stanie.\"",[11,1190,1191],{},"Rozważ popularny wzorzec, który psuje się pod dryftem:",[38,1193,1197],{"className":1194,"code":1195,"language":1196,"meta":46,"style":46},"language-yaml shiki shiki-themes github-light github-dark","# To wygląda OK. Nie jest OK jeśli serwis był ręcznie wyłączony.\n- name: Ensure application service is running\n  ansible.builtin.service:\n    name: myapp\n    state: started\n    enabled: true\n","yaml",[15,1198,1199,1205,1222,1230,1240,1250],{"__ignoreMap":46},[65,1200,1201],{"class":67,"line":68},[65,1202,1204],{"class":1203},"sJ8bj","# To wygląda OK. Nie jest OK jeśli serwis był ręcznie wyłączony.\n",[65,1206,1207,1211,1215,1218],{"class":67,"line":74},[65,1208,1210],{"class":1209},"sVt8B","- ",[65,1212,1214],{"class":1213},"s9eBZ","name",[65,1216,1217],{"class":1209},": ",[65,1219,1221],{"class":1220},"sZZnC","Ensure application service is running\n",[65,1223,1224,1227],{"class":67,"line":80},[65,1225,1226],{"class":1213},"  ansible.builtin.service",[65,1228,1229],{"class":1209},":\n",[65,1231,1232,1235,1237],{"class":67,"line":86},[65,1233,1234],{"class":1213},"    name",[65,1236,1217],{"class":1209},[65,1238,1239],{"class":1220},"myapp\n",[65,1241,1242,1245,1247],{"class":67,"line":92},[65,1243,1244],{"class":1213},"    state",[65,1246,1217],{"class":1209},[65,1248,1249],{"class":1220},"started\n",[65,1251,1252,1255,1257],{"class":67,"line":98},[65,1253,1254],{"class":1213},"    enabled",[65,1256,1217],{"class":1209},[65,1258,1260],{"class":1259},"sj4cs","true\n",[11,1262,1263,1264,1267,1268,1271,1272,1275],{},"Jeśli inżynier uruchomił ",[15,1265,1266],{},"systemctl disable myapp --now"," na serwerze żeby debugować spike CPU, a potem zapomniał, to zadanie raportuje ",[15,1269,1270],{},"ok"," (już uruchomiony) lub ",[15,1273,1274],{},"changed"," (ponownie włączony), ale nie mówi ci, że ręczna interwencja nastąpiła. Playbook konwerguje stan, ale straciłeś sygnał, że był dryft.",[11,1277,1278],{},"Wzorzec, którego zamiast tego używam:",[38,1280,1282],{"className":1194,"code":1281,"language":1196,"meta":46,"style":46},"- name: Check if service has been manually overridden\n  ansible.builtin.command: systemctl is-enabled myapp\n  register: svc_enabled\n  changed_when: false\n  failed_when: false\n\n- name: Warn on manual override\n  ansible.builtin.debug:\n    msg: \"WARNING: myapp service is {{ svc_enabled.stdout }} — expected 'enabled'\"\n  when: svc_enabled.stdout != 'enabled'\n\n- name: Converge service state\n  ansible.builtin.service:\n    name: myapp\n    state: started\n    enabled: true\n",[15,1283,1284,1295,1305,1315,1325,1334,1338,1349,1356,1366,1376,1380,1391,1397,1405,1413],{"__ignoreMap":46},[65,1285,1286,1288,1290,1292],{"class":67,"line":68},[65,1287,1210],{"class":1209},[65,1289,1214],{"class":1213},[65,1291,1217],{"class":1209},[65,1293,1294],{"class":1220},"Check if service has been manually overridden\n",[65,1296,1297,1300,1302],{"class":67,"line":74},[65,1298,1299],{"class":1213},"  ansible.builtin.command",[65,1301,1217],{"class":1209},[65,1303,1304],{"class":1220},"systemctl is-enabled myapp\n",[65,1306,1307,1310,1312],{"class":67,"line":80},[65,1308,1309],{"class":1213},"  register",[65,1311,1217],{"class":1209},[65,1313,1314],{"class":1220},"svc_enabled\n",[65,1316,1317,1320,1322],{"class":67,"line":86},[65,1318,1319],{"class":1213},"  changed_when",[65,1321,1217],{"class":1209},[65,1323,1324],{"class":1259},"false\n",[65,1326,1327,1330,1332],{"class":67,"line":92},[65,1328,1329],{"class":1213},"  failed_when",[65,1331,1217],{"class":1209},[65,1333,1324],{"class":1259},[65,1335,1336],{"class":67,"line":98},[65,1337,102],{"emptyLinePlaceholder":101},[65,1339,1340,1342,1344,1346],{"class":67,"line":105},[65,1341,1210],{"class":1209},[65,1343,1214],{"class":1213},[65,1345,1217],{"class":1209},[65,1347,1348],{"class":1220},"Warn on manual override\n",[65,1350,1351,1354],{"class":67,"line":111},[65,1352,1353],{"class":1213},"  ansible.builtin.debug",[65,1355,1229],{"class":1209},[65,1357,1358,1361,1363],{"class":67,"line":116},[65,1359,1360],{"class":1213},"    msg",[65,1362,1217],{"class":1209},[65,1364,1365],{"class":1220},"\"WARNING: myapp service is {{ svc_enabled.stdout }} — expected 'enabled'\"\n",[65,1367,1368,1371,1373],{"class":67,"line":122},[65,1369,1370],{"class":1213},"  when",[65,1372,1217],{"class":1209},[65,1374,1375],{"class":1220},"svc_enabled.stdout != 'enabled'\n",[65,1377,1378],{"class":67,"line":127},[65,1379,102],{"emptyLinePlaceholder":101},[65,1381,1382,1384,1386,1388],{"class":67,"line":133},[65,1383,1210],{"class":1209},[65,1385,1214],{"class":1213},[65,1387,1217],{"class":1209},[65,1389,1390],{"class":1220},"Converge service state\n",[65,1392,1393,1395],{"class":67,"line":139},[65,1394,1226],{"class":1213},[65,1396,1229],{"class":1209},[65,1398,1399,1401,1403],{"class":67,"line":145},[65,1400,1234],{"class":1213},[65,1402,1217],{"class":1209},[65,1404,1239],{"class":1220},[65,1406,1407,1409,1411],{"class":67,"line":151},[65,1408,1244],{"class":1213},[65,1410,1217],{"class":1209},[65,1412,1249],{"class":1220},[65,1414,1415,1417,1419],{"class":67,"line":157},[65,1416,1254],{"class":1213},[65,1418,1217],{"class":1209},[65,1420,1260],{"class":1259},[11,1422,1423],{},"Ostrzeżenie nie blokuje playbooka. Produkuje widoczny sygnał, że człowiek dokonał zmiany, którą Ansible teraz nadpisuje. W kontekście CI\u002FCD parsuje się ten output i tworzy alert.",[27,1425,1427],{"id":1426},"playbook-na-3-w-nocy","Playbook na 3 w nocy",[11,1429,1430],{},"Scenariusz: produkcyjne serwery API zwracają 502. Health checki load balancera nie przechodzą. Inżynier on-call ma 90 sekund zanim klienci to zauważą. Przyczyna: deploy job zakończył się timeoutem w połowie aktualizacji konfiguracji nginx upstream, pozostawiając trzy z ośmiu serwerów ze starą konfiguracją i pięć z nową.",[11,1432,1433],{},"Playbook remediacji piszesz gdy nie jesteś pod presją, żeby gdy jesteś pod presją, uruchomić jedną komendę:",[38,1435,1437],{"className":1194,"code":1436,"language":1196,"meta":46,"style":46},"---\n- name: Emergency nginx config convergence\n  hosts: api_servers\n  serial: 2               # konwerguj dwa naraz, trzymaj 6\u002F8 serwujących ruch\n  max_fail_percentage: 25 # przerwij jeśli więcej niż 2 serwery zawiodą konwergencję\n\n  tasks:\n    - name: Validate config template renders without errors\n      ansible.builtin.template:\n        src:  templates\u002Fnginx-upstream.conf.j2\n        dest: \u002Ftmp\u002Fnginx-upstream-validate.conf\n        mode: '0600'\n      changed_when: false\n\n    - name: Syntax check the rendered config\n      ansible.builtin.command: nginx -t -c \u002Ftmp\u002Fnginx-upstream-validate.conf\n      changed_when: false\n      # If nginx -t fails, the play fails here — before touching the live config\n\n    - name: Deploy nginx upstream config\n      ansible.builtin.template:\n        src:   templates\u002Fnginx-upstream.conf.j2\n        dest:  \u002Fetc\u002Fnginx\u002Fconf.d\u002Fupstream.conf\n        owner: root\n        group: root\n        mode:  '0644'\n        backup: true    # keeps upstream.conf.TIMESTAMP on the server\n      notify: reload nginx\n\n    - name: Verify health endpoint responds after reload\n      ansible.builtin.uri:\n        url:            \"http:\u002F\u002Flocalhost:{{ app_port }}\u002Fhealth\"\n        status_code:    200\n        timeout:        10\n      retries: 3\n      delay: 2\n\n  handlers:\n    - name: reload nginx\n      ansible.builtin.service:\n        name:  nginx\n        state: reloaded\n      # reloaded, not restarted — zero downtime config update\n",[15,1438,1439,1445,1456,1466,1479,1492,1496,1503,1515,1522,1533,1543,1553,1562,1566,1577,1587,1595,1600,1604,1615,1621,1630,1639,1649,1658,1667,1680,1690,1694,1705,1712,1723,1734,1745,1755,1765,1769,1776,1786,1793,1803,1813],{"__ignoreMap":46},[65,1440,1441],{"class":67,"line":68},[65,1442,1444],{"class":1443},"sScJk","---\n",[65,1446,1447,1449,1451,1453],{"class":67,"line":74},[65,1448,1210],{"class":1209},[65,1450,1214],{"class":1213},[65,1452,1217],{"class":1209},[65,1454,1455],{"class":1220},"Emergency nginx config convergence\n",[65,1457,1458,1461,1463],{"class":67,"line":80},[65,1459,1460],{"class":1213},"  hosts",[65,1462,1217],{"class":1209},[65,1464,1465],{"class":1220},"api_servers\n",[65,1467,1468,1471,1473,1476],{"class":67,"line":86},[65,1469,1470],{"class":1213},"  serial",[65,1472,1217],{"class":1209},[65,1474,1475],{"class":1259},"2",[65,1477,1478],{"class":1203},"               # konwerguj dwa naraz, trzymaj 6\u002F8 serwujących ruch\n",[65,1480,1481,1484,1486,1489],{"class":67,"line":92},[65,1482,1483],{"class":1213},"  max_fail_percentage",[65,1485,1217],{"class":1209},[65,1487,1488],{"class":1259},"25",[65,1490,1491],{"class":1203}," # przerwij jeśli więcej niż 2 serwery zawiodą konwergencję\n",[65,1493,1494],{"class":67,"line":98},[65,1495,102],{"emptyLinePlaceholder":101},[65,1497,1498,1501],{"class":67,"line":105},[65,1499,1500],{"class":1213},"  tasks",[65,1502,1229],{"class":1209},[65,1504,1505,1508,1510,1512],{"class":67,"line":111},[65,1506,1507],{"class":1209},"    - ",[65,1509,1214],{"class":1213},[65,1511,1217],{"class":1209},[65,1513,1514],{"class":1220},"Validate config template renders without errors\n",[65,1516,1517,1520],{"class":67,"line":116},[65,1518,1519],{"class":1213},"      ansible.builtin.template",[65,1521,1229],{"class":1209},[65,1523,1524,1527,1530],{"class":67,"line":122},[65,1525,1526],{"class":1213},"        src",[65,1528,1529],{"class":1209},":  ",[65,1531,1532],{"class":1220},"templates\u002Fnginx-upstream.conf.j2\n",[65,1534,1535,1538,1540],{"class":67,"line":127},[65,1536,1537],{"class":1213},"        dest",[65,1539,1217],{"class":1209},[65,1541,1542],{"class":1220},"\u002Ftmp\u002Fnginx-upstream-validate.conf\n",[65,1544,1545,1548,1550],{"class":67,"line":133},[65,1546,1547],{"class":1213},"        mode",[65,1549,1217],{"class":1209},[65,1551,1552],{"class":1220},"'0600'\n",[65,1554,1555,1558,1560],{"class":67,"line":139},[65,1556,1557],{"class":1213},"      changed_when",[65,1559,1217],{"class":1209},[65,1561,1324],{"class":1259},[65,1563,1564],{"class":67,"line":145},[65,1565,102],{"emptyLinePlaceholder":101},[65,1567,1568,1570,1572,1574],{"class":67,"line":151},[65,1569,1507],{"class":1209},[65,1571,1214],{"class":1213},[65,1573,1217],{"class":1209},[65,1575,1576],{"class":1220},"Syntax check the rendered config\n",[65,1578,1579,1582,1584],{"class":67,"line":157},[65,1580,1581],{"class":1213},"      ansible.builtin.command",[65,1583,1217],{"class":1209},[65,1585,1586],{"class":1220},"nginx -t -c \u002Ftmp\u002Fnginx-upstream-validate.conf\n",[65,1588,1589,1591,1593],{"class":67,"line":163},[65,1590,1557],{"class":1213},[65,1592,1217],{"class":1209},[65,1594,1324],{"class":1259},[65,1596,1597],{"class":67,"line":169},[65,1598,1599],{"class":1203},"      # If nginx -t fails, the play fails here — before touching the live config\n",[65,1601,1602],{"class":67,"line":175},[65,1603,102],{"emptyLinePlaceholder":101},[65,1605,1606,1608,1610,1612],{"class":67,"line":180},[65,1607,1507],{"class":1209},[65,1609,1214],{"class":1213},[65,1611,1217],{"class":1209},[65,1613,1614],{"class":1220},"Deploy nginx upstream config\n",[65,1616,1617,1619],{"class":67,"line":185},[65,1618,1519],{"class":1213},[65,1620,1229],{"class":1209},[65,1622,1623,1625,1628],{"class":67,"line":191},[65,1624,1526],{"class":1213},[65,1626,1627],{"class":1209},":   ",[65,1629,1532],{"class":1220},[65,1631,1632,1634,1636],{"class":67,"line":196},[65,1633,1537],{"class":1213},[65,1635,1529],{"class":1209},[65,1637,1638],{"class":1220},"\u002Fetc\u002Fnginx\u002Fconf.d\u002Fupstream.conf\n",[65,1640,1641,1644,1646],{"class":67,"line":202},[65,1642,1643],{"class":1213},"        owner",[65,1645,1217],{"class":1209},[65,1647,1648],{"class":1220},"root\n",[65,1650,1651,1654,1656],{"class":67,"line":207},[65,1652,1653],{"class":1213},"        group",[65,1655,1217],{"class":1209},[65,1657,1648],{"class":1220},[65,1659,1660,1662,1664],{"class":67,"line":212},[65,1661,1547],{"class":1213},[65,1663,1529],{"class":1209},[65,1665,1666],{"class":1220},"'0644'\n",[65,1668,1669,1672,1674,1677],{"class":67,"line":217},[65,1670,1671],{"class":1213},"        backup",[65,1673,1217],{"class":1209},[65,1675,1676],{"class":1259},"true",[65,1678,1679],{"class":1203},"    # keeps upstream.conf.TIMESTAMP on the server\n",[65,1681,1682,1685,1687],{"class":67,"line":223},[65,1683,1684],{"class":1213},"      notify",[65,1686,1217],{"class":1209},[65,1688,1689],{"class":1220},"reload nginx\n",[65,1691,1692],{"class":67,"line":229},[65,1693,102],{"emptyLinePlaceholder":101},[65,1695,1696,1698,1700,1702],{"class":67,"line":235},[65,1697,1507],{"class":1209},[65,1699,1214],{"class":1213},[65,1701,1217],{"class":1209},[65,1703,1704],{"class":1220},"Verify health endpoint responds after reload\n",[65,1706,1707,1710],{"class":67,"line":241},[65,1708,1709],{"class":1213},"      ansible.builtin.uri",[65,1711,1229],{"class":1209},[65,1713,1714,1717,1720],{"class":67,"line":246},[65,1715,1716],{"class":1213},"        url",[65,1718,1719],{"class":1209},":            ",[65,1721,1722],{"class":1220},"\"http:\u002F\u002Flocalhost:{{ app_port }}\u002Fhealth\"\n",[65,1724,1725,1728,1731],{"class":67,"line":251},[65,1726,1727],{"class":1213},"        status_code",[65,1729,1730],{"class":1209},":    ",[65,1732,1733],{"class":1259},"200\n",[65,1735,1736,1739,1742],{"class":67,"line":256},[65,1737,1738],{"class":1213},"        timeout",[65,1740,1741],{"class":1209},":        ",[65,1743,1744],{"class":1259},"10\n",[65,1746,1747,1750,1752],{"class":67,"line":261},[65,1748,1749],{"class":1213},"      retries",[65,1751,1217],{"class":1209},[65,1753,1754],{"class":1259},"3\n",[65,1756,1757,1760,1762],{"class":67,"line":267},[65,1758,1759],{"class":1213},"      delay",[65,1761,1217],{"class":1209},[65,1763,1764],{"class":1259},"2\n",[65,1766,1767],{"class":67,"line":272},[65,1768,102],{"emptyLinePlaceholder":101},[65,1770,1771,1774],{"class":67,"line":278},[65,1772,1773],{"class":1213},"  handlers",[65,1775,1229],{"class":1209},[65,1777,1778,1780,1782,1784],{"class":67,"line":283},[65,1779,1507],{"class":1209},[65,1781,1214],{"class":1213},[65,1783,1217],{"class":1209},[65,1785,1689],{"class":1220},[65,1787,1788,1791],{"class":67,"line":288},[65,1789,1790],{"class":1213},"      ansible.builtin.service",[65,1792,1229],{"class":1209},[65,1794,1795,1798,1800],{"class":67,"line":293},[65,1796,1797],{"class":1213},"        name",[65,1799,1529],{"class":1209},[65,1801,1802],{"class":1220},"nginx\n",[65,1804,1805,1808,1810],{"class":67,"line":299},[65,1806,1807],{"class":1213},"        state",[65,1809,1217],{"class":1209},[65,1811,1812],{"class":1220},"reloaded\n",[65,1814,1815],{"class":67,"line":305},[65,1816,1817],{"class":1203},"      # reloaded, not restarted — zero downtime config update\n",[11,1819,1820,1823,1824,1826],{},[15,1821,1822],{},"serial: 2"," to parametr, który ma największe znaczenie. Przy ośmiu serwerach i ",[15,1825,1822],{}," zawsze masz co najmniej sześć serwerów serwujących ruch podczas konwergencji. Bez tego Ansible konwerguje wszystkie hosty równolegle i dostajesz krótkie okno, gdzie wszystkie osiem jednocześnie przeładowuje nginx, wdrożenie z wiarą i modlitwą w najczystszej postaci.",[27,1828,1830],{"id":1829},"vault-i-sekret-który-przypadkowo-skomitowałeś","Vault i sekret, który przypadkowo skomitowałeś",[11,1832,1833,1834,1837],{},"Każdy zespół w końcu commituje sekret do repozytorium Ansible. Podręcznikowa odpowiedź to Ansible Vault. Produkcyjna odpowiedź: Ansible Vault dla sekretów należących do playbooka, zewnętrzne zarządzanie sekretami (HashiCorp Vault, AWS Secrets Manager) dla sekretów współdzielonych między systemami, i ",[15,1835,1836],{},"no_log: true"," na każdym zadaniu obsługującym którekolwiek z nich.",[38,1839,1841],{"className":1194,"code":1840,"language":1196,"meta":46,"style":46},"- name: Set database credentials in application config\n  ansible.builtin.template:\n    src:  templates\u002Fdatabase.php.j2\n    dest: \u002Fvar\u002Fwww\u002Fhtml\u002Fconfig\u002Fdatabase.php\n    mode: '0640'\n  vars:\n    db_password: \"{{ lookup('aws_ssm', '\u002Fprod\u002Fapp\u002Fdb_password', region='eu-west-1') }}\"\n  no_log: true   # prevents the rendered template (containing the password) from appearing in logs\n",[15,1842,1843,1854,1861,1871,1881,1891,1898,1908],{"__ignoreMap":46},[65,1844,1845,1847,1849,1851],{"class":67,"line":68},[65,1846,1210],{"class":1209},[65,1848,1214],{"class":1213},[65,1850,1217],{"class":1209},[65,1852,1853],{"class":1220},"Set database credentials in application config\n",[65,1855,1856,1859],{"class":67,"line":74},[65,1857,1858],{"class":1213},"  ansible.builtin.template",[65,1860,1229],{"class":1209},[65,1862,1863,1866,1868],{"class":67,"line":80},[65,1864,1865],{"class":1213},"    src",[65,1867,1529],{"class":1209},[65,1869,1870],{"class":1220},"templates\u002Fdatabase.php.j2\n",[65,1872,1873,1876,1878],{"class":67,"line":86},[65,1874,1875],{"class":1213},"    dest",[65,1877,1217],{"class":1209},[65,1879,1880],{"class":1220},"\u002Fvar\u002Fwww\u002Fhtml\u002Fconfig\u002Fdatabase.php\n",[65,1882,1883,1886,1888],{"class":67,"line":92},[65,1884,1885],{"class":1213},"    mode",[65,1887,1217],{"class":1209},[65,1889,1890],{"class":1220},"'0640'\n",[65,1892,1893,1896],{"class":67,"line":98},[65,1894,1895],{"class":1213},"  vars",[65,1897,1229],{"class":1209},[65,1899,1900,1903,1905],{"class":67,"line":105},[65,1901,1902],{"class":1213},"    db_password",[65,1904,1217],{"class":1209},[65,1906,1907],{"class":1220},"\"{{ lookup('aws_ssm', '\u002Fprod\u002Fapp\u002Fdb_password', region='eu-west-1') }}\"\n",[65,1909,1910,1913,1915,1917],{"class":67,"line":111},[65,1911,1912],{"class":1213},"  no_log",[65,1914,1217],{"class":1209},[65,1916,1676],{"class":1259},[65,1918,1919],{"class":1203},"   # prevents the rendered template (containing the password) from appearing in logs\n",[11,1921,1922,1924,1925,1928],{},[15,1923,1836],{}," nie tylko tłumi output zadania, tłumi też output diff. Jeśli uruchamiasz ",[15,1926,1927],{},"--diff"," żeby przejrzeć co się zmieniło, nie zobaczysz wyrenderowanego szablonu. To feature, nie bug.",[27,1930,1932],{"id":1931},"testowanie-playbooków-zanim-będą-miały-znaczenie","Testowanie playbooków zanim będą miały znaczenie",[11,1934,1935],{},"Dwa narzędzia, których używam do każdej nietrywialnej roli. Molecule do testowania na poziomie roli: odpala kontener lub VM, uruchamia rolę, uruchamia weryfikator (zazwyczaj Testinfra) i sprawdza, że pożądany stan faktycznie został osiągnięty, nie tylko że Ansible zaraportował sukces.",[38,1937,1939],{"className":1008,"code":1938,"language":1010,"meta":46,"style":46},"# molecule\u002Fdefault\u002Ftests\u002Ftest_nginx.py\nimport testinfra\n\ndef test_nginx_is_running(host):\n    nginx = host.service(\"nginx\")\n    assert nginx.is_running\n    assert nginx.is_enabled\n\ndef test_nginx_config_is_valid(host):\n    result = host.run(\"nginx -t\")\n    assert result.rc == 0\n",[15,1940,1941,1946,1951,1955,1960,1965,1970,1975,1979,1984,1989],{"__ignoreMap":46},[65,1942,1943],{"class":67,"line":68},[65,1944,1945],{},"# molecule\u002Fdefault\u002Ftests\u002Ftest_nginx.py\n",[65,1947,1948],{"class":67,"line":74},[65,1949,1950],{},"import testinfra\n",[65,1952,1953],{"class":67,"line":80},[65,1954,102],{"emptyLinePlaceholder":101},[65,1956,1957],{"class":67,"line":86},[65,1958,1959],{},"def test_nginx_is_running(host):\n",[65,1961,1962],{"class":67,"line":92},[65,1963,1964],{},"    nginx = host.service(\"nginx\")\n",[65,1966,1967],{"class":67,"line":98},[65,1968,1969],{},"    assert nginx.is_running\n",[65,1971,1972],{"class":67,"line":105},[65,1973,1974],{},"    assert nginx.is_enabled\n",[65,1976,1977],{"class":67,"line":111},[65,1978,102],{"emptyLinePlaceholder":101},[65,1980,1981],{"class":67,"line":116},[65,1982,1983],{},"def test_nginx_config_is_valid(host):\n",[65,1985,1986],{"class":67,"line":122},[65,1987,1988],{},"    result = host.run(\"nginx -t\")\n",[65,1990,1991],{"class":67,"line":127},[65,1992,1993],{},"    assert result.rc == 0\n",[11,1995,1996,1997,2000,2001,2003,2004,2007,2008,2010],{},"Tryb ",[15,1998,1999],{},"--check"," z ",[15,2002,1927],{}," przed każdym uruchomieniem produkcyjnym pokazuje co Ansible by zmienił bez faktycznej zmiany. Output diff na zadaniach template jest szczególnie przydatny: widzisz dokładnie które linie w pliku konfiguracyjnym zostałyby zmodyfikowane. Limitowanie do jednego serwera przez ",[15,2005,2006],{},"--limit api_servers[0]"," jest niezbędne, bo ",[15,2009,1999],{}," przez cały inventory produkcyjny może zajmować minuty, a na jednym reprezentatywnym serwerze sekundy.",[27,2012,2014],{"id":2013},"na-co-zwracam-uwagę-w-code-review-ansible","Na co zwracam uwagę w code review Ansible",[11,2016,2017,2018,2021,2022,2025,2026,2029,2030,2032,2033,2035,2036,2039,2040,2043,2044,2047],{},"Zadania ",[15,2019,2020],{},"command"," lub ",[15,2023,2024],{},"shell"," bez ",[15,2027,2028],{},"changed_when"," raportują ",[15,2031,1274],{}," za każdym razem gdy się uruchamiają, nawet jeśli nic się nie zmieniło. To sprawia, że twój diff z ",[15,2034,1999],{}," jest bezużyteczny. ",[15,2037,2038],{},"ignore_errors: true"," na czymkolwiek infrastrukturalnym to odpowiednik ",[15,2041,2042],{},"catch (Exception e) {}"," bez treści, playbook powinien się zatrzymać, nie kontynuować z potencjalnie uszkodzonym serwerem w puli. Brakujące ",[15,2045,2046],{},"become: false"," na zadaniach, które nie potrzebują roota: playbook gdzie każde zadanie działa jako root to playbook, gdzie bug ma blast radius całego serwera.",[936,2049,2050],{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}",{"title":46,"searchDepth":74,"depth":74,"links":2052},[2053,2054,2055,2056,2057],{"id":1184,"depth":74,"text":1185},{"id":1426,"depth":74,"text":1427},{"id":1829,"depth":74,"text":1830},{"id":1931,"depth":74,"text":1932},{"id":2013,"depth":74,"text":2014},"backend","2024-05-06","Demo playbook instaluje nginx i go uruchamia. Działa raz na czystej VM i wszyscy na demie kiwają głowami. To, czego nikt nie demonstruje, to uruchomienie tego samego playbooka sześć miesięcy później na serwerze, gdzie inżynier ręcznie edytował \u002Fetc\u002Fnginx\u002Fnginx.conf żeby tymczasowo naprawić problem produkcyjny i potem zapomniał to udokumentować. Albo po tym, jak pakiet nginx został zaktualizowany przez niezauważony apt cron job. Albo na serwerze, który nigdy nie był właściwie skonwergowany, bo ktoś anulował playbook w połowie.",{},"\u002Fpl\u002Farticles\u002Fansible-production",{"x":2064,"y":2065,"depth":957,"size":950},0.36,0.8,[1154,1155],{"title":1168,"description":2060},"infra-automation","pl\u002Farticles\u002Fansible-production",[2071,2072,2073,2074,2075,2076],"ansible","devops","infrastructure","automation","idempotency","configuration-management","maVkdtsMvOo2AyFNhTb777n0aiXrYnYpjixrQT5DMsY",{"id":4,"title":5,"articleId":6,"body":2079,"category":61,"codeLang":61,"date":947,"deploys":68,"description":948,"excerpt":949,"extension":950,"lang":951,"meta":2810,"navigation":101,"path":953,"pos":2811,"readMin":116,"related":2812,"seo":2813,"service":962,"stem":963,"tags":2814,"version":970,"__hash__":971},{"type":8,"value":2080,"toc":2802},[2081,2087,2089,2091,2093,2095,2100,2102,2104,2106,2290,2450,2452,2454,2456,2512,2516,2576,2578,2580,2640,2646,2652,2654,2656,2788,2790,2792,2800],[11,2082,13,2083,18,2085,22],{},[15,2084,17],{},[15,2086,21],{},[11,2088,25],{},[27,2090,30],{"id":29},[11,2092,33],{},[11,2094,36],{},[38,2096,2098],{"className":2097,"code":42,"language":43},[41],[15,2099,42],{"__ignoreMap":46},[11,2101,49],{},[27,2103,53],{"id":52},[11,2105,56],{},[38,2107,2108],{"className":59,"code":60,"language":61,"meta":46,"style":46},[15,2109,2110,2114,2118,2122,2126,2130,2134,2138,2142,2146,2150,2154,2158,2162,2166,2170,2174,2178,2182,2186,2190,2194,2198,2202,2206,2210,2214,2218,2222,2226,2230,2234,2238,2242,2246,2250,2254,2258,2262,2266,2270,2274,2278,2282,2286],{"__ignoreMap":46},[65,2111,2112],{"class":67,"line":68},[65,2113,71],{},[65,2115,2116],{"class":67,"line":74},[65,2117,77],{},[65,2119,2120],{"class":67,"line":80},[65,2121,83],{},[65,2123,2124],{"class":67,"line":86},[65,2125,89],{},[65,2127,2128],{"class":67,"line":92},[65,2129,95],{},[65,2131,2132],{"class":67,"line":98},[65,2133,102],{"emptyLinePlaceholder":101},[65,2135,2136],{"class":67,"line":105},[65,2137,108],{},[65,2139,2140],{"class":67,"line":111},[65,2141,83],{},[65,2143,2144],{"class":67,"line":116},[65,2145,119],{},[65,2147,2148],{"class":67,"line":122},[65,2149,102],{"emptyLinePlaceholder":101},[65,2151,2152],{"class":67,"line":127},[65,2153,130],{},[65,2155,2156],{"class":67,"line":133},[65,2157,136],{},[65,2159,2160],{"class":67,"line":139},[65,2161,142],{},[65,2163,2164],{"class":67,"line":145},[65,2165,148],{},[65,2167,2168],{"class":67,"line":151},[65,2169,154],{},[65,2171,2172],{"class":67,"line":157},[65,2173,160],{},[65,2175,2176],{"class":67,"line":163},[65,2177,166],{},[65,2179,2180],{"class":67,"line":169},[65,2181,172],{},[65,2183,2184],{"class":67,"line":175},[65,2185,95],{},[65,2187,2188],{"class":67,"line":180},[65,2189,102],{"emptyLinePlaceholder":101},[65,2191,2192],{"class":67,"line":185},[65,2193,188],{},[65,2195,2196],{"class":67,"line":191},[65,2197,83],{},[65,2199,2200],{"class":67,"line":196},[65,2201,199],{},[65,2203,2204],{"class":67,"line":202},[65,2205,102],{"emptyLinePlaceholder":101},[65,2207,2208],{"class":67,"line":207},[65,2209,130],{},[65,2211,2212],{"class":67,"line":212},[65,2213,136],{},[65,2215,2216],{"class":67,"line":217},[65,2217,220],{},[65,2219,2220],{"class":67,"line":223},[65,2221,226],{},[65,2223,2224],{"class":67,"line":229},[65,2225,232],{},[65,2227,2228],{"class":67,"line":235},[65,2229,238],{},[65,2231,2232],{"class":67,"line":241},[65,2233,166],{},[65,2235,2236],{"class":67,"line":246},[65,2237,172],{},[65,2239,2240],{"class":67,"line":251},[65,2241,95],{},[65,2243,2244],{"class":67,"line":256},[65,2245,102],{"emptyLinePlaceholder":101},[65,2247,2248],{"class":67,"line":261},[65,2249,264],{},[65,2251,2252],{"class":67,"line":267},[65,2253,83],{},[65,2255,2256],{"class":67,"line":272},[65,2257,275],{},[65,2259,2260],{"class":67,"line":278},[65,2261,102],{"emptyLinePlaceholder":101},[65,2263,2264],{"class":67,"line":283},[65,2265,130],{},[65,2267,2268],{"class":67,"line":288},[65,2269,136],{},[65,2271,2272],{"class":67,"line":293},[65,2273,296],{},[65,2275,2276],{"class":67,"line":299},[65,2277,302],{},[65,2279,2280],{"class":67,"line":305},[65,2281,308],{},[65,2283,2284],{"class":67,"line":311},[65,2285,172],{},[65,2287,2288],{"class":67,"line":316},[65,2289,95],{},[38,2291,2292],{"className":59,"code":321,"language":61,"meta":46,"style":46},[15,2293,2294,2298,2302,2306,2310,2314,2318,2322,2326,2330,2334,2338,2342,2346,2350,2354,2358,2362,2366,2370,2374,2378,2382,2386,2390,2394,2398,2402,2406,2410,2414,2418,2422,2426,2430,2434,2438,2442,2446],{"__ignoreMap":46},[65,2295,2296],{"class":67,"line":68},[65,2297,328],{},[65,2299,2300],{"class":67,"line":74},[65,2301,333],{},[65,2303,2304],{"class":67,"line":80},[65,2305,83],{},[65,2307,2308],{"class":67,"line":86},[65,2309,342],{},[65,2311,2312],{"class":67,"line":92},[65,2313,347],{},[65,2315,2316],{"class":67,"line":98},[65,2317,352],{},[65,2319,2320],{"class":67,"line":105},[65,2321,102],{"emptyLinePlaceholder":101},[65,2323,2324],{"class":67,"line":111},[65,2325,361],{},[65,2327,2328],{"class":67,"line":116},[65,2329,95],{},[65,2331,2332],{"class":67,"line":122},[65,2333,102],{"emptyLinePlaceholder":101},[65,2335,2336],{"class":67,"line":127},[65,2337,374],{},[65,2339,2340],{"class":67,"line":133},[65,2341,83],{},[65,2343,2344],{"class":67,"line":139},[65,2345,342],{},[65,2347,2348],{"class":67,"line":145},[65,2349,387],{},[65,2351,2352],{"class":67,"line":151},[65,2353,392],{},[65,2355,2356],{"class":67,"line":157},[65,2357,397],{},[65,2359,2360],{"class":67,"line":163},[65,2361,402],{},[65,2363,2364],{"class":67,"line":169},[65,2365,172],{},[65,2367,2368],{"class":67,"line":175},[65,2369,102],{"emptyLinePlaceholder":101},[65,2371,2372],{"class":67,"line":180},[65,2373,415],{},[65,2375,2376],{"class":67,"line":185},[65,2377,136],{},[65,2379,2380],{"class":67,"line":191},[65,2381,424],{},[65,2383,2384],{"class":67,"line":196},[65,2385,429],{},[65,2387,2388],{"class":67,"line":202},[65,2389,434],{},[65,2391,2392],{"class":67,"line":207},[65,2393,439],{},[65,2395,2396],{"class":67,"line":212},[65,2397,166],{},[65,2399,2400],{"class":67,"line":217},[65,2401,172],{},[65,2403,2404],{"class":67,"line":223},[65,2405,102],{"emptyLinePlaceholder":101},[65,2407,2408],{"class":67,"line":229},[65,2409,456],{},[65,2411,2412],{"class":67,"line":235},[65,2413,136],{},[65,2415,2416],{"class":67,"line":241},[65,2417,465],{},[65,2419,2420],{"class":67,"line":246},[65,2421,470],{},[65,2423,2424],{"class":67,"line":251},[65,2425,475],{},[65,2427,2428],{"class":67,"line":256},[65,2429,480],{},[65,2431,2432],{"class":67,"line":261},[65,2433,485],{},[65,2435,2436],{"class":67,"line":267},[65,2437,490],{},[65,2439,2440],{"class":67,"line":272},[65,2441,166],{},[65,2443,2444],{"class":67,"line":278},[65,2445,172],{},[65,2447,2448],{"class":67,"line":283},[65,2449,95],{},[11,2451,505],{},[27,2453,509],{"id":508},[11,2455,512],{},[38,2457,2458],{"className":59,"code":515,"language":61,"meta":46,"style":46},[15,2459,2460,2464,2468,2472,2476,2480,2484,2488,2492,2496,2500,2504,2508],{"__ignoreMap":46},[65,2461,2462],{"class":67,"line":68},[65,2463,522],{},[65,2465,2466],{"class":67,"line":74},[65,2467,527],{},[65,2469,2470],{"class":67,"line":80},[65,2471,532],{},[65,2473,2474],{"class":67,"line":86},[65,2475,537],{},[65,2477,2478],{"class":67,"line":92},[65,2479,542],{},[65,2481,2482],{"class":67,"line":98},[65,2483,547],{},[65,2485,2486],{"class":67,"line":105},[65,2487,102],{"emptyLinePlaceholder":101},[65,2489,2490],{"class":67,"line":111},[65,2491,556],{},[65,2493,2494],{"class":67,"line":116},[65,2495,527],{},[65,2497,2498],{"class":67,"line":122},[65,2499,565],{},[65,2501,2502],{"class":67,"line":127},[65,2503,537],{},[65,2505,2506],{"class":67,"line":133},[65,2507,542],{},[65,2509,2510],{"class":67,"line":139},[65,2511,578],{},[11,2513,581,2514,585],{},[15,2515,584],{},[38,2517,2518],{"className":59,"code":588,"language":61,"meta":46,"style":46},[15,2519,2520,2524,2528,2532,2536,2540,2544,2548,2552,2556,2560,2564,2568,2572],{"__ignoreMap":46},[65,2521,2522],{"class":67,"line":68},[65,2523,595],{},[65,2525,2526],{"class":67,"line":74},[65,2527,83],{},[65,2529,2530],{"class":67,"line":80},[65,2531,604],{},[65,2533,2534],{"class":67,"line":86},[65,2535,609],{},[65,2537,2538],{"class":67,"line":92},[65,2539,102],{"emptyLinePlaceholder":101},[65,2541,2542],{"class":67,"line":98},[65,2543,618],{},[65,2545,2546],{"class":67,"line":105},[65,2547,136],{},[65,2549,2550],{"class":67,"line":111},[65,2551,627],{},[65,2553,2554],{"class":67,"line":116},[65,2555,632],{},[65,2557,2558],{"class":67,"line":122},[65,2559,637],{},[65,2561,2562],{"class":67,"line":127},[65,2563,642],{},[65,2565,2566],{"class":67,"line":133},[65,2567,647],{},[65,2569,2570],{"class":67,"line":139},[65,2571,172],{},[65,2573,2574],{"class":67,"line":145},[65,2575,95],{},[27,2577,659],{"id":658},[11,2579,662],{},[38,2581,2582],{"className":59,"code":665,"language":61,"meta":46,"style":46},[15,2583,2584,2588,2592,2596,2600,2604,2608,2612,2616,2620,2624,2628,2632,2636],{"__ignoreMap":46},[65,2585,2586],{"class":67,"line":68},[65,2587,672],{},[65,2589,2590],{"class":67,"line":74},[65,2591,677],{},[65,2593,2594],{"class":67,"line":80},[65,2595,83],{},[65,2597,2598],{"class":67,"line":86},[65,2599,415],{},[65,2601,2602],{"class":67,"line":92},[65,2603,136],{},[65,2605,2606],{"class":67,"line":98},[65,2607,694],{},[65,2609,2610],{"class":67,"line":105},[65,2611,699],{},[65,2613,2614],{"class":67,"line":111},[65,2615,704],{},[65,2617,2618],{"class":67,"line":116},[65,2619,709],{},[65,2621,2622],{"class":67,"line":122},[65,2623,647],{},[65,2625,2626],{"class":67,"line":127},[65,2627,102],{"emptyLinePlaceholder":101},[65,2629,2630],{"class":67,"line":133},[65,2631,722],{},[65,2633,2634],{"class":67,"line":139},[65,2635,172],{},[65,2637,2638],{"class":67,"line":145},[65,2639,95],{},[11,2641,733,2642,737,2644,741],{},[15,2643,736],{},[15,2645,740],{},[11,2647,744,2648,748,2650,752],{},[15,2649,747],{},[15,2651,751],{},[27,2653,756],{"id":755},[11,2655,759],{},[38,2657,2658],{"className":59,"code":762,"language":61,"meta":46,"style":46},[15,2659,2660,2664,2668,2672,2676,2680,2684,2688,2692,2696,2700,2704,2708,2712,2716,2720,2724,2728,2732,2736,2740,2744,2748,2752,2756,2760,2764,2768,2772,2776,2780,2784],{"__ignoreMap":46},[65,2661,2662],{"class":67,"line":68},[65,2663,769],{},[65,2665,2666],{"class":67,"line":74},[65,2667,774],{},[65,2669,2670],{"class":67,"line":80},[65,2671,83],{},[65,2673,2674],{"class":67,"line":86},[65,2675,783],{},[65,2677,2678],{"class":67,"line":92},[65,2679,136],{},[65,2681,2682],{"class":67,"line":98},[65,2683,792],{},[65,2685,2686],{"class":67,"line":105},[65,2687,797],{},[65,2689,2690],{"class":67,"line":111},[65,2691,802],{},[65,2693,2694],{"class":67,"line":116},[65,2695,807],{},[65,2697,2698],{"class":67,"line":122},[65,2699,102],{"emptyLinePlaceholder":101},[65,2701,2702],{"class":67,"line":127},[65,2703,816],{},[65,2705,2706],{"class":67,"line":133},[65,2707,172],{},[65,2709,2710],{"class":67,"line":139},[65,2711,95],{},[65,2713,2714],{"class":67,"line":145},[65,2715,102],{"emptyLinePlaceholder":101},[65,2717,2718],{"class":67,"line":151},[65,2719,833],{},[65,2721,2722],{"class":67,"line":157},[65,2723,838],{},[65,2725,2726],{"class":67,"line":163},[65,2727,83],{},[65,2729,2730],{"class":67,"line":169},[65,2731,847],{},[65,2733,2734],{"class":67,"line":175},[65,2735,136],{},[65,2737,2738],{"class":67,"line":180},[65,2739,856],{},[65,2741,2742],{"class":67,"line":185},[65,2743,861],{},[65,2745,2746],{"class":67,"line":191},[65,2747,866],{},[65,2749,2750],{"class":67,"line":196},[65,2751,871],{},[65,2753,2754],{"class":67,"line":202},[65,2755,876],{},[65,2757,2758],{"class":67,"line":207},[65,2759,881],{},[65,2761,2762],{"class":67,"line":212},[65,2763,886],{},[65,2765,2766],{"class":67,"line":217},[65,2767,891],{},[65,2769,2770],{"class":67,"line":223},[65,2771,102],{"emptyLinePlaceholder":101},[65,2773,2774],{"class":67,"line":229},[65,2775,900],{},[65,2777,2778],{"class":67,"line":235},[65,2779,905],{},[65,2781,2782],{"class":67,"line":241},[65,2783,172],{},[65,2785,2786],{"class":67,"line":246},[65,2787,95],{},[11,2789,916],{},[27,2791,920],{"id":919},[11,2793,923,2794,927,2796,927,2798,934],{},[15,2795,926],{},[15,2797,930],{},[15,2799,933],{},[936,2801,938],{},{"title":46,"searchDepth":74,"depth":74,"links":2803},[2804,2805,2806,2807,2808,2809],{"id":29,"depth":74,"text":30},{"id":52,"depth":74,"text":53},{"id":508,"depth":74,"text":509},{"id":658,"depth":74,"text":659},{"id":755,"depth":74,"text":756},{"id":919,"depth":74,"text":920},{},{"x":955,"y":956,"depth":957,"size":950},[959,960],{"title":5,"description":948},[61,965,966,967,968,969],{"id":2816,"title":2817,"articleId":960,"body":2818,"category":61,"codeLang":61,"date":3236,"deploys":68,"description":2822,"excerpt":949,"extension":950,"lang":951,"meta":3237,"navigation":101,"path":3238,"pos":3239,"readMin":151,"related":3243,"seo":3245,"service":3246,"stem":3247,"tags":3248,"version":970,"__hash__":3251},"articles_pl\u002Fpl\u002Farticles\u002Fdesign-patterns-production.md","Wzorce projektowe w produkcji: co rozwiązują, co kosztują i kiedy ich nie używać",{"type":8,"value":2819,"toc":3229},[2820,2823,2826,2829,2833,2845,2855,2880,2886,2910,2914,2920,3012,3018,3091,3101,3111,3117,3121,3142,3170,3176,3186,3192,3196,3202,3208,3214,3218,3221,3224,3227],[11,2821,2822],{},"Książka GoF ukazała się w 1994 roku. W ciągu trzydziestu lat od tamtej pory wzorce projektowe przeszły co najmniej trzy pełne cykle: wprowadzenie, nadużycie, backlash i ostrożne ponowne przyjęcie. Jesteśmy gdzieś w czwartym lub piątym cyklu, zależnie od tego, w której części branży pracujesz.",[11,2824,2825],{},"Mój pogląd, ukształtowany przez lata czytania codebases zarówno z wzorcami jak i bez: wzorce to nie rozwiązania. To wspólny słownik do nazywania kształtu problemu, który już rozwiązałeś. Wartość nie leży w implementacji, leży w nazewnictwie, bo nazywanie tego, co zbudowałeś, to sposób komunikowania tego następnemu inżynierowi.",[11,2827,2828],{},"To, co piszę poniżej, to przewodnik zorientowany produkcyjnie. Nie omawiam wszystkich dwudziestu trzech kanonicznych wzorców plus cokolwiek, co społeczność dorzuciła od tamtej pory, tylko te, po które regularnie sięgam, te które najczęściej widzę źle zastosowane i te, dla których nigdy nie znalazłem prawdziwego uzasadnienia w warstwie aplikacyjnej PHP.",[27,2830,2832],{"id":2831},"wzorce-kreacyjne-konstruowanie-obiektów-jest-trudniejsze-niż-wygląda","Wzorce kreacyjne: konstruowanie obiektów jest trudniejsze niż wygląda",[11,2834,2835,2839,2840],{},[2836,2837,2838],"strong",{},"Singleton"," to wzorzec, który zestarzał się najgorzej. Uzasadniony dokładnie wtedy, gdy masz niemutowalną konfigurację, która jest kosztowna do załadowania i musi być współdzielona w ramach jednego procesu. W prawie każdym innym przypadku, nie. ",[2841,2842,2844],"a",{"href":2843},"\u002Fpl\u002Farticles\u002Fsingleton-pattern","Omówiony szczegółowo w innym miejscu tej serii.",[11,2846,2847,2850,2851],{},[2836,2848,2849],{},"Factory Method"," to wzorzec, po który sięgam najczęściej w grupie kreacyjnej. Konieczny wtedy, gdy typ tworzonego obiektu jest decyzją runtime, wybór bramki płatności, kanału notyfikacji, parsera dokumentów dla nieznanego formatu pliku. Factory nie buduje obiektów; deleguje konstrukcję do kontenera DI i zwraca interfejs. ",[2841,2852,2854],{"href":2853},"\u002Fpl\u002Farticles\u002Ffactory-method","Omówiony szczegółowo w tej serii.",[11,2856,2857,2860,2861,2864,2865,2868,2869,2872,2873,2876,2877,2879],{},[2836,2858,2859],{},"Builder"," jest niedostatecznie używany przy złożonych obiektach domeny, a zbyt często stosowany przy konstruowaniu zapytań. ",[15,2862,2863],{},"QueryBuilder"," to prawidłowy Builder: akumuluje warunki, potem produkuje niemutowalny obiekt zapytania. ",[15,2866,2867],{},"UserBuilder"," istniejący tylko po to, żeby testy były czytelne (",[15,2870,2871],{},"UserBuilder::new()->withName('Alice')->withRole('admin')->build()",") jest w porządku w testach, ale jeśli potrzebujesz buildera do konstruowania ",[15,2874,2875],{},"User"," w kodzie produkcyjnym, twój konstruktor ",[15,2878,2875],{}," prawdopodobnie robi za dużo.",[11,2881,2882,2885],{},[2836,2883,2884],{},"Abstract Factory"," rzadko jest konieczna w kodzie aplikacyjnym. Widziałem ją użytą poprawnie w bibliotece komponentów UI, która musiała przełączać się między jasnym a ciemnym motywem, produkując spójne komponenty button, input i modal bez wiedzy po stronie wywołującego, który motyw jest aktywny. W systemach backendowych kontener DI zazwyczaj zastępuje potrzebę abstract factory.",[11,2887,2888,2891,2892,2895,2896,2898,2899,2901,2902,2905,2906,2909],{},[2836,2889,2890],{},"Prototype",", przez dziesięć lat pisania produkcyjnego PHP potrzebowałem tego wzorca celowo jeden raz. Był to system szablonów dokumentów, gdzie kopiowanie struktury szablonu było wystarczająco kosztowne, żeby uzasadnić dedykowany interfejs ",[15,2893,2894],{},"clone",". W każdym innym przypadku ",[15,2897,2894],{}," działa bezpośrednio. Jeśli tworzysz interfejs ",[15,2900,2890],{}," z metodą ",[15,2903,2904],{},"copy()",", która po prostu wywołuje ",[15,2907,2908],{},"clone $this",", dodałeś warstwę pośrednią bez żadnej korzyści.",[27,2911,2913],{"id":2912},"wzorce-strukturalne-te-które-naprawdę-zarabiają","Wzorce strukturalne: te, które naprawdę zarabiają",[11,2915,2916,2919],{},[2836,2917,2918],{},"Adapter"," to wzorzec strukturalny o najwyższej wartości w kodzie aplikacyjnym. Każda integracja z zewnętrznym systemem, którą piszesz, jest adapterem: bierze interfejs zewnętrznego systemu i tłumaczy go na interfejs twojej domeny. Kluczowa dyscyplina: trzymaj adapter cienki. Jeśli twój adapter Stripe zawiera logikę biznesową o tym, kiedy ponawiać próby lub jak obliczać opłaty, to nie jest adapter, to serwis, który przy okazji wywołuje Stripe.",[38,2921,2923],{"className":59,"code":2922,"language":61,"meta":46,"style":46},"\u002F\u002F Thin adapter: translates types, nothing more\nfinal class StripePaymentGateway implements PaymentGatewayInterface\n{\n    public function __construct(private readonly \\Stripe\\StripeClient $stripe) {}\n\n    public function charge(Money $amount, string $currency): ChargeResult\n    {\n        try {\n            $intent = $this->stripe->paymentIntents->create([\n                'amount'   => $amount->getAmount(),   \u002F\u002F Stripe wants cents\n                'currency' => strtolower($currency),\n            ]);\n            return new ChargeResult(chargeId: $intent->id, status: ChargeStatus::Pending);\n        } catch (\\Stripe\\Exception\\CardException $e) {\n            return new ChargeResult(chargeId: null, status: ChargeStatus::Declined, error: $e->getMessage());\n        }\n    }\n}\n",[15,2924,2925,2930,2935,2939,2944,2948,2953,2957,2962,2967,2975,2980,2985,2990,2995,3000,3004,3008],{"__ignoreMap":46},[65,2926,2927],{"class":67,"line":68},[65,2928,2929],{},"\u002F\u002F Thin adapter: translates types, nothing more\n",[65,2931,2932],{"class":67,"line":74},[65,2933,2934],{},"final class StripePaymentGateway implements PaymentGatewayInterface\n",[65,2936,2937],{"class":67,"line":80},[65,2938,83],{},[65,2940,2941],{"class":67,"line":86},[65,2942,2943],{},"    public function __construct(private readonly \\Stripe\\StripeClient $stripe) {}\n",[65,2945,2946],{"class":67,"line":92},[65,2947,102],{"emptyLinePlaceholder":101},[65,2949,2950],{"class":67,"line":98},[65,2951,2952],{},"    public function charge(Money $amount, string $currency): ChargeResult\n",[65,2954,2955],{"class":67,"line":105},[65,2956,136],{},[65,2958,2959],{"class":67,"line":111},[65,2960,2961],{},"        try {\n",[65,2963,2964],{"class":67,"line":116},[65,2965,2966],{},"            $intent = $this->stripe->paymentIntents->create([\n",[65,2968,2969,2972],{"class":67,"line":122},[65,2970,2971],{},"                'amount'   => $amount->getAmount(),",[65,2973,2974],{},"   \u002F\u002F Stripe wants cents\n",[65,2976,2977],{"class":67,"line":127},[65,2978,2979],{},"                'currency' => strtolower($currency),\n",[65,2981,2982],{"class":67,"line":133},[65,2983,2984],{},"            ]);\n",[65,2986,2987],{"class":67,"line":139},[65,2988,2989],{},"            return new ChargeResult(chargeId: $intent->id, status: ChargeStatus::Pending);\n",[65,2991,2992],{"class":67,"line":145},[65,2993,2994],{},"        } catch (\\Stripe\\Exception\\CardException $e) {\n",[65,2996,2997],{"class":67,"line":151},[65,2998,2999],{},"            return new ChargeResult(chargeId: null, status: ChargeStatus::Declined, error: $e->getMessage());\n",[65,3001,3002],{"class":67,"line":157},[65,3003,647],{},[65,3005,3006],{"class":67,"line":163},[65,3007,172],{},[65,3009,3010],{"class":67,"line":169},[65,3011,95],{},[11,3013,3014,3017],{},[2836,3015,3016],{},"Decorator"," to wzorzec strukturalny, który najczęściej widzę nadmiernie skomplikowany. Dekorator dodaje zachowanie do obiektu bez zmiany jego interfejsu. Kanoniczne przypadki użycia w PHP: dekoratory cachujące, logujące, ograniczające ruch. To jest potężne i poprawne. Co widzę zamiast tego: łańcuchy dekoratorów siedem poziomów głęboko, gdzie debugowanie wymaga zrozumienia, który dekorator jest aktywny w którym kontekście i dlaczego.",[38,3019,3021],{"className":59,"code":3020,"language":61,"meta":46,"style":46},"\u002F\u002F Correct use: transparent caching\nfinal class CachingUserRepository implements UserRepositoryInterface\n{\n    public function __construct(\n        private readonly UserRepositoryInterface $inner,\n        private readonly CacheInterface $cache,\n        private readonly int $ttl = 300,\n    ) {}\n\n    public function findById(int $id): ?User\n    {\n        $key = \"user.{$id}\";\n        return $this->cache->remember($key, $this->ttl, fn() => $this->inner->findById($id));\n    }\n}\n",[15,3022,3023,3028,3033,3037,3041,3046,3051,3056,3060,3064,3069,3073,3078,3083,3087],{"__ignoreMap":46},[65,3024,3025],{"class":67,"line":68},[65,3026,3027],{},"\u002F\u002F Correct use: transparent caching\n",[65,3029,3030],{"class":67,"line":74},[65,3031,3032],{},"final class CachingUserRepository implements UserRepositoryInterface\n",[65,3034,3035],{"class":67,"line":80},[65,3036,83],{},[65,3038,3039],{"class":67,"line":86},[65,3040,342],{},[65,3042,3043],{"class":67,"line":92},[65,3044,3045],{},"        private readonly UserRepositoryInterface $inner,\n",[65,3047,3048],{"class":67,"line":98},[65,3049,3050],{},"        private readonly CacheInterface $cache,\n",[65,3052,3053],{"class":67,"line":105},[65,3054,3055],{},"        private readonly int $ttl = 300,\n",[65,3057,3058],{"class":67,"line":111},[65,3059,352],{},[65,3061,3062],{"class":67,"line":116},[65,3063,102],{"emptyLinePlaceholder":101},[65,3065,3066],{"class":67,"line":122},[65,3067,3068],{},"    public function findById(int $id): ?User\n",[65,3070,3071],{"class":67,"line":127},[65,3072,136],{},[65,3074,3075],{"class":67,"line":133},[65,3076,3077],{},"        $key = \"user.{$id}\";\n",[65,3079,3080],{"class":67,"line":139},[65,3081,3082],{},"        return $this->cache->remember($key, $this->ttl, fn() => $this->inner->findById($id));\n",[65,3084,3085],{"class":67,"line":145},[65,3086,172],{},[65,3088,3089],{"class":67,"line":151},[65,3090,95],{},[11,3092,3093,3094,3097,3098,3100],{},"Test dekoratora cachującego weryfikuje, że wywołuje ",[15,3095,3096],{},"inner"," przy chybieniu cache'a i pomija ",[15,3099,3096],{}," przy trafieniu. Test repozytorium bazodanowego weryfikuje dostęp do danych. Są niezależnie testowalne, niezależnie wdrażalne.",[11,3102,3103,3106,3107,3110],{},[2836,3104,3105],{},"Facade"," jest nadużywany jako plaster. Facade upraszcza złożony podsystem za pojedynczym interfejsem. Statyczne fasady Laravela (",[15,3108,3109],{},"DB::table('users')",") to opinionated implementacja tego wzorca. Poprawne użycie: gdy podsystem ma dziesięć klas, a wywołujący musi wchodzić w interakcję tylko z dwiema lub trzema operacjami. Nadużycie: owijanie pojedynczej klasy w facade, żeby uniknąć jej wstrzykiwania.",[11,3112,3113,3116],{},[2836,3114,3115],{},"Proxy"," najczęściej spotykamy w PHP przez generatory proxy do lazy-loadingu (Doctrine, Symfony). Budowanie własnego proxy jest rzadkie i zazwyczaj błędne. Jeśli potrzebujesz przechwytywać wywołania metod dla logowania, cachowania lub kontroli dostępu, dekorator jest prawie zawsze lepszym narzędziem, bo jest eksplicytny. Proxy przechwytujące wywołania transparentnie jest trudniejsze do testowania i trudniejsze do rozumowania.",[27,3118,3120],{"id":3119},"wzorce-behawioralne-tu-dzieje-się-większość-prawdziwej-pracy-projektowej","Wzorce behawioralne: tu dzieje się większość prawdziwej pracy projektowej",[11,3122,3123,3126,3127,3130,3131,3134,3135,3138,3139,3141],{},[2836,3124,3125],{},"Observer \u002F Event"," to wzorzec, który najczyściej skaluje się we współczesnym PHP, bo każdy framework ma dispatcher zdarzeń. Gdy ",[15,3128,3129],{},"Order"," przechodzi do stanu ",[15,3132,3133],{},"Paid",", dispatchuje ",[15,3136,3137],{},"OrderPaid",". Listener do emaila, listener do inventory i listener do analytics subskrybują niezależnie. Dodanie czwartego listenera nie wymaga zmian w ",[15,3140,3129],{}," ani w pozostałych trzech.",[11,3143,3144,3145,3147,3148,3147,3151,3147,3154,3147,3157,3147,3160,3147,3163,3166,3167,3169],{},"Tryb awarii: listenery zdarzeń z kaskadowymi efektami bez circuit breakera. Widziałem łańcuch, gdzie ",[15,3146,3137],{}," → ",[15,3149,3150],{},"ReserveInventory",[15,3152,3153],{},"InventoryLow",[15,3155,3156],{},"SendSupplierEmail",[15,3158,3159],{},"EmailDeliveryFailed",[15,3161,3162],{},"CreateAlertTask",[15,3164,3165],{},"AlertTaskCreated"," → pięć kolejnych listenerów. Oryginalne zdarzenie ",[15,3168,3137],{}," wyzwalało 47 zapytań bazodanowych w 9 klasach listenerów. Każde z osobna było rozsądne. Razem sprawiały, że każde zakończenie zamówienia zajmowało 800ms.",[11,3171,3172,3175],{},[2836,3173,3174],{},"Strategy"," to wzorzec, który najwyraźniej oddziela \"co zrobić\" od \"jak to zrobić.\" Kalkulator kosztów wysyłki, który wybiera między flat-rate, opartym na wadze i opartym na strefie na podstawie przewoźnika, używa Strategy poprawnie. Selekcja strategii powinna następować raz na request, nie wewnątrz gorącej ścieżki obliczeń.",[11,3177,3178,3181,3182,3185],{},[2836,3179,3180],{},"Command"," to wzorzec leżący u podstaw każdego nowoczesnego systemu kolejkowego. ",[15,3183,3184],{},"ChargeCustomerCommand"," to serializowalne, samowystarczalne opisanie intencji. Nie wykonuje niczego, opisuje to, co powinno zostać wykonane. Command bus (kolejka) pobiera go i dispatchuje do handlera. Wartość: komendy można opóźniać, ponawiać, audytować i odtwarzać w sposób, którego bezpośrednie wywołanie metody nie umożliwia.",[11,3187,3188,3191],{},[2836,3189,3190],{},"Template Method"," to wzorzec, po który sięgam najczęściej, nie zdając sobie z tego sprawy. Jeśli masz dwie klasy współdzielące 90% logiki i różniące się w jednym kroku (generator raportów formatujący identycznie, ale eksportujący do CSV lub PDF) klasa bazowa implementuje wspólną strukturę, a podklasa nadpisuje różniący się krok. To jest Template Method. Ostrzeżenie: dziedziczenie dla współdzielenia kodu jest w porządku; dziedziczenie dla polimorfizmu tam, gdzie sprawdziłaby się kompozycja, nie.",[27,3193,3195],{"id":3194},"te-dla-których-nie-znalazłem-prawdziwego-zastosowania","Te, dla których nie znalazłem prawdziwego zastosowania",[11,3197,3198,3201],{},[2836,3199,3200],{},"Interpreter"," to wzorzec zakładający budowanie parsera i ewaluatora dla własnego języka w aplikacyjnym PHP. Widziałem go raz, w silniku reguł dla systemu cennikowego. Był tam właściwym narzędziem. W pozostałych dwudziestu przypadkach, gdy go proponowano, prostszy ewaluator wyrażeń lub biblioteka jak Symfony ExpressionLanguage byłaby mniejszą ilością kodu i łatwiejsza w utrzymaniu.",[11,3203,3204,3207],{},[2836,3205,3206],{},"Mediator"," jest często opisywany jako \"Observer, ale obserwatorzy znają się nawzajem przez centralny broker.\" W praktyce dodana złożoność względem standardowego event dispatchera nie była uzasadniona w żadnym systemie, z którym pracowałem.",[11,3209,3210,3213],{},[2836,3211,3212],{},"Flyweight"," to optymalizacja pamięci dla dużej liczby drobnoziarnistych obiektów. Model pamięci PHP (ograniczony do requestu, izolowany procesami) sprawia, że rzadko jest konieczny. Widziałem go użyty poprawnie w parserze tworzącym tysiące obiektów tokenów, cachującym identyczne tokeny według wartości.",[27,3215,3217],{"id":3216},"pytanie-które-zadaję-przed-zastosowaniem-jakiegokolwiek-wzorca","Pytanie, które zadaję przed zastosowaniem jakiegokolwiek wzorca",[11,3219,3220],{},"Co ten wzorzec ułatwia do zmiany?",[11,3222,3223],{},"Dekorator ułatwia dodawanie lub usuwanie zachowań przekrojowych (cachowanie, logowanie) bez modyfikowania dekorowanej klasy. Strategy ułatwia dodanie nowego algorytmu bez modyfikowania kontekstu. Adapter ułatwia zamianę zewnętrznej zależności.",[11,3225,3226],{},"Jeśli odpowiedź brzmi \"nie jestem pewien, co ułatwia do zmiany, po prostu uważam, że to dobra architektura\", wzorzec prawdopodobnie rozwiązuje problem, którego jeszcze nie masz. Koszt abstrakcji jest realny i natychmiastowy. Korzyść jest hipotetyczna. Płać ten koszt wtedy, gdy korzyść jest równie realna.",[936,3228,938],{},{"title":46,"searchDepth":74,"depth":74,"links":3230},[3231,3232,3233,3234,3235],{"id":2831,"depth":74,"text":2832},{"id":2912,"depth":74,"text":2913},{"id":3119,"depth":74,"text":3120},{"id":3194,"depth":74,"text":3195},{"id":3216,"depth":74,"text":3217},"2023-02-10",{},"\u002Fpl\u002Farticles\u002Fdesign-patterns-production",{"x":3240,"y":3241,"depth":68,"size":3242},0.62,0.85,"lg",[3244,1154,959],"singleton-pattern",{"title":2817,"description":2822},"pattern-reference","pl\u002Farticles\u002Fdesign-patterns-production",[965,967,61,3249,3250],"refactoring","code-review","edMGvGCyylOgK3-BVgrWFdQg7QmUOINdGDmLSmx8Vxc",{"id":3253,"title":3254,"articleId":959,"body":3255,"category":61,"codeLang":61,"date":3826,"deploys":68,"description":3827,"excerpt":949,"extension":950,"lang":951,"meta":3828,"navigation":101,"path":2853,"pos":3829,"readMin":127,"related":3832,"seo":3833,"service":3834,"stem":3835,"tags":3836,"version":970,"__hash__":3839},"articles_pl\u002Fpl\u002Farticles\u002Ffactory-method.md","Factory Method: wzorzec, którego nikt nie potrzebuje, dopóki nie potrzebuje go do wszystkiego",{"type":8,"value":3256,"toc":3818},[3257,3282,3285,3289,3292,3295,3393,3404,3408,3547,3577,3580,3636,3639,3643,3650,3668,3672,3679,3682,3686,3689,3803,3807,3810,3816],[11,3258,3259,3260,3263,3264,2021,3267,3270,3271,3274,3275,2021,3278,3281],{},"Podręcznikowe przykłady Factory Method dotyczą kształtów i zwierząt. ",[15,3261,3262],{},"ShapeFactory"," zwracające ",[15,3265,3266],{},"Circle",[15,3268,3269],{},"Square"," na podstawie stringa. ",[15,3272,3273],{},"AnimalFactory"," konstruujące ",[15,3276,3277],{},"Dog",[15,3279,3280],{},"Cat",". Te przykłady są poprawne. Są też bezużyteczne jako wytyczne projektowe, bo w produkcji niczyja domena biznesowa nie dotyczy kształtów.",[11,3283,3284],{},"Wzorzec staje się ważny w momencie, gdy masz decyzję runtime o tym, którą implementację stworzyć, i punkt, w którym tę decyzję podejmujesz, nie powinien być rozsiany po całej bazie kodu.",[27,3286,3288],{"id":3287},"problem-z-bramką-płatności","Problem z bramką płatności",[11,3290,3291],{},"Obsługiwaliśmy cztery metody płatności: karta (Stripe), przelew bankowy (lokalny PSP), BLIK i finansowanie ratalne (integracja z podmiotem trzecim). Każda miała inne API, inne tryby błędów, inną semantykę retry, inne formaty webhooków.",[11,3293,3294],{},"Bez factory logika wyboru trafiła do kontrolera:",[38,3296,3298],{"className":59,"code":3297,"language":61,"meta":46,"style":46},"\u002F\u002F Before: selection logic in the wrong place\nclass PaymentController\n{\n    public function charge(Request $request): Response\n    {\n        $method = $request->input('payment_method');\n\n        if ($method === 'card') {\n            $gateway = new StripeGateway(config('stripe.secret'));\n        } elseif ($method === 'blik') {\n            $gateway = new BlikGateway(config('blik.merchant_id'), config('blik.api_key'));\n        } elseif ($method === 'transfer') {\n            $gateway = new BankTransferGateway(config('psp.endpoint'));\n        } else {\n            throw new \\InvalidArgumentException(\"Unknown payment method: {$method}\");\n        }\n\n        return $gateway->charge($request->input('amount'), $request->input('currency'));\n    }\n}\n",[15,3299,3300,3305,3310,3314,3319,3323,3328,3332,3337,3342,3347,3352,3357,3362,3367,3372,3376,3380,3385,3389],{"__ignoreMap":46},[65,3301,3302],{"class":67,"line":68},[65,3303,3304],{},"\u002F\u002F Before: selection logic in the wrong place\n",[65,3306,3307],{"class":67,"line":74},[65,3308,3309],{},"class PaymentController\n",[65,3311,3312],{"class":67,"line":80},[65,3313,83],{},[65,3315,3316],{"class":67,"line":86},[65,3317,3318],{},"    public function charge(Request $request): Response\n",[65,3320,3321],{"class":67,"line":92},[65,3322,136],{},[65,3324,3325],{"class":67,"line":98},[65,3326,3327],{},"        $method = $request->input('payment_method');\n",[65,3329,3330],{"class":67,"line":105},[65,3331,102],{"emptyLinePlaceholder":101},[65,3333,3334],{"class":67,"line":111},[65,3335,3336],{},"        if ($method === 'card') {\n",[65,3338,3339],{"class":67,"line":116},[65,3340,3341],{},"            $gateway = new StripeGateway(config('stripe.secret'));\n",[65,3343,3344],{"class":67,"line":122},[65,3345,3346],{},"        } elseif ($method === 'blik') {\n",[65,3348,3349],{"class":67,"line":127},[65,3350,3351],{},"            $gateway = new BlikGateway(config('blik.merchant_id'), config('blik.api_key'));\n",[65,3353,3354],{"class":67,"line":133},[65,3355,3356],{},"        } elseif ($method === 'transfer') {\n",[65,3358,3359],{"class":67,"line":139},[65,3360,3361],{},"            $gateway = new BankTransferGateway(config('psp.endpoint'));\n",[65,3363,3364],{"class":67,"line":145},[65,3365,3366],{},"        } else {\n",[65,3368,3369],{"class":67,"line":151},[65,3370,3371],{},"            throw new \\InvalidArgumentException(\"Unknown payment method: {$method}\");\n",[65,3373,3374],{"class":67,"line":157},[65,3375,647],{},[65,3377,3378],{"class":67,"line":163},[65,3379,102],{"emptyLinePlaceholder":101},[65,3381,3382],{"class":67,"line":169},[65,3383,3384],{},"        return $gateway->charge($request->input('amount'), $request->input('currency'));\n",[65,3386,3387],{"class":67,"line":175},[65,3388,172],{},[65,3390,3391],{"class":67,"line":180},[65,3392,95],{},[11,3394,3395,3396,3399,3400,3403],{},"To jest czytelne przy dwóch opcjach. Gdy dodaliśmy czwartą, ten sam łańcuch ",[15,3397,3398],{},"if-elseif"," istniał w kontrolerze, w handlerze zwrotów, w routerze webhooków i w adminowym jobie rekoncyliacji. Dodanie piątej bramki oznaczało znalezienie wszystkich czterech lokalizacji. Klasyczna architektura oparta na ślinie i trytytkach, gdzie trytyką był globalny search po ",[15,3401,3402],{},"elseif",".",[27,3405,3407],{"id":3406},"wyekstrahowana-factory","Wyekstrahowana factory",[38,3409,3411],{"className":59,"code":3410,"language":61,"meta":46,"style":46},"interface PaymentGatewayInterface\n{\n    public function charge(Money $amount, string $currency, array $metadata): ChargeResult;\n    public function refund(string $chargeId, Money $amount): RefundResult;\n    public function parseWebhook(array $payload, string $signature): WebhookEvent;\n}\n\nfinal class PaymentGatewayFactory\n{\n    private array $resolvers = [];\n\n    public function __construct(\n        private readonly ContainerInterface $container,\n    ) {}\n\n    public function register(string $method, string $gatewayClass): void\n    {\n        $this->resolvers[$method] = $gatewayClass;\n    }\n\n    public function make(string $method): PaymentGatewayInterface\n    {\n        if (!isset($this->resolvers[$method])) {\n            throw new UnsupportedPaymentMethodException($method);\n        }\n\n        \u002F\u002F Container resolves the gateway's own dependencies (credentials, HTTP client, logger)\n        return $this->container->make($this->resolvers[$method]);\n    }\n}\n",[15,3412,3413,3418,3422,3427,3432,3437,3441,3445,3450,3454,3459,3463,3467,3472,3476,3480,3485,3489,3494,3498,3502,3507,3511,3516,3521,3525,3529,3534,3539,3543],{"__ignoreMap":46},[65,3414,3415],{"class":67,"line":68},[65,3416,3417],{},"interface PaymentGatewayInterface\n",[65,3419,3420],{"class":67,"line":74},[65,3421,83],{},[65,3423,3424],{"class":67,"line":80},[65,3425,3426],{},"    public function charge(Money $amount, string $currency, array $metadata): ChargeResult;\n",[65,3428,3429],{"class":67,"line":86},[65,3430,3431],{},"    public function refund(string $chargeId, Money $amount): RefundResult;\n",[65,3433,3434],{"class":67,"line":92},[65,3435,3436],{},"    public function parseWebhook(array $payload, string $signature): WebhookEvent;\n",[65,3438,3439],{"class":67,"line":98},[65,3440,95],{},[65,3442,3443],{"class":67,"line":105},[65,3444,102],{"emptyLinePlaceholder":101},[65,3446,3447],{"class":67,"line":111},[65,3448,3449],{},"final class PaymentGatewayFactory\n",[65,3451,3452],{"class":67,"line":116},[65,3453,83],{},[65,3455,3456],{"class":67,"line":122},[65,3457,3458],{},"    private array $resolvers = [];\n",[65,3460,3461],{"class":67,"line":127},[65,3462,102],{"emptyLinePlaceholder":101},[65,3464,3465],{"class":67,"line":133},[65,3466,342],{},[65,3468,3469],{"class":67,"line":139},[65,3470,3471],{},"        private readonly ContainerInterface $container,\n",[65,3473,3474],{"class":67,"line":145},[65,3475,352],{},[65,3477,3478],{"class":67,"line":151},[65,3479,102],{"emptyLinePlaceholder":101},[65,3481,3482],{"class":67,"line":157},[65,3483,3484],{},"    public function register(string $method, string $gatewayClass): void\n",[65,3486,3487],{"class":67,"line":163},[65,3488,136],{},[65,3490,3491],{"class":67,"line":169},[65,3492,3493],{},"        $this->resolvers[$method] = $gatewayClass;\n",[65,3495,3496],{"class":67,"line":175},[65,3497,172],{},[65,3499,3500],{"class":67,"line":180},[65,3501,102],{"emptyLinePlaceholder":101},[65,3503,3504],{"class":67,"line":185},[65,3505,3506],{},"    public function make(string $method): PaymentGatewayInterface\n",[65,3508,3509],{"class":67,"line":191},[65,3510,136],{},[65,3512,3513],{"class":67,"line":196},[65,3514,3515],{},"        if (!isset($this->resolvers[$method])) {\n",[65,3517,3518],{"class":67,"line":202},[65,3519,3520],{},"            throw new UnsupportedPaymentMethodException($method);\n",[65,3522,3523],{"class":67,"line":207},[65,3524,647],{},[65,3526,3527],{"class":67,"line":212},[65,3528,102],{"emptyLinePlaceholder":101},[65,3530,3531],{"class":67,"line":217},[65,3532,3533],{},"        \u002F\u002F Container resolves the gateway's own dependencies (credentials, HTTP client, logger)\n",[65,3535,3536],{"class":67,"line":223},[65,3537,3538],{},"        return $this->container->make($this->resolvers[$method]);\n",[65,3540,3541],{"class":67,"line":229},[65,3542,172],{},[65,3544,3545],{"class":67,"line":235},[65,3546,95],{},[38,3548,3550],{"className":59,"code":3549,"language":61,"meta":46,"style":46},"\u002F\u002F Registered in a service provider — one place, one time\n$factory->register('card',     StripeGateway::class);\n$factory->register('blik',     BlikGateway::class);\n$factory->register('transfer', BankTransferGateway::class);\n$factory->register('financing',FinancingGateway::class);\n",[15,3551,3552,3557,3562,3567,3572],{"__ignoreMap":46},[65,3553,3554],{"class":67,"line":68},[65,3555,3556],{},"\u002F\u002F Registered in a service provider — one place, one time\n",[65,3558,3559],{"class":67,"line":74},[65,3560,3561],{},"$factory->register('card',     StripeGateway::class);\n",[65,3563,3564],{"class":67,"line":80},[65,3565,3566],{},"$factory->register('blik',     BlikGateway::class);\n",[65,3568,3569],{"class":67,"line":86},[65,3570,3571],{},"$factory->register('transfer', BankTransferGateway::class);\n",[65,3573,3574],{"class":67,"line":92},[65,3575,3576],{},"$factory->register('financing',FinancingGateway::class);\n",[11,3578,3579],{},"Kontroler staje się:",[38,3581,3583],{"className":59,"code":3582,"language":61,"meta":46,"style":46},"class PaymentController\n{\n    public function __construct(\n        private readonly PaymentGatewayFactory $factory,\n    ) {}\n\n    public function charge(Request $request): Response\n    {\n        $gateway = $this->factory->make($request->input('payment_method'));\n        return $gateway->charge(...);\n    }\n}\n",[15,3584,3585,3589,3593,3597,3602,3606,3610,3614,3618,3623,3628,3632],{"__ignoreMap":46},[65,3586,3587],{"class":67,"line":68},[65,3588,3309],{},[65,3590,3591],{"class":67,"line":74},[65,3592,83],{},[65,3594,3595],{"class":67,"line":80},[65,3596,342],{},[65,3598,3599],{"class":67,"line":86},[65,3600,3601],{},"        private readonly PaymentGatewayFactory $factory,\n",[65,3603,3604],{"class":67,"line":92},[65,3605,352],{},[65,3607,3608],{"class":67,"line":98},[65,3609,102],{"emptyLinePlaceholder":101},[65,3611,3612],{"class":67,"line":105},[65,3613,3318],{},[65,3615,3616],{"class":67,"line":111},[65,3617,136],{},[65,3619,3620],{"class":67,"line":116},[65,3621,3622],{},"        $gateway = $this->factory->make($request->input('payment_method'));\n",[65,3624,3625],{"class":67,"line":122},[65,3626,3627],{},"        return $gateway->charge(...);\n",[65,3629,3630],{"class":67,"line":127},[65,3631,172],{},[65,3633,3634],{"class":67,"line":133},[65,3635,95],{},[11,3637,3638],{},"Dodanie piątej bramki to: zaimplementuj interfejs, zarejestruj. Nie dotykasz niczego innego.",[27,3640,3642],{"id":3641},"dwa-tryby-awarii-w-implementacjach-factory","Dwa tryby awarii w implementacjach factory",[11,3644,3645,3646,3649],{},"Pierwszy to factory które konstruują zamiast rozwiązywać. Factory wywołujące ",[15,3647,3648],{},"new GatewayClass(config('...'))"," inline po cichu się psuje gdy bramka zyska nową zależność. Factory powinno delegować konstrukcję do kontenera DI. Jeśli nie jesteś na frameworku z kontenerem, minimum to żeby factory akceptowało instancje bramek przez konstruktor, nie budowało ich wewnętrznie.",[11,3651,3652,3653,3656,3657,3660,3661,927,3664,3667],{},"Drugi to factory które zwracają złą abstrakcję. Powyższe factory zwraca ",[15,3654,3655],{},"PaymentGatewayInterface",". Widziałem factory zwracające ",[15,3658,3659],{},"StripeGateway"," z interfejsem będącym de facto API Stripe (",[15,3662,3663],{},"createPaymentIntent()",[15,3665,3666],{},"retrieveBalance()",") z cienkimi wrapperami dla innych bramek, które sypią się pod obciążeniem, bo BLIK nie ma pojęcia o \"payment intent\". Interfejs powinien reprezentować słownik twojej domeny, nie API żadnego dostawcy.",[27,3669,3671],{"id":3670},"kiedy-używać-factory-a-kiedy-strategy","Kiedy używać factory a kiedy strategy",[11,3673,3674,3675,3678],{},"Pomylenie Factory Method i Strategy jest wystarczająco częste, żeby to bezpośrednio zaadresować. Wyglądają podobnie, ale rozwiązują różne problemy. Factory Method dotyczy tego, że typ obiektu się różni (tworzysz różne klasy na podstawie inputu runtime, caller nie trzyma referencji do factory, po prostu wywołuje ",[15,3676,3677],{},"make()"," i dostaje abstrakcję. Strategy dotyczy tego, że algorytm się różni) wstrzykujesz inną implementację tego samego interfejsu do klasy, która jej używa, klasa trzyma referencję do strategy i bezpośrednio wywołuje na niej metody.",[11,3680,3681],{},"W praktyce: jeśli wybór następuje raz na początku requestu i wynik jest używany przez cały czas (to factory. Jeśli wybór następuje wielokrotnie w ramach jednego obliczenia) to strategy.",[27,3683,3685],{"id":3684},"testowanie-factory","Testowanie factory",[11,3687,3688],{},"Sama factory ma trywialną powierzchnię testową: czy zwraca właściwy typ dla znanych inputów i czy rzuca dla nieznanych. Znaczące testy są na kontrakcie interfejsu, napisz współdzielony test, który każda bramka musi przejść:",[38,3690,3692],{"className":59,"code":3691,"language":61,"meta":46,"style":46},"abstract class PaymentGatewayContractTest extends TestCase\n{\n    abstract protected function makeGateway(): PaymentGatewayInterface;\n\n    public function testChargeReturnsChargeResult(): void\n    {\n        $gateway = $this->makeGateway();\n        $result  = $gateway->charge(\n            new Money(1000, 'PLN'),\n            'PLN',\n            ['order_id' => 'test-123']\n        );\n        $this->assertInstanceOf(ChargeResult::class, $result);\n        $this->assertNotEmpty($result->chargeId);\n    }\n}\n\nclass StripeGatewayTest extends PaymentGatewayContractTest\n{\n    protected function makeGateway(): PaymentGatewayInterface\n    {\n        return new StripeGateway(apiKey: 'sk_test_fake', httpClient: $this->mockClient());\n    }\n}\n",[15,3693,3694,3699,3703,3708,3712,3717,3721,3726,3731,3736,3741,3746,3750,3755,3760,3764,3768,3772,3777,3781,3786,3790,3795,3799],{"__ignoreMap":46},[65,3695,3696],{"class":67,"line":68},[65,3697,3698],{},"abstract class PaymentGatewayContractTest extends TestCase\n",[65,3700,3701],{"class":67,"line":74},[65,3702,83],{},[65,3704,3705],{"class":67,"line":80},[65,3706,3707],{},"    abstract protected function makeGateway(): PaymentGatewayInterface;\n",[65,3709,3710],{"class":67,"line":86},[65,3711,102],{"emptyLinePlaceholder":101},[65,3713,3714],{"class":67,"line":92},[65,3715,3716],{},"    public function testChargeReturnsChargeResult(): void\n",[65,3718,3719],{"class":67,"line":98},[65,3720,136],{},[65,3722,3723],{"class":67,"line":105},[65,3724,3725],{},"        $gateway = $this->makeGateway();\n",[65,3727,3728],{"class":67,"line":111},[65,3729,3730],{},"        $result  = $gateway->charge(\n",[65,3732,3733],{"class":67,"line":116},[65,3734,3735],{},"            new Money(1000, 'PLN'),\n",[65,3737,3738],{"class":67,"line":122},[65,3739,3740],{},"            'PLN',\n",[65,3742,3743],{"class":67,"line":127},[65,3744,3745],{},"            ['order_id' => 'test-123']\n",[65,3747,3748],{"class":67,"line":133},[65,3749,166],{},[65,3751,3752],{"class":67,"line":139},[65,3753,3754],{},"        $this->assertInstanceOf(ChargeResult::class, $result);\n",[65,3756,3757],{"class":67,"line":145},[65,3758,3759],{},"        $this->assertNotEmpty($result->chargeId);\n",[65,3761,3762],{"class":67,"line":151},[65,3763,172],{},[65,3765,3766],{"class":67,"line":157},[65,3767,95],{},[65,3769,3770],{"class":67,"line":163},[65,3771,102],{"emptyLinePlaceholder":101},[65,3773,3774],{"class":67,"line":169},[65,3775,3776],{},"class StripeGatewayTest extends PaymentGatewayContractTest\n",[65,3778,3779],{"class":67,"line":175},[65,3780,83],{},[65,3782,3783],{"class":67,"line":180},[65,3784,3785],{},"    protected function makeGateway(): PaymentGatewayInterface\n",[65,3787,3788],{"class":67,"line":185},[65,3789,136],{},[65,3791,3792],{"class":67,"line":191},[65,3793,3794],{},"        return new StripeGateway(apiKey: 'sk_test_fake', httpClient: $this->mockClient());\n",[65,3796,3797],{"class":67,"line":196},[65,3798,172],{},[65,3800,3801],{"class":67,"line":202},[65,3802,95],{},[27,3804,3806],{"id":3805},"na-co-zwracam-uwagę-w-code-review","Na co zwracam uwagę w code review",[11,3808,3809],{},"Gdy widzę factory, moje pierwsze pytanie brzmi: co wyzwala decyzję?",[11,3811,3812,3813,3815],{},"Jeśli odpowiedź to string z inputu użytkownika lub kolumna w bazie danych (poprawne użycie. Jeśli to stała kompilacji lub zmienna środowiskowa, która nigdy nie zmienia się w runtime) złe narzędzie, użyj kontenera DI do zbindowania konkretnego typu raz i wstrzyknij go bezpośrednio. Jeśli to rosnący łańcuch ",[15,3814,3398],{},", o którym zespół już jest nerwowy, factory jest spóźnione, ale nadal to właściwa naprawa.",[936,3817,938],{},{"title":46,"searchDepth":74,"depth":74,"links":3819},[3820,3821,3822,3823,3824,3825],{"id":3287,"depth":74,"text":3288},{"id":3406,"depth":74,"text":3407},{"id":3641,"depth":74,"text":3642},{"id":3670,"depth":74,"text":3671},{"id":3684,"depth":74,"text":3685},{"id":3805,"depth":74,"text":3806},"2024-07-15","Podręcznikowe przykłady Factory Method dotyczą kształtów i zwierząt. ShapeFactory zwracające Circle lub Square na podstawie stringa. AnimalFactory konstruujące Dog lub Cat. Te przykłady są poprawne. Są też bezużyteczne jako wytyczne projektowe, bo w produkcji niczyja domena biznesowa nie dotyczy kształtów.",{},{"x":3830,"y":3831,"depth":68,"size":950},0.58,0.48,[3244,1154],{"title":3254,"description":3827},"object-creation","pl\u002Farticles\u002Ffactory-method",[61,965,3837,3838,967],"factory","dependency-injection","Ls0CExpXhT37cUR6tso2B0biRyGJYU7n2rTU1KC2fYw",{"id":3841,"title":3842,"articleId":3843,"body":3844,"category":1144,"codeLang":61,"date":4437,"deploys":68,"description":3848,"excerpt":949,"extension":950,"lang":951,"meta":4438,"navigation":101,"path":4439,"pos":4440,"readMin":145,"related":4443,"seo":4444,"service":4445,"stem":4446,"tags":4447,"version":970,"__hash__":4452},"articles_pl\u002Fpl\u002Farticles\u002Fllm-in-php.md","LLMy w PHP: integracja modeli językowych z systemami produkcyjnymi bez przepisywania wszystkiego","llm-in-php",{"type":8,"value":3845,"toc":4429},[3846,3849,3852,3855,3859,3862,3865,3868,3871,3875,3878,3881,3936,4049,4052,4056,4059,4138,4205,4212,4216,4219,4372,4379,4383,4386,4411,4414,4417,4421,4424,4427],[11,3847,3848],{},"Każdy zespół, z którym rozmawiałem przez ostatnie dwa lata, przechodził tę samą rozmowę. Inżynierowie chcą dodać funkcje LLM, CTO mówi Python, a zespół platformowy, który jest właścicielem monolitu PHP z dziesięcioma latami logiki biznesowej, milknie. Argument brzmi, że narzędzia ML są Python-first, SDK dla LLMów są lepsze w Pythonie i tam jest pula talentów.",[11,3850,3851],{},"Ten argument jest w większości błędny, a zespoły, które na nim działają, spędzają sześć miesięcy budując mikroserwis Python, który wywołuje ich monolit PHP po logikę biznesową przez HTTP, wprowadzając granicę sieciową, dwa pipeline'y deploymentu i budżet latencji, którego nie planowały.",[11,3853,3854],{},"Tak wygląda integracja LLMów z produkcyjnym systemem PHP w praktyce, nie demo, ale system działający pod prawdziwym ruchem.",[27,3856,3858],{"id":3857},"krajobraz-php-llm","Krajobraz PHP + LLM",[11,3860,3861],{},"Ekosystem PHP ma trzy wiarygodne opcje integracji z LLMami. Bezpośrednie HTTP do API, OpenAI, Anthropic, Mistral udostępniają REST API, a klient HTTP i dekoder JSON to technicznie wszystko, czego potrzebujesz. Używałem tego do prostych completion w systemach, gdzie dodanie zależności było trudniejsze niż napisanie 40 linii kodu wrapper.",[11,3863,3864],{},"LLPhant to najbardziej kompletna biblioteka PHP do produkcyjnej pracy z LLMami. Opakowuje OpenAI i Anthropic, obsługuje streaming, implementuje wzorce RAG i wspiera function calling. To opcja, po którą teraz sięgam w nowych projektach PHP.",[11,3866,3867],{},"Integracja Symfony AI, dostarczona w Symfony 7.2, to first-party komponent z właściwym dependency injection, integracją z systemem eventów i szanowaniem konwencji frameworka. Jeśli jesteś na Symfony, to jest coraz lepsza odpowiedź.",[11,3869,3870],{},"Benchmark \"production-ready\" dla integracji z LLM to: czy obsługuje streaming poprawnie, czy wspiera function calling, czy pozwala wstrzyknąć obserwowalność i czy gracefully zawodzi gdy API zwraca 500. LLPhant spełnia wszystkie cztery.",[27,3872,3874],{"id":3873},"co-zrobiłem-źle-przy-pierwszym-deploymencie","Co zrobiłem źle przy pierwszym deploymencie",[11,3876,3877],{},"Nasza pierwsza integracja z LLMem to był system triage'u zgłoszeń supportu. Model czytał przychodzące tickety i klasyfikował je według pilności i działu. Kod PHP był czysty. Deployment był katastrofą.",[11,3879,3880],{},"Nie wzięliśmy pod uwagę latencji API w timeoucie queue workera. Wywołanie LLM uśredniało 3,2 sekundy. Domyślny timeout queue workera to 30 sekund. Pod burst loadem workery przetwarzające wiele ticketów jednocześnie osiągały timeout, job był retryowany, a my płaciliśmy API dwa razy za ten sam ticket, z różnymi klasyfikacjami, co psuło downstream logikę routingu.",[38,3882,3884],{"className":59,"code":3883,"language":61,"meta":46,"style":46},"\u002F\u002F Co mieliśmy:\nclass TicketTriageJob implements ShouldQueue\n{\n    public $timeout = 30;  \u002F\u002F default — nie myśleliśmy o latencji LLM\n\n    public function handle(LLMClient $client): void\n    {\n        $classification = $client->classify($this->ticket->body);\n        $this->ticket->update(['department' => $classification->department]);\n    }\n}\n",[15,3885,3886,3891,3896,3900,3905,3909,3914,3918,3923,3928,3932],{"__ignoreMap":46},[65,3887,3888],{"class":67,"line":68},[65,3889,3890],{},"\u002F\u002F Co mieliśmy:\n",[65,3892,3893],{"class":67,"line":74},[65,3894,3895],{},"class TicketTriageJob implements ShouldQueue\n",[65,3897,3898],{"class":67,"line":80},[65,3899,83],{},[65,3901,3902],{"class":67,"line":86},[65,3903,3904],{},"    public $timeout = 30;  \u002F\u002F default — nie myśleliśmy o latencji LLM\n",[65,3906,3907],{"class":67,"line":92},[65,3908,102],{"emptyLinePlaceholder":101},[65,3910,3911],{"class":67,"line":98},[65,3912,3913],{},"    public function handle(LLMClient $client): void\n",[65,3915,3916],{"class":67,"line":105},[65,3917,136],{},[65,3919,3920],{"class":67,"line":111},[65,3921,3922],{},"        $classification = $client->classify($this->ticket->body);\n",[65,3924,3925],{"class":67,"line":116},[65,3926,3927],{},"        $this->ticket->update(['department' => $classification->department]);\n",[65,3929,3930],{"class":67,"line":122},[65,3931,172],{},[65,3933,3934],{"class":67,"line":127},[65,3935,95],{},[38,3937,3939],{"className":59,"code":3938,"language":61,"meta":46,"style":46},"\u002F\u002F Co było potrzebne:\nclass TicketTriageJob implements ShouldQueue\n{\n    public $timeout = 120;       \u002F\u002F wywołanie LLM + overhead przetwarzania\n    public $tries = 1;           \u002F\u002F nigdy nie retry — wywołania LLM nie są idempotentne\n    public $uniqueFor = 3600;    \u002F\u002F zapobiegaj duplikatom przetwarzania\n\n    public function handle(LLMClient $client): void\n    {\n        if ($this->ticket->fresh()->triaged_at !== null) {\n            return;  \u002F\u002F już przetworzone przez poprzednią próbę\n        }\n\n        $classification = $client->classify($this->ticket->body);\n\n        DB::transaction(function () use ($classification) {\n            $this->ticket->update([\n                'department'  => $classification->department,\n                'priority'    => $classification->priority,\n                'triaged_at'  => now(),\n            ]);\n        });\n    }\n}\n",[15,3940,3941,3946,3950,3954,3959,3964,3969,3973,3977,3981,3986,3991,3995,3999,4003,4007,4012,4017,4022,4027,4032,4036,4041,4045],{"__ignoreMap":46},[65,3942,3943],{"class":67,"line":68},[65,3944,3945],{},"\u002F\u002F Co było potrzebne:\n",[65,3947,3948],{"class":67,"line":74},[65,3949,3895],{},[65,3951,3952],{"class":67,"line":80},[65,3953,83],{},[65,3955,3956],{"class":67,"line":86},[65,3957,3958],{},"    public $timeout = 120;       \u002F\u002F wywołanie LLM + overhead przetwarzania\n",[65,3960,3961],{"class":67,"line":92},[65,3962,3963],{},"    public $tries = 1;           \u002F\u002F nigdy nie retry — wywołania LLM nie są idempotentne\n",[65,3965,3966],{"class":67,"line":98},[65,3967,3968],{},"    public $uniqueFor = 3600;    \u002F\u002F zapobiegaj duplikatom przetwarzania\n",[65,3970,3971],{"class":67,"line":105},[65,3972,102],{"emptyLinePlaceholder":101},[65,3974,3975],{"class":67,"line":111},[65,3976,3913],{},[65,3978,3979],{"class":67,"line":116},[65,3980,136],{},[65,3982,3983],{"class":67,"line":122},[65,3984,3985],{},"        if ($this->ticket->fresh()->triaged_at !== null) {\n",[65,3987,3988],{"class":67,"line":127},[65,3989,3990],{},"            return;  \u002F\u002F już przetworzone przez poprzednią próbę\n",[65,3992,3993],{"class":67,"line":133},[65,3994,647],{},[65,3996,3997],{"class":67,"line":139},[65,3998,102],{"emptyLinePlaceholder":101},[65,4000,4001],{"class":67,"line":145},[65,4002,3922],{},[65,4004,4005],{"class":67,"line":151},[65,4006,102],{"emptyLinePlaceholder":101},[65,4008,4009],{"class":67,"line":157},[65,4010,4011],{},"        DB::transaction(function () use ($classification) {\n",[65,4013,4014],{"class":67,"line":163},[65,4015,4016],{},"            $this->ticket->update([\n",[65,4018,4019],{"class":67,"line":169},[65,4020,4021],{},"                'department'  => $classification->department,\n",[65,4023,4024],{"class":67,"line":175},[65,4025,4026],{},"                'priority'    => $classification->priority,\n",[65,4028,4029],{"class":67,"line":180},[65,4030,4031],{},"                'triaged_at'  => now(),\n",[65,4033,4034],{"class":67,"line":185},[65,4035,2984],{},[65,4037,4038],{"class":67,"line":191},[65,4039,4040],{},"        });\n",[65,4042,4043],{"class":67,"line":196},[65,4044,172],{},[65,4046,4047],{"class":67,"line":202},[65,4048,95],{},[11,4050,4051],{},"Nieidempotentność wywołań LLM to coś, co zespoły konsekwentnie niedoceniają. Model nie zwraca tego samego outputu dla tego samego inputu, a ponowne uruchomienie klasyfikacji po częściowej awarii nie jest bezpieczne, jeśli systemy downstream już zareagowały na pierwszy wynik.",[27,4053,4055],{"id":4054},"rag-w-produkcji-indeks-jest-produktem","RAG w produkcji: indeks jest produktem",[11,4057,4058],{},"Retrieval-augmented generation to miejsce, gdzie integracje PHP z LLMem stają się interesujące i gdzie luka z Pythonem kurczy się prawie do zera. Ciężka praca (generowanie embeddingów, storage wektorowy, wyszukiwanie podobieństwa) dzieje się w czasie indeksowania, nie w czasie zapytania. W momencie zapytania wykonujesz wywołanie HTTP i zapytanie do bazy danych.",[38,4060,4062],{"className":59,"code":4061,"language":61,"meta":46,"style":46},"use LLPhant\\Embeddings\\EmbeddingGenerator\\OpenAI\\OpenAI3LargeEmbeddingGenerator;\nuse LLPhant\\Embeddings\\VectorStores\\Doctrine\\DoctrineVectorStore;\n\n\u002F\u002F Indeksowanie (uruchom raz lub przy aktualizacji treści)\n$generator  = new OpenAI3LargeEmbeddingGenerator();\n$vectorStore = new DoctrineVectorStore($entityManager, DocumentChunk::class);\n\nforeach ($documents as $doc) {\n    $chunks = $splitter->splitDocument($doc, chunkSize: 512, overlap: 64);\n\n    foreach ($chunks as $chunk) {\n        $chunk->embedding = $generator->embedText($chunk->content);\n    }\n\n    $vectorStore->addDocuments($chunks);\n}\n",[15,4063,4064,4069,4074,4078,4083,4088,4093,4097,4102,4107,4111,4116,4121,4125,4129,4134],{"__ignoreMap":46},[65,4065,4066],{"class":67,"line":68},[65,4067,4068],{},"use LLPhant\\Embeddings\\EmbeddingGenerator\\OpenAI\\OpenAI3LargeEmbeddingGenerator;\n",[65,4070,4071],{"class":67,"line":74},[65,4072,4073],{},"use LLPhant\\Embeddings\\VectorStores\\Doctrine\\DoctrineVectorStore;\n",[65,4075,4076],{"class":67,"line":80},[65,4077,102],{"emptyLinePlaceholder":101},[65,4079,4080],{"class":67,"line":86},[65,4081,4082],{},"\u002F\u002F Indeksowanie (uruchom raz lub przy aktualizacji treści)\n",[65,4084,4085],{"class":67,"line":92},[65,4086,4087],{},"$generator  = new OpenAI3LargeEmbeddingGenerator();\n",[65,4089,4090],{"class":67,"line":98},[65,4091,4092],{},"$vectorStore = new DoctrineVectorStore($entityManager, DocumentChunk::class);\n",[65,4094,4095],{"class":67,"line":105},[65,4096,102],{"emptyLinePlaceholder":101},[65,4098,4099],{"class":67,"line":111},[65,4100,4101],{},"foreach ($documents as $doc) {\n",[65,4103,4104],{"class":67,"line":116},[65,4105,4106],{},"    $chunks = $splitter->splitDocument($doc, chunkSize: 512, overlap: 64);\n",[65,4108,4109],{"class":67,"line":122},[65,4110,102],{"emptyLinePlaceholder":101},[65,4112,4113],{"class":67,"line":127},[65,4114,4115],{},"    foreach ($chunks as $chunk) {\n",[65,4117,4118],{"class":67,"line":133},[65,4119,4120],{},"        $chunk->embedding = $generator->embedText($chunk->content);\n",[65,4122,4123],{"class":67,"line":139},[65,4124,172],{},[65,4126,4127],{"class":67,"line":145},[65,4128,102],{"emptyLinePlaceholder":101},[65,4130,4131],{"class":67,"line":151},[65,4132,4133],{},"    $vectorStore->addDocuments($chunks);\n",[65,4135,4136],{"class":67,"line":157},[65,4137,95],{},[38,4139,4141],{"className":59,"code":4140,"language":61,"meta":46,"style":46},"\u002F\u002F Czas zapytania (per request użytkownika)\n$query     = $request->input('question');\n$embedding = $generator->embedText($query);\n\n\u002F\u002F pgvector cosine similarity — pojedyncze zapytanie, \u003C 20ms na zindeksowanych danych\n$relevant  = $vectorStore->similaritySearch($embedding, maxResults: 5, minScore: 0.78);\n\n$context   = implode(\"\\n\\n\", array_map(fn($c) => $c->content, $relevant));\n\n$answer = $llm->chat([\n    ['role' => 'system',    'content' => \"Odpowiadaj używając tylko dostarczonego kontekstu.\\n\\n{$context}\"],\n    ['role' => 'user',      'content' => $query],\n]);\n",[15,4142,4143,4148,4153,4158,4162,4167,4172,4176,4181,4185,4190,4195,4200],{"__ignoreMap":46},[65,4144,4145],{"class":67,"line":68},[65,4146,4147],{},"\u002F\u002F Czas zapytania (per request użytkownika)\n",[65,4149,4150],{"class":67,"line":74},[65,4151,4152],{},"$query     = $request->input('question');\n",[65,4154,4155],{"class":67,"line":80},[65,4156,4157],{},"$embedding = $generator->embedText($query);\n",[65,4159,4160],{"class":67,"line":86},[65,4161,102],{"emptyLinePlaceholder":101},[65,4163,4164],{"class":67,"line":92},[65,4165,4166],{},"\u002F\u002F pgvector cosine similarity — pojedyncze zapytanie, \u003C 20ms na zindeksowanych danych\n",[65,4168,4169],{"class":67,"line":98},[65,4170,4171],{},"$relevant  = $vectorStore->similaritySearch($embedding, maxResults: 5, minScore: 0.78);\n",[65,4173,4174],{"class":67,"line":105},[65,4175,102],{"emptyLinePlaceholder":101},[65,4177,4178],{"class":67,"line":111},[65,4179,4180],{},"$context   = implode(\"\\n\\n\", array_map(fn($c) => $c->content, $relevant));\n",[65,4182,4183],{"class":67,"line":116},[65,4184,102],{"emptyLinePlaceholder":101},[65,4186,4187],{"class":67,"line":122},[65,4188,4189],{},"$answer = $llm->chat([\n",[65,4191,4192],{"class":67,"line":127},[65,4193,4194],{},"    ['role' => 'system',    'content' => \"Odpowiadaj używając tylko dostarczonego kontekstu.\\n\\n{$context}\"],\n",[65,4196,4197],{"class":67,"line":133},[65,4198,4199],{},"    ['role' => 'user',      'content' => $query],\n",[65,4201,4202],{"class":67,"line":139},[65,4203,4204],{},"]);\n",[11,4206,4207,4208,4211],{},"Próg podobieństwa 0,78 nie jest domyślny, jest dostrojony. Zbyt niski i pobierasz nieistotny kontekst, który myli model. Zbyt wysoki i nie pobierasz niczego. Uruchomiliśmy 200 przykładowych zapytań na holdout odpowiedziach i zmierzyliśmy recall przy różnych progach przed wdrożeniem. ",[15,4209,4210],{},"0.78"," było punktem, gdzie recall był stabilny, a halucynacje spadły do akceptowalnego poziomu.",[27,4213,4215],{"id":4214},"function-calling-gdzie-php-pasuje-lepiej-niż-się-spodziewasz","Function calling: gdzie PHP pasuje lepiej niż się spodziewasz",[11,4217,4218],{},"Function calling (model decydujący o wywołaniu narzędzia i zwracający ustrukturyzowane argumenty) to podstawowy mechanizm, który czyni agentów LLM praktycznymi. PHP dobrze się do tego nadaje, bo \"narzędzia\" to zazwyczaj istniejąca logika domenowa: pobierz klienta, sprawdź status zamówienia, wykonaj obliczenie. Masz już ten kod.",[38,4220,4222],{"className":59,"code":4221,"language":61,"meta":46,"style":46},"$tools = [\n    Tool::create('get_order_status')\n        ->description('Zwraca aktualny status i ETA dla podanego ID zamówienia')\n        ->parameter('order_id', 'string', 'UUID zamówienia', required: true),\n\n    Tool::create('calculate_refund')\n        ->description('Oblicza kwotę zwrotu na podstawie ID zamówienia i powodu')\n        ->parameter('order_id', 'string', required: true)\n        ->parameter('reason', 'string', 'cancellation | defect | not_received', required: true),\n];\n\n$response = $llm->chat($messages, tools: $tools);\n\n\u002F\u002F Model może zwrócić wywołanie narzędzia zamiast tekstu\nwhile ($response->hasToolCalls()) {\n    foreach ($response->toolCalls() as $call) {\n        $result = match ($call->name) {\n            'get_order_status'  => $orderService->getStatus($call->arguments['order_id']),\n            'calculate_refund'  => $refundCalculator->calculate(\n                $call->arguments['order_id'],\n                $call->arguments['reason']\n            ),\n            default => throw new UnknownToolException($call->name),\n        };\n\n        \u002F\u002F Przekaż wynik narzędzia z powrotem do konwersacji\n        $messages[] = ['role' => 'tool', 'tool_call_id' => $call->id, 'content' => json_encode($result)];\n    }\n\n    $response = $llm->chat($messages, tools: $tools);\n}\n",[15,4223,4224,4229,4234,4239,4244,4248,4253,4258,4263,4268,4273,4277,4282,4286,4291,4296,4301,4306,4311,4316,4321,4326,4331,4336,4341,4345,4350,4355,4359,4363,4368],{"__ignoreMap":46},[65,4225,4226],{"class":67,"line":68},[65,4227,4228],{},"$tools = [\n",[65,4230,4231],{"class":67,"line":74},[65,4232,4233],{},"    Tool::create('get_order_status')\n",[65,4235,4236],{"class":67,"line":80},[65,4237,4238],{},"        ->description('Zwraca aktualny status i ETA dla podanego ID zamówienia')\n",[65,4240,4241],{"class":67,"line":86},[65,4242,4243],{},"        ->parameter('order_id', 'string', 'UUID zamówienia', required: true),\n",[65,4245,4246],{"class":67,"line":92},[65,4247,102],{"emptyLinePlaceholder":101},[65,4249,4250],{"class":67,"line":98},[65,4251,4252],{},"    Tool::create('calculate_refund')\n",[65,4254,4255],{"class":67,"line":105},[65,4256,4257],{},"        ->description('Oblicza kwotę zwrotu na podstawie ID zamówienia i powodu')\n",[65,4259,4260],{"class":67,"line":111},[65,4261,4262],{},"        ->parameter('order_id', 'string', required: true)\n",[65,4264,4265],{"class":67,"line":116},[65,4266,4267],{},"        ->parameter('reason', 'string', 'cancellation | defect | not_received', required: true),\n",[65,4269,4270],{"class":67,"line":122},[65,4271,4272],{},"];\n",[65,4274,4275],{"class":67,"line":127},[65,4276,102],{"emptyLinePlaceholder":101},[65,4278,4279],{"class":67,"line":133},[65,4280,4281],{},"$response = $llm->chat($messages, tools: $tools);\n",[65,4283,4284],{"class":67,"line":139},[65,4285,102],{"emptyLinePlaceholder":101},[65,4287,4288],{"class":67,"line":145},[65,4289,4290],{},"\u002F\u002F Model może zwrócić wywołanie narzędzia zamiast tekstu\n",[65,4292,4293],{"class":67,"line":151},[65,4294,4295],{},"while ($response->hasToolCalls()) {\n",[65,4297,4298],{"class":67,"line":157},[65,4299,4300],{},"    foreach ($response->toolCalls() as $call) {\n",[65,4302,4303],{"class":67,"line":163},[65,4304,4305],{},"        $result = match ($call->name) {\n",[65,4307,4308],{"class":67,"line":169},[65,4309,4310],{},"            'get_order_status'  => $orderService->getStatus($call->arguments['order_id']),\n",[65,4312,4313],{"class":67,"line":175},[65,4314,4315],{},"            'calculate_refund'  => $refundCalculator->calculate(\n",[65,4317,4318],{"class":67,"line":180},[65,4319,4320],{},"                $call->arguments['order_id'],\n",[65,4322,4323],{"class":67,"line":185},[65,4324,4325],{},"                $call->arguments['reason']\n",[65,4327,4328],{"class":67,"line":191},[65,4329,4330],{},"            ),\n",[65,4332,4333],{"class":67,"line":196},[65,4334,4335],{},"            default => throw new UnknownToolException($call->name),\n",[65,4337,4338],{"class":67,"line":202},[65,4339,4340],{},"        };\n",[65,4342,4343],{"class":67,"line":207},[65,4344,102],{"emptyLinePlaceholder":101},[65,4346,4347],{"class":67,"line":212},[65,4348,4349],{},"        \u002F\u002F Przekaż wynik narzędzia z powrotem do konwersacji\n",[65,4351,4352],{"class":67,"line":217},[65,4353,4354],{},"        $messages[] = ['role' => 'tool', 'tool_call_id' => $call->id, 'content' => json_encode($result)];\n",[65,4356,4357],{"class":67,"line":223},[65,4358,172],{},[65,4360,4361],{"class":67,"line":229},[65,4362,102],{"emptyLinePlaceholder":101},[65,4364,4365],{"class":67,"line":235},[65,4366,4367],{},"    $response = $llm->chat($messages, tools: $tools);\n",[65,4369,4370],{"class":67,"line":241},[65,4371,95],{},[11,4373,4374,4375,4378],{},"Pętla ",[15,4376,4377],{},"while"," obsługuje wieloetapowe użycie narzędzi. W praktyce większość produkcyjnych agentów wykonuje 1–3 wywołania narzędzi per tura rozmowy. Więcej niż to i latencja staje się dominującym problemem UX.",[27,4380,4382],{"id":4381},"obserwowalność-której-faktycznie-potrzebujesz","Obserwowalność, której faktycznie potrzebujesz",[11,4384,4385],{},"Trzy metryki, które śledzę dla każdej integracji z LLM. Zużycie tokenów per endpoint, koszty LLM skalują się z tokenami, nie requestami. Jeden endpoint przekazujący 10 000-tokenowy system prompt przy każdym wywołaniu zdominuje twój rachunek API w ciągu dni.",[38,4387,4389],{"className":59,"code":4388,"language":61,"meta":46,"style":46},"\u002F\u002F Po każdym wywołaniu LLM\n$this->metrics->increment('llm.tokens.prompt',     $response->usage()->promptTokens);\n$this->metrics->increment('llm.tokens.completion', $response->usage()->completionTokens);\n$this->metrics->timing('llm.latency_ms',           $response->latencyMs());\n",[15,4390,4391,4396,4401,4406],{"__ignoreMap":46},[65,4392,4393],{"class":67,"line":68},[65,4394,4395],{},"\u002F\u002F Po każdym wywołaniu LLM\n",[65,4397,4398],{"class":67,"line":74},[65,4399,4400],{},"$this->metrics->increment('llm.tokens.prompt',     $response->usage()->promptTokens);\n",[65,4402,4403],{"class":67,"line":80},[65,4404,4405],{},"$this->metrics->increment('llm.tokens.completion', $response->usage()->completionTokens);\n",[65,4407,4408],{"class":67,"line":86},[65,4409,4410],{},"$this->metrics->timing('llm.latency_ms',           $response->latencyMs());\n",[11,4412,4413],{},"Dla ustrukturyzowanych outputów (klasyfikacje, wywołania funkcji, ekstrakcja JSON) model będzie okazjonalnie zwracał malformed output. Śledź wskaźnik błędów parsowania. Jeśli przekroczy 2%, twój prompt degraduje się, model był po cichu zaktualizowany, albo dystrybucja inputu się przesunęła.",[11,4415,4416],{},"Jeśli wykonujesz pracę LLM w background jobach, głębokość kolejki jest leading indicator tego, czy twoja liczba workerów nadąża za wolumenem requestów. Obserwuj ją zanim stanie się pagerem.",[27,4418,4420],{"id":4419},"pytanie-o-przepisywanie-odpowiedziane-uczciwie","Pytanie o przepisywanie, odpowiedziane uczciwie",[11,4422,4423],{},"Czy Python jest lepszy do pracy z LLMami? Do czystych badań ML, trenowania i fine-tuningu, tak, bez wątpienia. Do budowania funkcji rozszerzonych o LLM na istniejącym systemie PHP: luka jest mniejsza niż koszt migracji w prawie każdym przypadku, który oceniałem.",[11,4425,4426],{},"Pytanie nie brzmi \"który język jest lepszy do LLMów\", ale \"gdzie mieszka logika biznesowa, na której LLM musi działać?\" Jeśli jest w systemie PHP z dziesięcioma latami modelowania domeny, nie zduplikujesz tego w sześć miesięcy w nowym serwisie Python. Skończyć z cienkim wrapperem Python wywołującym twoje API PHP, i zapłacisz pełną cenę za przepisanie, nie zyskując niczego, czego LLPhant nie mógłby zrobić bezpośrednio z PHP.",[936,4428,938],{},{"title":46,"searchDepth":74,"depth":74,"links":4430},[4431,4432,4433,4434,4435,4436],{"id":3857,"depth":74,"text":3858},{"id":3873,"depth":74,"text":3874},{"id":4054,"depth":74,"text":4055},{"id":4214,"depth":74,"text":4215},{"id":4381,"depth":74,"text":4382},{"id":4419,"depth":74,"text":4420},"2024-10-28",{},"\u002Fpl\u002Farticles\u002Fllm-in-php",{"x":4441,"y":956,"depth":4442,"size":3242},0.47,1.3,[976,3244],{"title":3842,"description":3848},"llm-integration","pl\u002Farticles\u002Fllm-in-php",[61,4448,1160,4449,4450,4451],"llm","llphant","openai","production","HDin2-VabAlgwf5tRaDuBwx-EJcULJ-oc77KnnhOt64",{"id":4454,"title":4455,"articleId":1154,"body":4456,"category":4572,"codeLang":43,"date":4573,"deploys":68,"description":4460,"excerpt":949,"extension":950,"lang":951,"meta":4574,"navigation":101,"path":4575,"pos":4576,"readMin":191,"related":4580,"seo":4581,"service":4582,"stem":4583,"tags":4584,"version":4588,"__hash__":4589},"articles_pl\u002Fpl\u002Farticles\u002Fmicroservice-cost.md","Ukryty koszt granic mikroserwisów: pięcioletnia retrospekcja",{"type":8,"value":4457,"toc":4565},[4458,4461,4464,4468,4471,4474,4477,4481,4484,4490,4512,4522,4526,4529,4532,4535,4539,4546,4549,4553,4556,4562],[11,4459,4460],{},"W 2021 roku narysowaliśmy 47 pudełek na tablicy. Liczba pochodziła z instytutu badań z dupy: nikt niczego nie mierzył, po prostu wydało nam się, że 47 to mniej więcej tyle ile trzeba na rozmiar domeny. W 2024 roku 22 z tych pudełek były z powrotem wewnątrz innych pudełek. To nie jest historia o tym, że mikroserwisy są złe. To historia o tym, że granica między dwoma serwisami jest najdroższą rzeczą w systemie do zmiany, i że tego jeszcze nie wiedzieliśmy.",[11,4462,4463],{},"Jedyne pytanie, które teraz zadaję przed narysowaniem linii: \"jaka jest najmniejsza zmiana, która musi przekraczać tę granicę, i jak często będziemy ją robić?\" Jeśli odpowiedź brzmi \"co sprint, w lockstepie\", tej linii nie powinno być. Nie ma architektury wystarczająco sprytnej, żeby tę linię uczynić tanią.",[27,4465,4467],{"id":4466},"co-myśleliśmy-że-kupujemy","Co myśleliśmy, że kupujemy",[11,4469,4470],{},"Mieliśmy monolit. Deployował się wolno, testował wolno, własność jednego zespołu, obawiano się go wszędzie. Pitch dla mikroserwisów brzmiał: niezależność, zespoły mogą deployować swoje serwisy we własnym rytmie, własnym językiem, własną bazą danych. Overhead koordynacji monolitu miał zniknąć.",[11,4472,4473],{},"Przez pierwsze 18 miesięcy działało. Zespoły poruszały się szybciej. Serwisy deployowały bez koordynacji. Burden on-calla się rozłożył. Promień incydentu się zmniejszył.",[11,4475,4476],{},"Potem zaczęliśmy budować featury.",[27,4478,4480],{"id":4479},"framework-kosztu-granicy","Framework kosztu granicy",[11,4482,4483],{},"Każda granica między serwisami ma koszt. Nie jest stały, skaluje się wraz z tym, jak często trzeba zmieniać obie strony jednocześnie.",[38,4485,4488],{"className":4486,"code":4487,"language":43,"meta":46},[41],"# framework kosztu granicy, na odwrocie serwetki\n\n  cost(granica) =\n        f_change  *  cost_per_change\n      + f_failure *  blast_radius\n      - autonomy_gained\n\n# jeśli dominuje pierwszy składnik, granica jest w złym miejscu.\n# jeśli dominuje drugi — granica jest OK, inwestuj w izolację awarii.\n# jeśli dominuje trzeci — shippuj.\n",[15,4489,4487],{"__ignoreMap":46},[11,4491,4492,4495,4496,4499,4500,4503,4504,4507,4508,4511],{},[15,4493,4494],{},"f_change"," to częstotliwość skoordynowanych zmian przez granicę. ",[15,4497,4498],{},"cost_per_change"," to overhead: wersjonowanie, contract testy, koordynacja deployów, osobne kolejki PR. ",[15,4501,4502],{},"f_failure"," to jak często awaria jednego serwisu dotyka drugiego. ",[15,4505,4506],{},"blast_radius"," to skala tej awarii. ",[15,4509,4510],{},"autonomy_gained"," to faktyczna niezależność na poziomie zespołu, którą granica umożliwia.",[11,4513,4514,4515,748,4518,4521],{},"W naszym przypadku 22 z 47 serwisów miało ",[15,4516,4517],{},"f_change > 1 per sprint",[15,4519,4520],{},"autonomy_gained ≈ 0",", bo należały do tego samego zespołu. Granica dodawała overhead bez żadnej korzyści. Z dupy driven development w czystej postaci: architektura wyglądała świetnie na slajdzie konferencyjnym i kosztowała nas spotkanie koordynacyjne deployów każdego czwartku.",[27,4523,4525],{"id":4524},"_12-które-przetrwało-niezmienione","12, które przetrwało niezmienione",[11,4527,4528],{},"Serwisy, które zachowały niezależną wartość po pięciu latach, miały jedną wspólną właściwość: należały do zespołów z naprawdę różnym rytmem deploymentu.",[11,4530,4531],{},"Serwis płatności deployuje się codziennie. Serwis modelu wykrywania fraudów deployuje się co sześć tygodni (gdy nowy model jest zwalidowany). Granica między nimi jest wartościowa, bo pozwala płatnościom shippować bez czekania na walidację modelu fraudów.",[11,4533,4534],{},"Serwis notyfikacji deployuje się gdy zmieniają się szablony emaili. Serwis profilu klienta deployuje się z każdym sprintem produktowym. Różne rytmy, różne tryby awarii. Granica zasługuje na swój koszt.",[27,4536,4538],{"id":4537},"scalanie-z-powrotem","Scalanie z powrotem",[11,4540,4541,4542,4545],{},"Proces ponownego łączenia był zaskakująco tani w większości przypadków. Serwisy z ciasno sprzężonymi bazami danych wymagały jednej operacji ",[15,4543,4544],{},"ALTER TABLE ... INHERIT"," plus zmiany aplikacji. Serwisy z osobnymi bazami wymagały migracji danych, ale dla tych, które powinny były być scalone, modele danych były prawie identyczne.",[11,4547,4548],{},"Drogie były fuzje, gdzie każdy serwis przez lata nazbierał własny dialekt modelu domeny. Cztery lata dywergencji spakowane w jedną migrację. Zaplanowaliśmy je na Q1 2025.",[27,4550,4552],{"id":4551},"co-zrobiłbym-inaczej","Co zrobiłbym inaczej",[11,4554,4555],{},"Zacznij od modułów, nie serwisów. Dobrze ustrukturyzowany monolit z wewnętrznymi granicami modułów jest tańszy do ekstrakcji niż do scalenia. Jeśli moduł udowodni, że potrzebuje niezależnego deploymentu, ekstrahuj go. Koszt ekstrakcji jest jednorazowy. Koszt przedwczesnej ekstrakcji to każdy featura przez pięć lat.",[11,4557,4558,4559,4561],{},"Zmierz ",[15,4560,4494],{}," przed narysowaniem linii. W 2021 roku nie mieliśmy narzędzi do tego. Dziś istnieją narzędzia do analizy couplingu zmian między repozytoriami, które powiedzą ci z historii gita, które pliki zmieniają się razem. Używaj ich przed code review architektury, nie po, bo do momentu architecture review wszyscy już zakochali się w swoim pudełku na tablicy.",[11,4563,4564],{},"Najpierw przejmij warstwę platformy. Pierwsze sześć miesięcy mikroserwisów powinno być poświęcone na wspólny system buildów, wspólną obserwowalność, wspólny CI\u002FCD. My poświęciliśmy je na logikę biznesową. Dług techniczny z tej decyzji kosztował nas więcej niż błędne granice serwisów.",{"title":46,"searchDepth":74,"depth":74,"links":4566},[4567,4568,4569,4570,4571],{"id":4466,"depth":74,"text":4467},{"id":4479,"depth":74,"text":4480},{"id":4524,"depth":74,"text":4525},{"id":4537,"depth":74,"text":4538},{"id":4551,"depth":74,"text":4552},"arch","2026-05-14",{},"\u002Fpl\u002Farticles\u002Fmicroservice-cost",{"x":4577,"y":4578,"depth":4579,"size":3242},0.72,0.68,1.1,[976,960],{"title":4455,"description":4460},"service-boundaries","pl\u002Farticles\u002Fmicroservice-cost",[967,4585,4586,4587],"organisation","platform","devex","v5.0.0","3-cgoVoDpIxqH9vQt5s-ZCyL91aJ2rmhD6HAkEe1lLg",{"id":4591,"title":4592,"articleId":4593,"body":4594,"category":2058,"codeLang":61,"date":5000,"deploys":68,"description":4598,"excerpt":949,"extension":950,"lang":951,"meta":5001,"navigation":101,"path":5002,"pos":5003,"readMin":122,"related":5006,"seo":5007,"service":5008,"stem":5009,"tags":5010,"version":970,"__hash__":5016},"articles_pl\u002Fpl\u002Farticles\u002Fphp-references.md","Referencje w PHP: footgun, który wchodzi na produkcję szybciej niż myślisz","php-references",{"type":8,"value":4595,"toc":4992},[4596,4599,4606,4610,4613,4616,4664,4674,4678,4681,4737,4767,4794,4809,4813,4819,4887,4902,4906,4913,4917,4920,4976,4978,4987,4990],[11,4597,4598],{},"Referencje w PHP to jedna z nielicznych funkcji językowych, przed którymi manual PHP explicite ostrzega przed niepotrzebnym używaniem. Ostrzeżenie jest zasadne. Debugowałem trzy oddzielne incydenty produkcyjne spowodowane przez referencje, i w dwóch z nich oryginalny developer nie był świadomy, że w ogóle wprowadził referencję.",[11,4600,4601,4602,4605],{},"To nie jest artykuł o tym, dlaczego ",[15,4603,4604],{},"&"," to code smell. To artykuł o tym, żeby rozumieć dokładnie co robi, bo spotkasz go w legacy codebases, będziesz go okazjonalnie potrzebować i na pewno kiedyś będziesz debugować buga przez niego spowodowanego.",[27,4607,4609],{"id":4608},"czym-referencja-faktycznie-jest","Czym referencja faktycznie jest",[11,4611,4612],{},"Domyślne zachowanie PHP to copy-on-write: gdy przypisujesz jedną zmienną do drugiej, początkowo współdzielą tę samą wartość w pamięci. Kopia następuje dopiero gdy jedna z nich jest modyfikowana. To już jest dość wydajne przy czytaniu danych.",[11,4614,4615],{},"Referencja omija copy-on-write całkowicie. Dwie zmienne będące referencjami do tej samej wartości współdzielą pamięć niezależnie od modyfikacji. Modyfikacja którejkolwiek modyfikuje wartość bazową, do której obie wskazują.",[38,4617,4619],{"className":59,"code":4618,"language":61,"meta":46,"style":46},"$a = 'original';\n$b = $a;   \u002F\u002F copy-on-write: $b points to the same memory, but...\n$b = 'modified';  \u002F\u002F ...the copy happens here. $a is still 'original'.\nvar_dump($a);     \u002F\u002F string(8) \"original\"\n\n$a = 'original';\n$b = &$a;  \u002F\u002F reference: $b is an alias for the same memory location as $a\n$b = 'modified';  \u002F\u002F no copy — modifies the underlying value directly\nvar_dump($a);     \u002F\u002F string(8) \"modified\"  ← $a changed, not $b\n",[15,4620,4621,4626,4631,4636,4641,4645,4649,4654,4659],{"__ignoreMap":46},[65,4622,4623],{"class":67,"line":68},[65,4624,4625],{},"$a = 'original';\n",[65,4627,4628],{"class":67,"line":74},[65,4629,4630],{},"$b = $a;   \u002F\u002F copy-on-write: $b points to the same memory, but...\n",[65,4632,4633],{"class":67,"line":80},[65,4634,4635],{},"$b = 'modified';  \u002F\u002F ...the copy happens here. $a is still 'original'.\n",[65,4637,4638],{"class":67,"line":86},[65,4639,4640],{},"var_dump($a);     \u002F\u002F string(8) \"original\"\n",[65,4642,4643],{"class":67,"line":92},[65,4644,102],{"emptyLinePlaceholder":101},[65,4646,4647],{"class":67,"line":98},[65,4648,4625],{},[65,4650,4651],{"class":67,"line":105},[65,4652,4653],{},"$b = &$a;  \u002F\u002F reference: $b is an alias for the same memory location as $a\n",[65,4655,4656],{"class":67,"line":111},[65,4657,4658],{},"$b = 'modified';  \u002F\u002F no copy — modifies the underlying value directly\n",[65,4660,4661],{"class":67,"line":116},[65,4662,4663],{},"var_dump($a);     \u002F\u002F string(8) \"modified\"  ← $a changed, not $b\n",[11,4665,4666,4667,4670,4671,4673],{},"Różnica ma znaczenie, bo zachowanie referencji w PHP nie jest zawsze oczywiste przy czytaniu kodu. Referencje nie wyglądają inaczej od zwykłych zmiennych po przypisaniu, ",[15,4668,4669],{},"$b"," wygląda tak samo w obu przypadkach. Trzeba cofnąć się do miejsca, gdzie ",[15,4672,4604],{}," zostało wprowadzone.",[27,4675,4677],{"id":4676},"incydent-1-foreach-który-skorumpował-tablicę","Incydent 1: foreach który skorumpował tablicę",[11,4679,4680],{},"To najczęstszy bug referencyjny, który widziałem w produkcyjnych codebases. Pojawia się w kodzie sprzed PHP 7, który nigdy nie był refaktorowany:",[38,4682,4684],{"className":59,"code":4683,"language":61,"meta":46,"style":46},"$prices = [100, 200, 300, 400, 500];\n\nforeach ($prices as &$price) {\n    $price = $price * 0.9;\n}\n\u002F\u002F After the loop: $prices = [90, 180, 270, 360, 450] ✓\n\n\u002F\u002F Some other code, three lines later, iterates the same array:\nforeach ($prices as $price) {\n    echo $price . \"\\n\";\n}\n",[15,4685,4686,4691,4695,4700,4705,4709,4714,4718,4723,4728,4733],{"__ignoreMap":46},[65,4687,4688],{"class":67,"line":68},[65,4689,4690],{},"$prices = [100, 200, 300, 400, 500];\n",[65,4692,4693],{"class":67,"line":74},[65,4694,102],{"emptyLinePlaceholder":101},[65,4696,4697],{"class":67,"line":80},[65,4698,4699],{},"foreach ($prices as &$price) {\n",[65,4701,4702],{"class":67,"line":86},[65,4703,4704],{},"    $price = $price * 0.9;\n",[65,4706,4707],{"class":67,"line":92},[65,4708,95],{},[65,4710,4711],{"class":67,"line":98},[65,4712,4713],{},"\u002F\u002F After the loop: $prices = [90, 180, 270, 360, 450] ✓\n",[65,4715,4716],{"class":67,"line":105},[65,4717,102],{"emptyLinePlaceholder":101},[65,4719,4720],{"class":67,"line":111},[65,4721,4722],{},"\u002F\u002F Some other code, three lines later, iterates the same array:\n",[65,4724,4725],{"class":67,"line":116},[65,4726,4727],{},"foreach ($prices as $price) {\n",[65,4729,4730],{"class":67,"line":122},[65,4731,4732],{},"    echo $price . \"\\n\";\n",[65,4734,4735],{"class":67,"line":127},[65,4736,95],{},[11,4738,4739,4740,927,4743,4746,4747,4750,4751,4753,4754,4756,4757,4759,4760,4763,4764,4766],{},"Oczekiwany output: 90, 180, 270, 360, 450. Rzeczywisty output: 90, 180, 270, 360, 360. Ostatni element jest błędny. Po pierwszym ",[15,4741,4742],{},"foreach",[15,4744,4745],{},"$price"," jest nadal referencją do ostatniego elementu ",[15,4748,4749],{},"$prices",", wartości pod indeksem 4. Drugi ",[15,4752,4742],{}," przypisuje po kolei każdą wartość do ",[15,4755,4745],{},". Gdy przypisuje czwartą wartość (360) do ",[15,4758,4745],{},", zapisuje 360 do ",[15,4761,4762],{},"$prices[4]",". Potem próbuje odczytać ",[15,4765,4762],{}," dla piątej iteracji i znajduje 360, nie 450.",[38,4768,4770],{"className":59,"code":4769,"language":61,"meta":46,"style":46},"\u002F\u002F The fix\nforeach ($prices as &$price) {\n    $price = $price * 0.9;\n}\nunset($price);  \u002F\u002F break the reference before the variable goes out of scope\n",[15,4771,4772,4777,4781,4785,4789],{"__ignoreMap":46},[65,4773,4774],{"class":67,"line":68},[65,4775,4776],{},"\u002F\u002F The fix\n",[65,4778,4779],{"class":67,"line":74},[65,4780,4699],{},[65,4782,4783],{"class":67,"line":80},[65,4784,4704],{},[65,4786,4787],{"class":67,"line":86},[65,4788,95],{},[65,4790,4791],{"class":67,"line":92},[65,4792,4793],{},"unset($price);  \u002F\u002F break the reference before the variable goes out of scope\n",[11,4795,4796,4799,4800,4802,4803,4805,4806,4808],{},[15,4797,4798],{},"unset($price)"," nie niszczy ostatniego elementu tablicy. Niszczy połączenie referencji między ",[15,4801,4745],{}," a ",[15,4804,4762],{},". W każdej codebase, gdzie widziałem ten bug, brakowało ",[15,4807,4798],{},". Dokumentacja PHP explicite o tym wspomina. Nadal brakuje tego w codebases dziś.",[27,4810,4812],{"id":4811},"incydent-2-funkcja-która-po-cichu-mutowała-dane-wywołującego","Incydent 2: funkcja, która po cichu mutowała dane wywołującego",[11,4814,4815,4816,4818],{},"Pipeline transformacji danych miał funkcję normalizującą dane produktów. Była wywoływana z dużymi tablicami, i ktoś dodał ",[15,4817,4604],{}," żeby uniknąć kopiowania:",[38,4820,4822],{"className":59,"code":4821,"language":61,"meta":46,"style":46},"\u002F\u002F Original: safe, no side effects\nfunction normaliseProduct(array $product): array\n{\n    $product['title'] = trim(strtolower($product['title']));\n    $product['price'] = round($product['price'] * 100) \u002F 100;\n    return $product;\n}\n\n\u002F\u002F \"Optimised\" version: unsafe\nfunction normaliseProduct(array &$product): void\n{\n    $product['title'] = trim(strtolower($product['title']));\n    $product['price'] = round($product['price'] * 100) \u002F 100;\n}\n",[15,4823,4824,4829,4834,4838,4843,4848,4853,4857,4861,4866,4871,4875,4879,4883],{"__ignoreMap":46},[65,4825,4826],{"class":67,"line":68},[65,4827,4828],{},"\u002F\u002F Original: safe, no side effects\n",[65,4830,4831],{"class":67,"line":74},[65,4832,4833],{},"function normaliseProduct(array $product): array\n",[65,4835,4836],{"class":67,"line":80},[65,4837,83],{},[65,4839,4840],{"class":67,"line":86},[65,4841,4842],{},"    $product['title'] = trim(strtolower($product['title']));\n",[65,4844,4845],{"class":67,"line":92},[65,4846,4847],{},"    $product['price'] = round($product['price'] * 100) \u002F 100;\n",[65,4849,4850],{"class":67,"line":98},[65,4851,4852],{},"    return $product;\n",[65,4854,4855],{"class":67,"line":105},[65,4856,95],{},[65,4858,4859],{"class":67,"line":111},[65,4860,102],{"emptyLinePlaceholder":101},[65,4862,4863],{"class":67,"line":116},[65,4864,4865],{},"\u002F\u002F \"Optimised\" version: unsafe\n",[65,4867,4868],{"class":67,"line":122},[65,4869,4870],{},"function normaliseProduct(array &$product): void\n",[65,4872,4873],{"class":67,"line":127},[65,4874,83],{},[65,4876,4877],{"class":67,"line":133},[65,4878,4842],{},[65,4880,4881],{"class":67,"line":139},[65,4882,4847],{},[65,4884,4885],{"class":67,"line":145},[65,4886,95],{},[11,4888,4889,4890,4893,4894,4897,4898,4901],{},"Wywołanie ",[15,4891,4892],{},"$normalised = normaliseProduct($product)"," w oryginalnej wersji zwracało zmodyfikowaną kopię. W \"zoptymalizowanej\" wersji funkcja zwracała void, ",[15,4895,4896],{},"$normalised"," był null, a ",[15,4899,4900],{},"$product"," był modyfikowany w miejscu. Dane w cache dla każdego produktu to było null. System raportowania nic nie pokazywał. Nikt nie zauważył przez dwa dni, bo główna ścieżka odczytu trafiała do bazy danych, nie do cache'a. \"Optymalizacja\" referencją zaoszczędziła dosłownie zero pamięci, tablice PHP i tak używają copy-on-write, a funkcja odczytuje tylko dwa klucze.",[27,4903,4905],{"id":4904},"kiedy-referencje-są-faktycznie-poprawne","Kiedy referencje są faktycznie poprawne",[11,4907,4908,4909,4912],{},"Referencje są odpowiednie w dokładnie dwóch sytuacjach, które napotkałem. Pierwsza to duże struktury danych modyfikowane w miejscu w algorytmie rekurencyjnym: jeśli przeglądasz i modyfikujesz głęboko zagnieżdżoną tablicę, przekazywanie przez referencję unika kopiowania całej struktury na każdej głębokości rekursji. To prawdziwy problem wydajności tylko przy znaczącej skali, nie sięgałbym po to poniżej 10MB danych. Druga to parametry wyjściowe w funkcjach w stylu rozszerzeń C, takich jak ",[15,4910,4911],{},"preg_match()"," z tablicą dopasowań.",[27,4914,4916],{"id":4915},"błędne-przekonanie-o-referencjach-obiektów","Błędne przekonanie o referencjach obiektów",[11,4918,4919],{},"Bardzo powszechne nieporozumienie: obiekty w PHP są już \"przekazywane przez referencję.\" Nie są. Obiekty są przekazywane przez uchwyt, wskaźnik do obiektu, nie sam obiekt. Ponowne przypisanie uchwytu wewnątrz funkcji nie wpływa na uchwyt wywołującego. Modyfikacja obiektu przez uchwyt już tak.",[38,4921,4923],{"className":59,"code":4922,"language":61,"meta":46,"style":46},"class Counter { public int $count = 0; }\n\nfunction increment(Counter $counter): void\n{\n    $counter->count++;       \u002F\u002F modifies the object — caller sees this\n    $counter = new Counter;  \u002F\u002F reassigns the handle — caller does NOT see this\n}\n\n$c = new Counter;\nincrement($c);\nvar_dump($c->count);  \u002F\u002F int(1) — the increment happened, the reassignment did not\n",[15,4924,4925,4930,4934,4939,4943,4948,4953,4957,4961,4966,4971],{"__ignoreMap":46},[65,4926,4927],{"class":67,"line":68},[65,4928,4929],{},"class Counter { public int $count = 0; }\n",[65,4931,4932],{"class":67,"line":74},[65,4933,102],{"emptyLinePlaceholder":101},[65,4935,4936],{"class":67,"line":80},[65,4937,4938],{},"function increment(Counter $counter): void\n",[65,4940,4941],{"class":67,"line":86},[65,4942,83],{},[65,4944,4945],{"class":67,"line":92},[65,4946,4947],{},"    $counter->count++;       \u002F\u002F modifies the object — caller sees this\n",[65,4949,4950],{"class":67,"line":98},[65,4951,4952],{},"    $counter = new Counter;  \u002F\u002F reassigns the handle — caller does NOT see this\n",[65,4954,4955],{"class":67,"line":105},[65,4956,95],{},[65,4958,4959],{"class":67,"line":111},[65,4960,102],{"emptyLinePlaceholder":101},[65,4962,4963],{"class":67,"line":116},[65,4964,4965],{},"$c = new Counter;\n",[65,4967,4968],{"class":67,"line":122},[65,4969,4970],{},"increment($c);\n",[65,4972,4973],{"class":67,"line":127},[65,4974,4975],{},"var_dump($c->count);  \u002F\u002F int(1) — the increment happened, the reassignment did not\n",[27,4977,3806],{"id":3805},[11,4979,4980,4981,4983,4984,4986],{},"Gdy widzę ",[15,4982,4604],{}," w sygnaturze funkcji lub w ",[15,4985,4742],{},", zatrzymuję się i uważnie czytam otaczające dwadzieścia linii. Czy ta referencja jest nadal aktywna po pętli? Czy wywołujący oczekuje, że funkcja nie będzie miała efektów ubocznych na argumencie? Czy uzasadnienie wydajnościowe jest realne, czy to przedwczesna optymalizacja od kogoś, kto nie przeczytał dokumentacji o copy-on-write?",[11,4988,4989],{},"Referencja w kodzie PHP warstwy aplikacji to żółta flaga, nie dlatego, że zawsze jest błędna, ale dlatego, że kod polega na semantyce aliasowania, która jest nieoczywista dla następnego czytającego. A \"nieoczywiste dla następnego czytającego\" to miejsca, gdzie żyją bugi.",[936,4991,938],{},{"title":46,"searchDepth":74,"depth":74,"links":4993},[4994,4995,4996,4997,4998,4999],{"id":4608,"depth":74,"text":4609},{"id":4676,"depth":74,"text":4677},{"id":4811,"depth":74,"text":4812},{"id":4904,"depth":74,"text":4905},{"id":4915,"depth":74,"text":4916},{"id":3805,"depth":74,"text":3806},"2023-09-30",{},"\u002Fpl\u002Farticles\u002Fphp-references",{"x":5004,"y":5005,"depth":2065,"size":950},0.12,0.5,[3244,959],{"title":4592,"description":4598},"memory-management","pl\u002Farticles\u002Fphp-references",[61,5011,5012,5013,5014,5015],"memory","debugging","references","performance","footguns","a42q2WY9fFlbb1OsMVyLWhGo6Dyr5Yg-gn6Fb87diM8",{"id":5018,"title":5019,"articleId":1155,"body":5020,"category":2058,"codeLang":5057,"date":5183,"deploys":111,"description":5024,"excerpt":949,"extension":950,"lang":951,"meta":5184,"navigation":101,"path":5185,"pos":5186,"readMin":116,"related":5189,"seo":5191,"service":5192,"stem":5193,"tags":5194,"version":5199,"__hash__":5200},"articles_pl\u002Fpl\u002Farticles\u002Fpostgres-edge.md","Postgres na edge: przemyślenie kluczy głównych dla globalnych zapisów",{"type":8,"value":5021,"toc":5176},[5022,5025,5028,5032,5039,5042,5046,5053,5127,5138,5142,5149,5153,5163,5167,5174],[11,5023,5024],{},"Seryjny klucz główny nie jest kluczem. Jest uzgodnieniem między każdym writerem, że będą kolejno czekać na swoją kolej. Dotrzymaj tej umowy w dwóch regionach, a z definicji zrezygnowałeś z dostępności lub świeżości.",[11,5026,5027],{},"ULIDy i UUIDv7 nie są egzotyczne. To najbardziej nudna możliwa odpowiedź na pytanie \"jak pozwolić drugiemu regionowi pisać bez dzwonienia do domu po numer sekwencji\". Ciekawa część to wszystko, co zależy od starego klucza (klucze obce, indeksy, tabele audytu, pipeline analityczny) i jak migrować bez sobotniej nocnej przerwy serwisowej.",[27,5029,5031],{"id":5030},"dlaczego-sekwencje-psują-się-na-edge","Dlaczego sekwencje psują się na edge",[11,5033,5034,5035,5038],{},"Klucz główny ",[15,5036,5037],{},"bigserial"," wymaga scentralizowanego licznika sekwencji. Każdy insert w regionie B musi albo czekać na round-trip do regionu A, albo ryzykować lukę w sekwencji. Przy 40ms latencji między regionami i 5000 zapisach na sekundę, to 200 równoległych żądań czekających w kolejce na numer.",[11,5040,5041],{},"Alternatywy dzielą się na dwie rodziny. Losowe identyfikatory jak UUIDv4 są globalnie unikalne bez koordynacji, ale nie da się ich sortować. Fragmentacja indeksu na dużych tabelach jest poważna, a złączenia na kolumnach UUID są mierzalnie wolniejsze niż na liczbach całkowitych. Identyfikatory z porządkiem czasowym (UUIDv7 i ULID) są globalnie unikalne, mniej więcej sortowalne po czasie tworzenia i przyjazne B-tree przy insertach. To właściwa odpowiedź dla nowych tabel.",[27,5043,5045],{"id":5044},"ścieżka-migracji-bez-downtime","Ścieżka migracji (bez downtime)",[11,5047,5048,5049,5052],{},"Przenieśliśmy 240M wierszy w tabeli ",[15,5050,5051],{},"orders",". Strategia to podejście shadow column: dodaj nowy klucz obok starego, uzupełnij dane, zbuduj covering index, zamień za pomocą widoku.",[38,5054,5058],{"className":5055,"code":5056,"language":5057,"meta":46,"style":46},"language-sql shiki shiki-themes github-light github-dark","-- shadow column, backfill, swap. celowo nudne.\nalter table orders add column id_v2 uuid;\n\nupdate orders set id_v2 = uuidv7_from_timestamp(created_at)\nwhere id_v2 is null;\n\ncreate unique index concurrently orders_id_v2_uq on orders (id_v2);\n\n-- zamień za pomocą widoku; przełącz w jednej transakcji.\nbegin;\n  alter table orders rename to orders_legacy;\n  create view orders as\n    select id_v2 as id, \u002F* ...inne kolumny... *\u002F from orders_legacy;\ncommit;\n","sql",[15,5059,5060,5065,5070,5074,5079,5084,5088,5093,5097,5102,5107,5112,5117,5122],{"__ignoreMap":46},[65,5061,5062],{"class":67,"line":68},[65,5063,5064],{},"-- shadow column, backfill, swap. celowo nudne.\n",[65,5066,5067],{"class":67,"line":74},[65,5068,5069],{},"alter table orders add column id_v2 uuid;\n",[65,5071,5072],{"class":67,"line":80},[65,5073,102],{"emptyLinePlaceholder":101},[65,5075,5076],{"class":67,"line":86},[65,5077,5078],{},"update orders set id_v2 = uuidv7_from_timestamp(created_at)\n",[65,5080,5081],{"class":67,"line":92},[65,5082,5083],{},"where id_v2 is null;\n",[65,5085,5086],{"class":67,"line":98},[65,5087,102],{"emptyLinePlaceholder":101},[65,5089,5090],{"class":67,"line":105},[65,5091,5092],{},"create unique index concurrently orders_id_v2_uq on orders (id_v2);\n",[65,5094,5095],{"class":67,"line":111},[65,5096,102],{"emptyLinePlaceholder":101},[65,5098,5099],{"class":67,"line":116},[65,5100,5101],{},"-- zamień za pomocą widoku; przełącz w jednej transakcji.\n",[65,5103,5104],{"class":67,"line":122},[65,5105,5106],{},"begin;\n",[65,5108,5109],{"class":67,"line":127},[65,5110,5111],{},"  alter table orders rename to orders_legacy;\n",[65,5113,5114],{"class":67,"line":133},[65,5115,5116],{},"  create view orders as\n",[65,5118,5119],{"class":67,"line":139},[65,5120,5121],{},"    select id_v2 as id, \u002F* ...inne kolumny... *\u002F from orders_legacy;\n",[65,5123,5124],{"class":67,"line":145},[65,5125,5126],{},"commit;\n",[11,5128,5129,5130,5133,5134,5137],{},"Funkcja ",[15,5131,5132],{},"uuidv7_from_timestamp"," konwertuje istniejące ",[15,5135,5136],{},"created_at"," na UUIDv7 z porządkiem czasowym, zachowując kolejność sortowania, od której zależy downstream pipeline analityczny. To pojedyncze rozszerzenie C, ~50 linii.",[27,5139,5141],{"id":5140},"sztuczka-z-widokiem","Sztuczka z widokiem",[11,5143,5144,5145,5148],{},"Widok pozwala nam atomowo zmienić nazwę kolumny z perspektywy aplikacji. Stary kod czytający ",[15,5146,5147],{},"orders.id"," nadal działa. Nowy kod używa tej samej nazwy kolumny. Uruchamiamy oboje w produkcji przez dwa tygodnie, weryfikujemy referencje kluczy obcych, a potem dropujemy legacy tabelę.",[27,5150,5152],{"id":5151},"czego-nie-przewidzieliśmy","Czego nie przewidzieliśmy",[11,5154,5155,5156,5159,5160,5162],{},"Tabela audytu miała ",[15,5157,5158],{},"order_id bigint"," jako klucz obcy. Musieliśmy uzupełnić i to. Zajęło dłużej niż uzupełnienie tabeli orders, tabela audytu była trzy razy większa i nie miała kolumny ",[15,5161,5136],{},", więc musieliśmy joinować z powrotem do orders żeby wyderywować timestamps. Następnym razem najpierw zmigrowalibyśmy tabelę audytu używając logical replication i zamienili ją w tej samej transakcji.",[27,5164,5166],{"id":5165},"wyniki","Wyniki",[11,5168,5169,5170,5173],{},"Przepustowość zapisów w regionie B wzrosła z 1200 do 4800 insertów na sekundę. Latencja p99 zapisu spadła z 38ms do 4ms. Serwer sekwencji (pojedyncza instancja Postgres, która nic nie robiła poza obsługą ",[15,5171,5172],{},"nextval"," i stała się najważniejszą maszyną w całym parku) został wycofany z eksploatacji.",[936,5175,938],{},{"title":46,"searchDepth":74,"depth":74,"links":5177},[5178,5179,5180,5181,5182],{"id":5030,"depth":74,"text":5031},{"id":5044,"depth":74,"text":5045},{"id":5140,"depth":74,"text":5141},{"id":5151,"depth":74,"text":5152},{"id":5165,"depth":74,"text":5166},"2026-03-30",{},"\u002Fpl\u002Farticles\u002Fpostgres-edge",{"x":5187,"y":5188,"depth":957,"size":950},0.21,0.71,[1154,5190],"state-machine",{"title":5019,"description":5024},"pg-primary-keys","pl\u002Farticles\u002Fpostgres-edge",[5195,5196,5197,5198],"postgres","distributed-systems","ulid","replication","v1.3.2","PwHekWT2Ol91w6a0jYJTf1P5H6qfhBD7S1hhm9iaPKA",{"id":5202,"title":5203,"articleId":3244,"body":5204,"category":61,"codeLang":61,"date":6004,"deploys":80,"description":5208,"excerpt":949,"extension":950,"lang":951,"meta":6005,"navigation":101,"path":2843,"pos":6006,"readMin":157,"related":6010,"seo":6011,"service":6012,"stem":6013,"tags":6014,"version":6017,"__hash__":6018},"articles_pl\u002Fpl\u002Farticles\u002Fsingleton-pattern.md","Pułapka Singleton: globalny stan, workery PHP-FPM i wzorzec, który źle się zestarzał",{"type":8,"value":5205,"toc":5995},[5206,5209,5212,5216,5219,5222,5362,5365,5368,5372,5375,5382,5386,5389,5392,5511,5518,5521,5574,5577,5581,5584,5645,5656,5659,5773,5776,5847,5850,5854,5857,5915,5918,5922,5929,5971,5982,5984,5987,5990,5993],[11,5207,5208],{},"Byłem na dokładnie dwóch code review, gdzie ktoś zaproponował Singleton i miał rację. Byłem może na czterdziestu, gdzie nie miał. Wzorzec nie jest problemem. Problem polega na tym, że \"chcę mieć tylko jeden egzemplarz tego obiektu\" brzmi jak właściwa motywacja prawie zawsze, i prawie nigdy nią nie jest.",[11,5210,5211],{},"To jest retrospekcja produkcyjna. Kod jest prawdziwy. Incydenty się wydarzyły.",[27,5213,5215],{"id":5214},"kłamstwo-php-fpm","Kłamstwo PHP-FPM",[11,5217,5218],{},"Najgroźniejsze nieporozumienie dotyczące Singletona w PHP polega na przekonaniu, że daje ci jedną instancję na całą aplikację. Nie daje. Daje jedną instancję na każdy worker process. Na standardowej puli PHP-FPM z 32 workerami masz 32 Singletony.",[11,5220,5221],{},"To nie jest edge case. To jest domyślne zachowanie. Każda aplikacja PHP pod jakimkolwiek sensownym ruchem działa właśnie tak.",[38,5223,5225],{"className":59,"code":5224,"language":61,"meta":46,"style":46},"\u002F\u002F Myślisz, że masz:\n\u002F\u002F   Aplikacja → Singleton → jedna instancja\n\u002F\u002F\n\u002F\u002F W rzeczywistości masz:\n\u002F\u002F   Request #1 → Worker #1 → Singleton::$instance (obiekt A)\n\u002F\u002F   Request #2 → Worker #7 → Singleton::$instance (obiekt B)\n\u002F\u002F   Request #3 → Worker #7 → Singleton::$instance (obiekt B)  ← ten sam co #2\n\u002F\u002F   Request #4 → Worker #1 → Singleton::$instance (obiekt A)  ← ten sam co #1\n\nclass RateLimiter\n{\n    private static ?self $instance = null;\n    private array $buckets = [];  \u002F\u002F mutable state: liczba requestów per IP per minuta\n\n    public static function getInstance(): static\n    {\n        if (static::$instance === null) {\n            static::$instance = new static();\n        }\n        return static::$instance;\n    }\n\n    public function check(string $ip, int $limit): bool\n    {\n        $key = $ip . ':' . floor(time() \u002F 60);\n        $this->buckets[$key] = ($this->buckets[$key] ?? 0) + 1;\n        return $this->buckets[$key] \u003C= $limit;\n    }\n}\n",[15,5226,5227,5232,5237,5242,5247,5252,5257,5262,5267,5271,5276,5280,5285,5290,5294,5299,5303,5308,5313,5317,5322,5326,5330,5335,5339,5344,5349,5354,5358],{"__ignoreMap":46},[65,5228,5229],{"class":67,"line":68},[65,5230,5231],{},"\u002F\u002F Myślisz, że masz:\n",[65,5233,5234],{"class":67,"line":74},[65,5235,5236],{},"\u002F\u002F   Aplikacja → Singleton → jedna instancja\n",[65,5238,5239],{"class":67,"line":80},[65,5240,5241],{},"\u002F\u002F\n",[65,5243,5244],{"class":67,"line":86},[65,5245,5246],{},"\u002F\u002F W rzeczywistości masz:\n",[65,5248,5249],{"class":67,"line":92},[65,5250,5251],{},"\u002F\u002F   Request #1 → Worker #1 → Singleton::$instance (obiekt A)\n",[65,5253,5254],{"class":67,"line":98},[65,5255,5256],{},"\u002F\u002F   Request #2 → Worker #7 → Singleton::$instance (obiekt B)\n",[65,5258,5259],{"class":67,"line":105},[65,5260,5261],{},"\u002F\u002F   Request #3 → Worker #7 → Singleton::$instance (obiekt B)  ← ten sam co #2\n",[65,5263,5264],{"class":67,"line":111},[65,5265,5266],{},"\u002F\u002F   Request #4 → Worker #1 → Singleton::$instance (obiekt A)  ← ten sam co #1\n",[65,5268,5269],{"class":67,"line":116},[65,5270,102],{"emptyLinePlaceholder":101},[65,5272,5273],{"class":67,"line":122},[65,5274,5275],{},"class RateLimiter\n",[65,5277,5278],{"class":67,"line":127},[65,5279,83],{},[65,5281,5282],{"class":67,"line":133},[65,5283,5284],{},"    private static ?self $instance = null;\n",[65,5286,5287],{"class":67,"line":139},[65,5288,5289],{},"    private array $buckets = [];  \u002F\u002F mutable state: liczba requestów per IP per minuta\n",[65,5291,5292],{"class":67,"line":145},[65,5293,102],{"emptyLinePlaceholder":101},[65,5295,5296],{"class":67,"line":151},[65,5297,5298],{},"    public static function getInstance(): static\n",[65,5300,5301],{"class":67,"line":157},[65,5302,136],{},[65,5304,5305],{"class":67,"line":163},[65,5306,5307],{},"        if (static::$instance === null) {\n",[65,5309,5310],{"class":67,"line":169},[65,5311,5312],{},"            static::$instance = new static();\n",[65,5314,5315],{"class":67,"line":175},[65,5316,647],{},[65,5318,5319],{"class":67,"line":180},[65,5320,5321],{},"        return static::$instance;\n",[65,5323,5324],{"class":67,"line":185},[65,5325,172],{},[65,5327,5328],{"class":67,"line":191},[65,5329,102],{"emptyLinePlaceholder":101},[65,5331,5332],{"class":67,"line":196},[65,5333,5334],{},"    public function check(string $ip, int $limit): bool\n",[65,5336,5337],{"class":67,"line":202},[65,5338,136],{},[65,5340,5341],{"class":67,"line":207},[65,5342,5343],{},"        $key = $ip . ':' . floor(time() \u002F 60);\n",[65,5345,5346],{"class":67,"line":212},[65,5347,5348],{},"        $this->buckets[$key] = ($this->buckets[$key] ?? 0) + 1;\n",[65,5350,5351],{"class":67,"line":217},[65,5352,5353],{},"        return $this->buckets[$key] \u003C= $limit;\n",[65,5355,5356],{"class":67,"line":223},[65,5357,172],{},[65,5359,5360],{"class":67,"line":229},[65,5361,95],{},[11,5363,5364],{},"Ten rate limiter działał w produkcji przez trzy tygodnie, zanim zauważyliśmy, że nasz limit 100 requestów na minutę był egzekwowany jako mniej więcej 100 \u002F 32 = 3 requesty na minutę per worker, albo wcale, gdy requesty trafiały na różne workery. Bufor w pamięci nigdy nie był współdzielony. Każdy worker zbierał własne liczniki i limity były bez znaczenia.",[11,5366,5367],{},"Naprawa nie polegała na \"lepszym Singletonie\". Naprawa to był Redis. Wzorzec był od początku nieodpowiedni do wymagania. Wymaganie brzmiało \"jeden limit per IP w całej aplikacji\", a to jest jawnie rozproszone ograniczenie. Żaden wzorzec in-process tego nie spełni.",[27,5369,5371],{"id":5370},"co-ten-wzorzec-faktycznie-gwarantuje","Co ten wzorzec faktycznie gwarantuje",[11,5373,5374],{},"Sprowadzony do esencji, Singleton gwarantuje jedną instancję per process, per klasa, per kontekst ładowania klas. Nic ponadto. W PHP-FPM to jedna per worker, w skrypcie CLI jedna per uruchomienie, w suite testów działającej w jednym procesie jedna przez wszystkie testy, co jest zazwyczaj katastrofą, która wychodzi na jaw godziny później gdy ktoś uruchomi suite w innej kolejności.",[11,5376,5377,5378,5381],{},"Mechanizm ",[15,5379,5380],{},"getInstance()"," to w zasadzie leniwy konstruktor z globalnym dostępem. Wartościową gwarancją jest leniwa inicjalizacja. Niebezpieczną częścią jest globalny dostęp. Te dwie odpowiedzialności są ze sobą sprzęgnięte w tym wzorcu, i przez większość czasu chcesz jednej bez drugiej.",[27,5383,5385],{"id":5384},"kiedy-jest-faktycznie-poprawny","Kiedy jest faktycznie poprawny",[11,5387,5388],{},"Istnieją trzy scenariusze, w których widziałem Singleton użyty poprawnie w produkcyjnych systemach PHP.",[11,5390,5391],{},"Niemutowalna konfiguracja aplikacji ładowana raz ze środowiska to najczystszy przypadek:",[38,5393,5395],{"className":59,"code":5394,"language":61,"meta":46,"style":46},"final class AppConfig\n{\n    private static ?self $instance = null;\n\n    private function __construct(\n        public readonly string $appEnv,\n        public readonly string $dbDsn,\n        public readonly int    $dbPoolSize,\n        public readonly string $redisUrl,\n    ) {}\n\n    public static function load(): static\n    {\n        if (static::$instance !== null) {\n            return static::$instance;\n        }\n\n        return static::$instance = new static(\n            appEnv:      (string) ($_ENV['APP_ENV']       ?? 'production'),\n            dbDsn:       (string) ($_ENV['DATABASE_URL']  ?? throw new \\RuntimeException('DATABASE_URL not set')),\n            dbPoolSize:  (int)    ($_ENV['DB_POOL_SIZE']  ?? 10),\n            redisUrl:    (string) ($_ENV['REDIS_URL']     ?? throw new \\RuntimeException('REDIS_URL not set')),\n        );\n    }\n}\n",[15,5396,5397,5402,5406,5410,5414,5419,5424,5429,5434,5439,5443,5447,5452,5456,5461,5466,5470,5474,5479,5484,5489,5494,5499,5503,5507],{"__ignoreMap":46},[65,5398,5399],{"class":67,"line":68},[65,5400,5401],{},"final class AppConfig\n",[65,5403,5404],{"class":67,"line":74},[65,5405,83],{},[65,5407,5408],{"class":67,"line":80},[65,5409,5284],{},[65,5411,5412],{"class":67,"line":86},[65,5413,102],{"emptyLinePlaceholder":101},[65,5415,5416],{"class":67,"line":92},[65,5417,5418],{},"    private function __construct(\n",[65,5420,5421],{"class":67,"line":98},[65,5422,5423],{},"        public readonly string $appEnv,\n",[65,5425,5426],{"class":67,"line":105},[65,5427,5428],{},"        public readonly string $dbDsn,\n",[65,5430,5431],{"class":67,"line":111},[65,5432,5433],{},"        public readonly int    $dbPoolSize,\n",[65,5435,5436],{"class":67,"line":116},[65,5437,5438],{},"        public readonly string $redisUrl,\n",[65,5440,5441],{"class":67,"line":122},[65,5442,352],{},[65,5444,5445],{"class":67,"line":127},[65,5446,102],{"emptyLinePlaceholder":101},[65,5448,5449],{"class":67,"line":133},[65,5450,5451],{},"    public static function load(): static\n",[65,5453,5454],{"class":67,"line":139},[65,5455,136],{},[65,5457,5458],{"class":67,"line":145},[65,5459,5460],{},"        if (static::$instance !== null) {\n",[65,5462,5463],{"class":67,"line":151},[65,5464,5465],{},"            return static::$instance;\n",[65,5467,5468],{"class":67,"line":157},[65,5469,647],{},[65,5471,5472],{"class":67,"line":163},[65,5473,102],{"emptyLinePlaceholder":101},[65,5475,5476],{"class":67,"line":169},[65,5477,5478],{},"        return static::$instance = new static(\n",[65,5480,5481],{"class":67,"line":175},[65,5482,5483],{},"            appEnv:      (string) ($_ENV['APP_ENV']       ?? 'production'),\n",[65,5485,5486],{"class":67,"line":180},[65,5487,5488],{},"            dbDsn:       (string) ($_ENV['DATABASE_URL']  ?? throw new \\RuntimeException('DATABASE_URL not set')),\n",[65,5490,5491],{"class":67,"line":185},[65,5492,5493],{},"            dbPoolSize:  (int)    ($_ENV['DB_POOL_SIZE']  ?? 10),\n",[65,5495,5496],{"class":67,"line":191},[65,5497,5498],{},"            redisUrl:    (string) ($_ENV['REDIS_URL']     ?? throw new \\RuntimeException('REDIS_URL not set')),\n",[65,5500,5501],{"class":67,"line":196},[65,5502,166],{},[65,5504,5505],{"class":67,"line":202},[65,5506,172],{},[65,5508,5509],{"class":67,"line":207},[65,5510,95],{},[11,5512,5513,5514,5517],{},"Właściwości ",[15,5515,5516],{},"readonly"," sprawiają, że jest to bezpieczne: nie ma mutable state, który mógłby się psuć między requestami. Walidacja w konstruktorze oznacza, że błędna konfiguracja wybucha natychmiast przy starcie zamiast cicho w runtime.",[11,5519,5520],{},"Sam kontener IoC to drugi zasadny przypadek. Każdy nowoczesny framework PHP (Laravel, Symfony, Laminas) ma kontener IoC, który jest efektywnie Singletonem. Kontener jest tworzony raz, wypełniany bindingami i rozwiązywany przez cały cykl życia requestu. Jest to poprawne, ponieważ kontener jest bezstanowy między rozwiązaniami: trzyma definicje, nie instancje każdej zbindowanej klasy.",[38,5522,5524],{"className":59,"code":5523,"language":61,"meta":46,"style":46},"\u002F\u002F Klasa Application Laravela rozszerza Container i jest bootstrapowana raz.\n\u002F\u002F $app jest przekazywany wszędzie, ale jest to ten sam obiekt.\n\u002F\u002F Co czyni to akceptowalnym: kontener trzyma domknięcia, nie rozwiązane obiekty.\n\u002F\u002F Rozwiązanie następuje na żądanie, a rozwiązane instancje mają własne reguły lifetime.\n\n$app->bind(PaymentGatewayInterface::class, StripeGateway::class);\n\u002F\u002F ↑ przechowuje definicję bindingu — brak efektów ubocznych\n\n$gateway = $app->make(PaymentGatewayInterface::class);\n\u002F\u002F ↑ rozwiązuje na żądanie — domyślnie świeża instancja\n",[15,5525,5526,5531,5536,5541,5546,5550,5555,5560,5564,5569],{"__ignoreMap":46},[65,5527,5528],{"class":67,"line":68},[65,5529,5530],{},"\u002F\u002F Klasa Application Laravela rozszerza Container i jest bootstrapowana raz.\n",[65,5532,5533],{"class":67,"line":74},[65,5534,5535],{},"\u002F\u002F $app jest przekazywany wszędzie, ale jest to ten sam obiekt.\n",[65,5537,5538],{"class":67,"line":80},[65,5539,5540],{},"\u002F\u002F Co czyni to akceptowalnym: kontener trzyma domknięcia, nie rozwiązane obiekty.\n",[65,5542,5543],{"class":67,"line":86},[65,5544,5545],{},"\u002F\u002F Rozwiązanie następuje na żądanie, a rozwiązane instancje mają własne reguły lifetime.\n",[65,5547,5548],{"class":67,"line":92},[65,5549,102],{"emptyLinePlaceholder":101},[65,5551,5552],{"class":67,"line":98},[65,5553,5554],{},"$app->bind(PaymentGatewayInterface::class, StripeGateway::class);\n",[65,5556,5557],{"class":67,"line":105},[65,5558,5559],{},"\u002F\u002F ↑ przechowuje definicję bindingu — brak efektów ubocznych\n",[65,5561,5562],{"class":67,"line":111},[65,5563,102],{"emptyLinePlaceholder":101},[65,5565,5566],{"class":67,"line":116},[65,5567,5568],{},"$gateway = $app->make(PaymentGatewayInterface::class);\n",[65,5570,5571],{"class":67,"line":122},[65,5572,5573],{},"\u002F\u002F ↑ rozwiązuje na żądanie — domyślnie świeża instancja\n",[11,5575,5576],{},"Trzeci przypadek to menadżery puli połączeń, ale wyłącznie menadżer, nie same połączenia. Menadżer puli, który śledzi dostępne połączenia, jest zasadnie Singletonem per process: powinien być dokładnie jeden per worker i żyć przez cały czas życia workera. Same połączenia muszą być pobierane i zwracane.",[27,5578,5580],{"id":5579},"katastrofa-z-testami","Katastrofa z testami",[11,5582,5583],{},"Powód, dla którego \"Singleton to antywzorzec\" stał się powszechną mądrością, wynika prawie wyłącznie z testów. Statyczny stan utrzymuje się między przypadkami testowymi w jednym procesie. Każdy test dotykający Singletona przecieka stan do następnego.",[38,5585,5587],{"className":59,"code":5586,"language":61,"meta":46,"style":46},"class UserRepositoryTest extends TestCase\n{\n    public function testFindByEmail(): void\n    {\n        \u002F\u002F Database::getInstance() otwiera prawdziwe połączenie w __construct.\n        \u002F\u002F Jeśli poprzedni test zostawił otwartą transakcję, ten test\n        \u002F\u002F będzie czekał w deadlocku na zwolnienie locka, który nigdy nie nastąpi.\n        $repo = new UserRepository(Database::getInstance());\n        $user = $repo->findByEmail('alice@example.com');\n        $this->assertNotNull($user);\n    }\n}\n",[15,5588,5589,5594,5598,5603,5607,5612,5617,5622,5627,5632,5637,5641],{"__ignoreMap":46},[65,5590,5591],{"class":67,"line":68},[65,5592,5593],{},"class UserRepositoryTest extends TestCase\n",[65,5595,5596],{"class":67,"line":74},[65,5597,83],{},[65,5599,5600],{"class":67,"line":80},[65,5601,5602],{},"    public function testFindByEmail(): void\n",[65,5604,5605],{"class":67,"line":86},[65,5606,136],{},[65,5608,5609],{"class":67,"line":92},[65,5610,5611],{},"        \u002F\u002F Database::getInstance() otwiera prawdziwe połączenie w __construct.\n",[65,5613,5614],{"class":67,"line":98},[65,5615,5616],{},"        \u002F\u002F Jeśli poprzedni test zostawił otwartą transakcję, ten test\n",[65,5618,5619],{"class":67,"line":105},[65,5620,5621],{},"        \u002F\u002F będzie czekał w deadlocku na zwolnienie locka, który nigdy nie nastąpi.\n",[65,5623,5624],{"class":67,"line":111},[65,5625,5626],{},"        $repo = new UserRepository(Database::getInstance());\n",[65,5628,5629],{"class":67,"line":116},[65,5630,5631],{},"        $user = $repo->findByEmail('alice@example.com');\n",[65,5633,5634],{"class":67,"line":122},[65,5635,5636],{},"        $this->assertNotNull($user);\n",[65,5638,5639],{"class":67,"line":127},[65,5640,172],{},[65,5642,5643],{"class":67,"line":133},[65,5644,95],{},[11,5646,5647,5648,5651,5652,5655],{},"Standardowe obejście (",[15,5649,5650],{},"Database::resetInstance()"," w ",[15,5653,5654],{},"tearDown()",") jest gorsze niż choroba. Tesujesz teraz zachowanie resetu, a zapomnienie jednego teardownu psuje resztę suite w sposób, który jest żmudny do zdiagnozowania. Spędziłem więcej niż jeden piątkowy wieczór szukając właśnie tego.",[11,5657,5658],{},"Strukturalna naprawa to dependency injection z interfejsami:",[38,5660,5662],{"className":59,"code":5661,"language":61,"meta":46,"style":46},"interface DatabaseConnectionInterface\n{\n    public function query(string $sql, array $params = []): array;\n    public function execute(string $sql, array $params = []): int;\n    public function beginTransaction(): void;\n    public function commit(): void;\n    public function rollback(): void;\n}\n\nclass UserRepository\n{\n    public function __construct(\n        private readonly DatabaseConnectionInterface $db\n    ) {}\n\n    public function findByEmail(string $email): ?User\n    {\n        $rows = $this->db->query(\n            'SELECT * FROM users WHERE email = ? LIMIT 1',\n            [$email]\n        );\n        return $rows ? User::fromRow($rows[0]) : null;\n    }\n}\n",[15,5663,5664,5669,5673,5678,5683,5688,5693,5698,5702,5706,5711,5715,5719,5724,5728,5732,5737,5741,5746,5751,5756,5760,5765,5769],{"__ignoreMap":46},[65,5665,5666],{"class":67,"line":68},[65,5667,5668],{},"interface DatabaseConnectionInterface\n",[65,5670,5671],{"class":67,"line":74},[65,5672,83],{},[65,5674,5675],{"class":67,"line":80},[65,5676,5677],{},"    public function query(string $sql, array $params = []): array;\n",[65,5679,5680],{"class":67,"line":86},[65,5681,5682],{},"    public function execute(string $sql, array $params = []): int;\n",[65,5684,5685],{"class":67,"line":92},[65,5686,5687],{},"    public function beginTransaction(): void;\n",[65,5689,5690],{"class":67,"line":98},[65,5691,5692],{},"    public function commit(): void;\n",[65,5694,5695],{"class":67,"line":105},[65,5696,5697],{},"    public function rollback(): void;\n",[65,5699,5700],{"class":67,"line":111},[65,5701,95],{},[65,5703,5704],{"class":67,"line":116},[65,5705,102],{"emptyLinePlaceholder":101},[65,5707,5708],{"class":67,"line":122},[65,5709,5710],{},"class UserRepository\n",[65,5712,5713],{"class":67,"line":127},[65,5714,83],{},[65,5716,5717],{"class":67,"line":133},[65,5718,342],{},[65,5720,5721],{"class":67,"line":139},[65,5722,5723],{},"        private readonly DatabaseConnectionInterface $db\n",[65,5725,5726],{"class":67,"line":145},[65,5727,352],{},[65,5729,5730],{"class":67,"line":151},[65,5731,102],{"emptyLinePlaceholder":101},[65,5733,5734],{"class":67,"line":157},[65,5735,5736],{},"    public function findByEmail(string $email): ?User\n",[65,5738,5739],{"class":67,"line":163},[65,5740,136],{},[65,5742,5743],{"class":67,"line":169},[65,5744,5745],{},"        $rows = $this->db->query(\n",[65,5747,5748],{"class":67,"line":175},[65,5749,5750],{},"            'SELECT * FROM users WHERE email = ? LIMIT 1',\n",[65,5752,5753],{"class":67,"line":180},[65,5754,5755],{},"            [$email]\n",[65,5757,5758],{"class":67,"line":185},[65,5759,166],{},[65,5761,5762],{"class":67,"line":191},[65,5763,5764],{},"        return $rows ? User::fromRow($rows[0]) : null;\n",[65,5766,5767],{"class":67,"line":196},[65,5768,172],{},[65,5770,5771],{"class":67,"line":202},[65,5772,95],{},[11,5774,5775],{},"Teraz test wstrzykuje mocka. Prawdziwe połączenie jest podłączone przez kontener. Test nigdy nie dotyka globalnego stanu.",[38,5777,5779],{"className":59,"code":5778,"language":61,"meta":46,"style":46},"class UserRepositoryTest extends TestCase\n{\n    public function testFindByEmail(): void\n    {\n        $db = $this->createMock(DatabaseConnectionInterface::class);\n        $db->method('query')\n           ->with('SELECT * FROM users WHERE email = ? LIMIT 1', ['alice@example.com'])\n           ->willReturn([['id' => 1, 'email' => 'alice@example.com', 'name' => 'Alice']]);\n\n        $repo = new UserRepository($db);\n        $user = $repo->findByEmail('alice@example.com');\n\n        $this->assertSame('alice@example.com', $user->email);\n    }\n}\n",[15,5780,5781,5785,5789,5793,5797,5802,5807,5812,5817,5821,5826,5830,5834,5839,5843],{"__ignoreMap":46},[65,5782,5783],{"class":67,"line":68},[65,5784,5593],{},[65,5786,5787],{"class":67,"line":74},[65,5788,83],{},[65,5790,5791],{"class":67,"line":80},[65,5792,5602],{},[65,5794,5795],{"class":67,"line":86},[65,5796,136],{},[65,5798,5799],{"class":67,"line":92},[65,5800,5801],{},"        $db = $this->createMock(DatabaseConnectionInterface::class);\n",[65,5803,5804],{"class":67,"line":98},[65,5805,5806],{},"        $db->method('query')\n",[65,5808,5809],{"class":67,"line":105},[65,5810,5811],{},"           ->with('SELECT * FROM users WHERE email = ? LIMIT 1', ['alice@example.com'])\n",[65,5813,5814],{"class":67,"line":111},[65,5815,5816],{},"           ->willReturn([['id' => 1, 'email' => 'alice@example.com', 'name' => 'Alice']]);\n",[65,5818,5819],{"class":67,"line":116},[65,5820,102],{"emptyLinePlaceholder":101},[65,5822,5823],{"class":67,"line":122},[65,5824,5825],{},"        $repo = new UserRepository($db);\n",[65,5827,5828],{"class":67,"line":127},[65,5829,5631],{},[65,5831,5832],{"class":67,"line":133},[65,5833,102],{"emptyLinePlaceholder":101},[65,5835,5836],{"class":67,"line":139},[65,5837,5838],{},"        $this->assertSame('alice@example.com', $user->email);\n",[65,5840,5841],{"class":67,"line":145},[65,5842,172],{},[65,5844,5845],{"class":67,"line":151},[65,5846,95],{},[11,5848,5849],{},"Zero statycznego stanu. Zero teardownów. Zero skażenia między testami. Test jest szybszy, deterministyczny i testuje dokładnie jedną rzecz.",[27,5851,5853],{"id":5852},"kontener-di-robi-to-czego-chciałeś-od-singletona","Kontener DI robi to, czego chciałeś od Singletona",[11,5855,5856],{},"Developerzy sięgają po Singleton z jednego z trzech powodów: chcą leniwej inicjalizacji i nie chcą płacić za obiekt, dopóki nie jest potrzebny, chcą współdzielonego stanu żeby wszyscy czytali tę samą konfigurację i rozmawiali z tą samą pulą, albo chcą wygody i nie chcą przekazywać obiektu przez pół aplikacji. Kontener DI ze scope'ami lifetime rozwiązuje wszystkie trzy bez problemu globalnego stanu:",[38,5858,5860],{"className":59,"code":5859,"language":61,"meta":46,"style":46},"\u002F\u002F Singleton-scoped: instancjonowany raz przez cały czas życia kontenera\n$container->singleton(AppConfig::class, fn() => AppConfig::load());\n\n\u002F\u002F Transient: świeża instancja przy każdym rozwiązaniu\n$container->bind(PaymentGatewayInterface::class, StripeGateway::class);\n\n\u002F\u002F Request-scoped (w procesie długożyciowym jak Swoole\u002FRoadRunner):\n\u002F\u002F jedna instancja per request, zwalniana po zakończeniu requestu\n$container->scoped(UserSession::class, fn($c) => new UserSession(\n    $c->make(RequestInterface::class)\n));\n",[15,5861,5862,5867,5872,5876,5881,5886,5890,5895,5900,5905,5910],{"__ignoreMap":46},[65,5863,5864],{"class":67,"line":68},[65,5865,5866],{},"\u002F\u002F Singleton-scoped: instancjonowany raz przez cały czas życia kontenera\n",[65,5868,5869],{"class":67,"line":74},[65,5870,5871],{},"$container->singleton(AppConfig::class, fn() => AppConfig::load());\n",[65,5873,5874],{"class":67,"line":80},[65,5875,102],{"emptyLinePlaceholder":101},[65,5877,5878],{"class":67,"line":86},[65,5879,5880],{},"\u002F\u002F Transient: świeża instancja przy każdym rozwiązaniu\n",[65,5882,5883],{"class":67,"line":92},[65,5884,5885],{},"$container->bind(PaymentGatewayInterface::class, StripeGateway::class);\n",[65,5887,5888],{"class":67,"line":98},[65,5889,102],{"emptyLinePlaceholder":101},[65,5891,5892],{"class":67,"line":105},[65,5893,5894],{},"\u002F\u002F Request-scoped (w procesie długożyciowym jak Swoole\u002FRoadRunner):\n",[65,5896,5897],{"class":67,"line":111},[65,5898,5899],{},"\u002F\u002F jedna instancja per request, zwalniana po zakończeniu requestu\n",[65,5901,5902],{"class":67,"line":116},[65,5903,5904],{},"$container->scoped(UserSession::class, fn($c) => new UserSession(\n",[65,5906,5907],{"class":67,"line":122},[65,5908,5909],{},"    $c->make(RequestInterface::class)\n",[65,5911,5912],{"class":67,"line":127},[65,5913,5914],{},"));\n",[11,5916,5917],{},"Kontener to jedyny zasadny singleton w skali całej aplikacji. Wszystko inne dostaje swój lifetime zarządzany przez kontener. To nie jest semantyczna sztuczka, zmienia to operacyjne właściwości systemu. Możesz podmieniać implementacje w testach, resetować request-scoped stan między requestami w procesach długożyciowych i inspekcjonować graf zależności bez czytania każdej klasy.",[27,5919,5921],{"id":5920},"wzorzec-którego-faktycznie-używam","Wzorzec, którego faktycznie używam",[11,5923,5924,5925,5928],{},"Dla obiektów, które naprawdę potrzebują istnienia jeden-per-process, używam helpera ",[15,5926,5927],{},"once()"," zamiast klasy Singleton:",[38,5930,5932],{"className":59,"code":5931,"language":61,"meta":46,"style":46},"function once(string $key, callable $factory): mixed\n{\n    static $store = [];\n    return $store[$key] ??= $factory();\n}\n\n\u002F\u002F Użycie: leniwa inicjalizacja, współdzielona w procesie, brak class pollution\n$config = once(AppConfig::class, fn() => AppConfig::load());\n",[15,5933,5934,5939,5943,5948,5953,5957,5961,5966],{"__ignoreMap":46},[65,5935,5936],{"class":67,"line":68},[65,5937,5938],{},"function once(string $key, callable $factory): mixed\n",[65,5940,5941],{"class":67,"line":74},[65,5942,83],{},[65,5944,5945],{"class":67,"line":80},[65,5946,5947],{},"    static $store = [];\n",[65,5949,5950],{"class":67,"line":86},[65,5951,5952],{},"    return $store[$key] ??= $factory();\n",[65,5954,5955],{"class":67,"line":92},[65,5956,95],{},[65,5958,5959],{"class":67,"line":98},[65,5960,102],{"emptyLinePlaceholder":101},[65,5962,5963],{"class":67,"line":105},[65,5964,5965],{},"\u002F\u002F Użycie: leniwa inicjalizacja, współdzielona w procesie, brak class pollution\n",[65,5967,5968],{"class":67,"line":111},[65,5969,5970],{},"$config = once(AppConfig::class, fn() => AppConfig::load());\n",[11,5972,5973,5974,5977,5978,5981],{},"Sześćdziesiąt znaków. Przechodzi code review. Nie rozsywa ",[15,5975,5976],{},"private static $instance"," po całej bazie kodu. W teście możesz wyczyścić cały statyczny store jedną funkcją reset zamiast szukać metod ",[15,5979,5980],{},"resetInstance()"," w dwudziestu klasach.",[27,5983,3806],{"id":3805},[11,5985,5986],{},"Singleton to żółta flaga, nie czerwona. Moje pierwsze pytanie brzmi zawsze: jaki jest czas życia mutable state, który ten obiekt trzyma?",[11,5988,5989],{},"Jeśli odpowiedź brzmi \"trzyma tylko wartości konfiguracyjne ustawione w czasie konstrukcji, nigdy potem niemodyfikowane\" (wzorzec jest do obrony. Jeśli odpowiedź brzmi \"akumuluje stan między requestami) liczniki, cache'e, bufory\", to jest pułapka PHP-FPM. Naprawa to albo Redis\u002FMemcached dla stanu, który musi przeżyć poza pojedynczym workerem, albo request-scoped lifetime zarządzany przez kontener dla stanu, który powinien się resetować per request.",[11,5991,5992],{},"Wzorzec nie jest zły. Globalna mutable state jest zła. Singleton sprawia, że globalną mutable state łatwo wprowadzić i trudno zauważyć, aż do incydentu w piątek po południu.",[936,5994,938],{},{"title":46,"searchDepth":74,"depth":74,"links":5996},[5997,5998,5999,6000,6001,6002,6003],{"id":5214,"depth":74,"text":5215},{"id":5370,"depth":74,"text":5371},{"id":5384,"depth":74,"text":5385},{"id":5579,"depth":74,"text":5580},{"id":5852,"depth":74,"text":5853},{"id":5920,"depth":74,"text":5921},{"id":3805,"depth":74,"text":3806},"2024-10-15",{},{"x":6007,"y":6008,"depth":6009,"size":3242},0.22,0.34,1.2,[1154,976],{"title":5203,"description":5208},"global-state-mgmt","pl\u002Farticles\u002Fsingleton-pattern",[61,965,3838,967,6015,6016],"testing","php-fpm","v4.0.0","XgTsr6zrcI7qwArQwCR3VWBj7032Md5cByWlEt1tIAo",{"id":6020,"title":6021,"articleId":5190,"body":6022,"category":61,"codeLang":61,"date":6595,"deploys":68,"description":6596,"excerpt":949,"extension":950,"lang":951,"meta":6597,"navigation":101,"path":6598,"pos":6599,"readMin":133,"related":6602,"seo":6603,"service":6604,"stem":6605,"tags":6606,"version":970,"__hash__":6610},"articles_pl\u002Fpl\u002Farticles\u002Fstate-machine.md","Maszyny stanów w PHP: modelowanie cyklu życia zamówienia bez spaghetti",{"type":8,"value":6023,"toc":6588},[6024,6031,6046,6050,6061,6151,6161,6165,6417,6427,6431,6434,6437,6493,6496,6500,6503,6551,6567,6569,6586],[11,6025,6026,6027,6030],{},"Raport błędu brzmiał: \"Klient został obciążony dwa razy za to samo zamówienie.\" Zamówienie było w statusie ",[15,6028,6029],{},"payment_pending",". Timeout frontendu sprawił, że klient kliknął \"Zapłać\" ponownie. Drugie kliknięcie wyzwoliło nowy payment intent. Oba intenty zakończyły się sukcesem w ciągu 200 milisekund od siebie. Ani frontend, ani backend nie miał mechanizmu zapobiegającego drugiej płatności na zamówieniu będącym w trakcie procesu pobierania należności.",[11,6032,6033,6034,6036,6037,6040,6041,3147,6043,6045],{},"Naprawa nie była muteksem. Była maszyną stanów. Przejście z ",[15,6035,6029],{}," do ",[15,6038,6039],{},"paid"," może nastąpić tylko raz, a gdy już nastąpiło, przejście ",[15,6042,6029],{},[15,6044,6039],{}," po prostu nie istnieje. Druga próba płatności nie miała dokąd trafić.",[27,6047,6049],{"id":6048},"implicytna-maszyna-stanów-którą-już-masz","Implicytna maszyna stanów, którą już masz",[11,6051,6052,6053,6056,6057,6060],{},"Każda aplikacja z konceptami workflow (zamówienia, subskrypcje, tickety supportu, wnioski kredytowe) ma już maszynę stanów. Po prostu implicytną: kolumna ",[15,6054,6055],{},"status"," w bazie danych i instrukcje ",[15,6058,6059],{},"if"," rozrzucone po serwisach, które ją sprawdzają.",[38,6062,6064],{"className":59,"code":6063,"language":61,"meta":46,"style":46},"\u002F\u002F The implicit version — found in most codebases\nclass OrderService\n{\n    public function processPayment(Order $order, PaymentResult $result): void\n    {\n        if ($order->status !== 'payment_pending') {\n            throw new \\LogicException(\"Cannot process payment for order in status: {$order->status}\");\n        }\n        \u002F\u002F ...\n    }\n\n    public function cancel(Order $order): void\n    {\n        if (in_array($order->status, ['shipped', 'delivered', 'refunded'])) {\n            throw new \\LogicException(\"Cannot cancel order in status: {$order->status}\");\n        }\n        \u002F\u002F ...\n    }\n}\n",[15,6065,6066,6071,6076,6080,6085,6089,6094,6099,6103,6108,6112,6116,6121,6125,6130,6135,6139,6143,6147],{"__ignoreMap":46},[65,6067,6068],{"class":67,"line":68},[65,6069,6070],{},"\u002F\u002F The implicit version — found in most codebases\n",[65,6072,6073],{"class":67,"line":74},[65,6074,6075],{},"class OrderService\n",[65,6077,6078],{"class":67,"line":80},[65,6079,83],{},[65,6081,6082],{"class":67,"line":86},[65,6083,6084],{},"    public function processPayment(Order $order, PaymentResult $result): void\n",[65,6086,6087],{"class":67,"line":92},[65,6088,136],{},[65,6090,6091],{"class":67,"line":98},[65,6092,6093],{},"        if ($order->status !== 'payment_pending') {\n",[65,6095,6096],{"class":67,"line":105},[65,6097,6098],{},"            throw new \\LogicException(\"Cannot process payment for order in status: {$order->status}\");\n",[65,6100,6101],{"class":67,"line":111},[65,6102,647],{},[65,6104,6105],{"class":67,"line":116},[65,6106,6107],{},"        \u002F\u002F ...\n",[65,6109,6110],{"class":67,"line":122},[65,6111,172],{},[65,6113,6114],{"class":67,"line":127},[65,6115,102],{"emptyLinePlaceholder":101},[65,6117,6118],{"class":67,"line":133},[65,6119,6120],{},"    public function cancel(Order $order): void\n",[65,6122,6123],{"class":67,"line":139},[65,6124,136],{},[65,6126,6127],{"class":67,"line":145},[65,6128,6129],{},"        if (in_array($order->status, ['shipped', 'delivered', 'refunded'])) {\n",[65,6131,6132],{"class":67,"line":151},[65,6133,6134],{},"            throw new \\LogicException(\"Cannot cancel order in status: {$order->status}\");\n",[65,6136,6137],{"class":67,"line":157},[65,6138,647],{},[65,6140,6141],{"class":67,"line":163},[65,6142,6107],{},[65,6144,6145],{"class":67,"line":169},[65,6146,172],{},[65,6148,6149],{"class":67,"line":175},[65,6150,95],{},[11,6152,6153,6154,6157,6158,6160],{},"To działa, dopóki nowy developer nie doda logiki ",[15,6155,6156],{},"cancel()"," w kontrolerze, nie zapomni sprawdzić statusu i zamówienie zostanie anulowane po tym, jak już zostało wysłane. Dozwolone przejścia nigdzie nie są explicite. Istnieją tylko jako suma wszystkich checków ",[15,6159,6059],{}," w wszystkich metodach.",[27,6162,6164],{"id":6163},"uczynienie-maszyny-explicytną","Uczynienie maszyny explicytną",[38,6166,6168],{"className":59,"code":6167,"language":61,"meta":46,"style":46},"enum OrderStatus: string\n{\n    case Draft          = 'draft';\n    case PaymentPending = 'payment_pending';\n    case Paid           = 'paid';\n    case Shipped        = 'shipped';\n    case Delivered      = 'delivered';\n    case Cancelled      = 'cancelled';\n    case Refunded       = 'refunded';\n}\n\nfinal class OrderStateMachine\n{\n    \u002F\u002F The complete allowed transition graph — one place, one truth\n    private const TRANSITIONS = [\n        OrderStatus::Draft->value => [\n            OrderStatus::PaymentPending,\n            OrderStatus::Cancelled,\n        ],\n        OrderStatus::PaymentPending->value => [\n            OrderStatus::Paid,\n            OrderStatus::Cancelled,\n        ],\n        OrderStatus::Paid->value => [\n            OrderStatus::Shipped,\n            OrderStatus::Refunded,\n        ],\n        OrderStatus::Shipped->value  => [OrderStatus::Delivered],\n        OrderStatus::Delivered->value => [OrderStatus::Refunded],\n        OrderStatus::Cancelled->value => [],  \u002F\u002F terminal\n        OrderStatus::Refunded->value  => [],  \u002F\u002F terminal\n    ];\n\n    public function transition(Order $order, OrderStatus $to): void\n    {\n        if (!in_array($to, self::TRANSITIONS[$order->status->value] ?? [], strict: true)) {\n            throw new InvalidTransitionException(\n                from: $order->status,\n                to:   $to,\n                orderId: $order->id,\n            );\n        }\n\n        $previousStatus        = $order->status;\n        $order->status         = $to;\n        $order->status_changed_at = now();\n\n        \u002F\u002F Dispatch transition event — side effects happen in listeners, not here\n        event(new OrderStatusTransitioned(order: $order, from: $previousStatus, to: $to));\n    }\n}\n",[15,6169,6170,6175,6179,6184,6189,6194,6199,6204,6209,6214,6218,6222,6227,6231,6236,6241,6246,6251,6256,6261,6266,6271,6275,6279,6284,6289,6294,6298,6303,6308,6313,6318,6323,6327,6332,6336,6341,6346,6351,6356,6361,6366,6370,6374,6379,6384,6390,6395,6401,6407,6412],{"__ignoreMap":46},[65,6171,6172],{"class":67,"line":68},[65,6173,6174],{},"enum OrderStatus: string\n",[65,6176,6177],{"class":67,"line":74},[65,6178,83],{},[65,6180,6181],{"class":67,"line":80},[65,6182,6183],{},"    case Draft          = 'draft';\n",[65,6185,6186],{"class":67,"line":86},[65,6187,6188],{},"    case PaymentPending = 'payment_pending';\n",[65,6190,6191],{"class":67,"line":92},[65,6192,6193],{},"    case Paid           = 'paid';\n",[65,6195,6196],{"class":67,"line":98},[65,6197,6198],{},"    case Shipped        = 'shipped';\n",[65,6200,6201],{"class":67,"line":105},[65,6202,6203],{},"    case Delivered      = 'delivered';\n",[65,6205,6206],{"class":67,"line":111},[65,6207,6208],{},"    case Cancelled      = 'cancelled';\n",[65,6210,6211],{"class":67,"line":116},[65,6212,6213],{},"    case Refunded       = 'refunded';\n",[65,6215,6216],{"class":67,"line":122},[65,6217,95],{},[65,6219,6220],{"class":67,"line":127},[65,6221,102],{"emptyLinePlaceholder":101},[65,6223,6224],{"class":67,"line":133},[65,6225,6226],{},"final class OrderStateMachine\n",[65,6228,6229],{"class":67,"line":139},[65,6230,83],{},[65,6232,6233],{"class":67,"line":145},[65,6234,6235],{},"    \u002F\u002F The complete allowed transition graph — one place, one truth\n",[65,6237,6238],{"class":67,"line":151},[65,6239,6240],{},"    private const TRANSITIONS = [\n",[65,6242,6243],{"class":67,"line":157},[65,6244,6245],{},"        OrderStatus::Draft->value => [\n",[65,6247,6248],{"class":67,"line":163},[65,6249,6250],{},"            OrderStatus::PaymentPending,\n",[65,6252,6253],{"class":67,"line":169},[65,6254,6255],{},"            OrderStatus::Cancelled,\n",[65,6257,6258],{"class":67,"line":175},[65,6259,6260],{},"        ],\n",[65,6262,6263],{"class":67,"line":180},[65,6264,6265],{},"        OrderStatus::PaymentPending->value => [\n",[65,6267,6268],{"class":67,"line":185},[65,6269,6270],{},"            OrderStatus::Paid,\n",[65,6272,6273],{"class":67,"line":191},[65,6274,6255],{},[65,6276,6277],{"class":67,"line":196},[65,6278,6260],{},[65,6280,6281],{"class":67,"line":202},[65,6282,6283],{},"        OrderStatus::Paid->value => [\n",[65,6285,6286],{"class":67,"line":207},[65,6287,6288],{},"            OrderStatus::Shipped,\n",[65,6290,6291],{"class":67,"line":212},[65,6292,6293],{},"            OrderStatus::Refunded,\n",[65,6295,6296],{"class":67,"line":217},[65,6297,6260],{},[65,6299,6300],{"class":67,"line":223},[65,6301,6302],{},"        OrderStatus::Shipped->value  => [OrderStatus::Delivered],\n",[65,6304,6305],{"class":67,"line":229},[65,6306,6307],{},"        OrderStatus::Delivered->value => [OrderStatus::Refunded],\n",[65,6309,6310],{"class":67,"line":235},[65,6311,6312],{},"        OrderStatus::Cancelled->value => [],  \u002F\u002F terminal\n",[65,6314,6315],{"class":67,"line":241},[65,6316,6317],{},"        OrderStatus::Refunded->value  => [],  \u002F\u002F terminal\n",[65,6319,6320],{"class":67,"line":246},[65,6321,6322],{},"    ];\n",[65,6324,6325],{"class":67,"line":251},[65,6326,102],{"emptyLinePlaceholder":101},[65,6328,6329],{"class":67,"line":256},[65,6330,6331],{},"    public function transition(Order $order, OrderStatus $to): void\n",[65,6333,6334],{"class":67,"line":261},[65,6335,136],{},[65,6337,6338],{"class":67,"line":267},[65,6339,6340],{},"        if (!in_array($to, self::TRANSITIONS[$order->status->value] ?? [], strict: true)) {\n",[65,6342,6343],{"class":67,"line":272},[65,6344,6345],{},"            throw new InvalidTransitionException(\n",[65,6347,6348],{"class":67,"line":278},[65,6349,6350],{},"                from: $order->status,\n",[65,6352,6353],{"class":67,"line":283},[65,6354,6355],{},"                to:   $to,\n",[65,6357,6358],{"class":67,"line":288},[65,6359,6360],{},"                orderId: $order->id,\n",[65,6362,6363],{"class":67,"line":293},[65,6364,6365],{},"            );\n",[65,6367,6368],{"class":67,"line":299},[65,6369,647],{},[65,6371,6372],{"class":67,"line":305},[65,6373,102],{"emptyLinePlaceholder":101},[65,6375,6376],{"class":67,"line":311},[65,6377,6378],{},"        $previousStatus        = $order->status;\n",[65,6380,6381],{"class":67,"line":316},[65,6382,6383],{},"        $order->status         = $to;\n",[65,6385,6387],{"class":67,"line":6386},46,[65,6388,6389],{},"        $order->status_changed_at = now();\n",[65,6391,6393],{"class":67,"line":6392},47,[65,6394,102],{"emptyLinePlaceholder":101},[65,6396,6398],{"class":67,"line":6397},48,[65,6399,6400],{},"        \u002F\u002F Dispatch transition event — side effects happen in listeners, not here\n",[65,6402,6404],{"class":67,"line":6403},49,[65,6405,6406],{},"        event(new OrderStatusTransitioned(order: $order, from: $previousStatus, to: $to));\n",[65,6408,6410],{"class":67,"line":6409},50,[65,6411,172],{},[65,6413,6415],{"class":67,"line":6414},51,[65,6416,95],{},[11,6418,6419,6422,6423,6426],{},[15,6420,6421],{},"TRANSITIONS"," to cała specyfikacja twojego workflow. Żeby dodać nowe przejście, dodajesz jeden wpis. Żeby zrozumieć które przejścia są możliwe z danego stanu, czytasz jedną tablicę. Żeby udowodnić, że ",[15,6424,6425],{},"cancelled → refunded"," jest niemożliwe, patrzysz na pustą tablicę.",[27,6428,6430],{"id":6429},"efekty-uboczne-należą-do-listenerów","Efekty uboczne należą do listenerów",[11,6432,6433],{},"Klasyczny błąd po przyjęciu explicytnych maszyn stanów to wkładanie efektów ubocznych do metody transition. Maszyna stanów jest teraz sprzężona z emailem, inventory i analytics, testowanie przejścia wymaga mockowania trzech zależności. Co ważniejsze: jeśli wysyłanie emaila się nie powiedzie, czy zamówienie pozostaje nieopłacone?",[11,6435,6436],{},"Zamiast tego emituj event i pozwól listenerom decydować:",[38,6438,6440],{"className":59,"code":6439,"language":61,"meta":46,"style":46},"\u002F\u002F In OrderEventSubscriber\npublic function onOrderStatusTransitioned(OrderStatusTransitioned $event): void\n{\n    if ($event->to !== OrderStatus::Paid) {\n        return;\n    }\n\n    \u002F\u002F Each listener is independently retryable, independently testable\n    $this->emailQueue->dispatch(new SendPaymentConfirmationEmail($event->order->id));\n    $this->inventoryQueue->dispatch(new ReserveOrderItems($event->order->id));\n}\n",[15,6441,6442,6447,6452,6456,6461,6466,6470,6474,6479,6484,6489],{"__ignoreMap":46},[65,6443,6444],{"class":67,"line":68},[65,6445,6446],{},"\u002F\u002F In OrderEventSubscriber\n",[65,6448,6449],{"class":67,"line":74},[65,6450,6451],{},"public function onOrderStatusTransitioned(OrderStatusTransitioned $event): void\n",[65,6453,6454],{"class":67,"line":80},[65,6455,83],{},[65,6457,6458],{"class":67,"line":86},[65,6459,6460],{},"    if ($event->to !== OrderStatus::Paid) {\n",[65,6462,6463],{"class":67,"line":92},[65,6464,6465],{},"        return;\n",[65,6467,6468],{"class":67,"line":98},[65,6469,172],{},[65,6471,6472],{"class":67,"line":105},[65,6473,102],{"emptyLinePlaceholder":101},[65,6475,6476],{"class":67,"line":111},[65,6477,6478],{},"    \u002F\u002F Each listener is independently retryable, independently testable\n",[65,6480,6481],{"class":67,"line":116},[65,6482,6483],{},"    $this->emailQueue->dispatch(new SendPaymentConfirmationEmail($event->order->id));\n",[65,6485,6486],{"class":67,"line":122},[65,6487,6488],{},"    $this->inventoryQueue->dispatch(new ReserveOrderItems($event->order->id));\n",[65,6490,6491],{"class":67,"line":127},[65,6492,95],{},[11,6494,6495],{},"Nieudany job email nie cofa statusu płatności. Zamówienie jest opłacone. Email spróbuje ponownie. To są osobne sprawy.",[27,6497,6499],{"id":6498},"bezpieczne-persystowanie-stanu","Bezpieczne persystowanie stanu",[11,6501,6502],{},"W systemie współbieżnym (a każda aplikacja webowa nim jest) dwa requesty mogą jednocześnie próbować dokonać przejścia tego samego zamówienia. Ochrona na poziomie bazy danych:",[38,6504,6506],{"className":59,"code":6505,"language":61,"meta":46,"style":46},"public function transitionWithLock(int $orderId, OrderStatus $to): void\n{\n    DB::transaction(function () use ($orderId, $to) {\n        \u002F\u002F FOR UPDATE locks the row for the duration of this transaction\n        $order = Order::where('id', $orderId)->lockForUpdate()->firstOrFail();\n        $this->stateMachine->transition($order, $to);\n        $order->save();\n    });\n}\n",[15,6507,6508,6513,6517,6522,6527,6532,6537,6542,6547],{"__ignoreMap":46},[65,6509,6510],{"class":67,"line":68},[65,6511,6512],{},"public function transitionWithLock(int $orderId, OrderStatus $to): void\n",[65,6514,6515],{"class":67,"line":74},[65,6516,83],{},[65,6518,6519],{"class":67,"line":80},[65,6520,6521],{},"    DB::transaction(function () use ($orderId, $to) {\n",[65,6523,6524],{"class":67,"line":86},[65,6525,6526],{},"        \u002F\u002F FOR UPDATE locks the row for the duration of this transaction\n",[65,6528,6529],{"class":67,"line":92},[65,6530,6531],{},"        $order = Order::where('id', $orderId)->lockForUpdate()->firstOrFail();\n",[65,6533,6534],{"class":67,"line":98},[65,6535,6536],{},"        $this->stateMachine->transition($order, $to);\n",[65,6538,6539],{"class":67,"line":105},[65,6540,6541],{},"        $order->save();\n",[65,6543,6544],{"class":67,"line":111},[65,6545,6546],{},"    });\n",[65,6548,6549],{"class":67,"line":116},[65,6550,95],{},[11,6552,6553,6556,6557,6559,6560,6562,6563,6566],{},[15,6554,6555],{},"lockForUpdate()"," uniemożliwia drugiemu równoległemu requestowi odczyt zamówienia ",[15,6558,6029],{}," dopóki pierwsza transakcja nie zostanie zatwierdzona. Drugi request odczyta wtedy zamówienie ",[15,6561,6039],{},", nie znajdzie żadnego dozwolonego przejścia i rzuci ",[15,6564,6565],{},"InvalidTransitionException",". Zero podwójnego obciążenia.",[27,6568,3806],{"id":3805},[11,6570,6571,6572,6574,6575,6578,6579,748,6582,6585],{},"Kolumna ",[15,6573,6055],{}," bez odpowiadającej definicji maszyny stanów to zobowiązanie czekające na wykorzystanie. Moje pytanie: czy aplikacja może osiągnąć nieprawidłową kombinację stanów? Zamówienie z ",[15,6576,6577],{},"status = 'shipped'"," i bez adresu wysyłki powinno być niemożliwe. Zamówienie z ",[15,6580,6581],{},"status = 'refunded'",[15,6583,6584],{},"payment_status = 'pending'"," jest również niemożliwe, jeśli maszyna jest poprawnie zdefiniowana. Jeśli odpowiedź na \"czy to może osiągnąć nieprawidłowy stan\" brzmi \"teoretycznie, jeśli dwie rzeczy zdarzą się w odpowiedniej kolejności\", masz implicytną maszynę. Uczyń ją explicytną.",[936,6587,938],{},{"title":46,"searchDepth":74,"depth":74,"links":6589},[6590,6591,6592,6593,6594],{"id":6048,"depth":74,"text":6049},{"id":6163,"depth":74,"text":6164},{"id":6429,"depth":74,"text":6430},{"id":6498,"depth":74,"text":6499},{"id":3805,"depth":74,"text":3806},"2024-02-10","Raport błędu brzmiał: \"Klient został obciążony dwa razy za to samo zamówienie.\" Zamówienie było w statusie payment_pending. Timeout frontendu sprawił, że klient kliknął \"Zapłać\" ponownie. Drugie kliknięcie wyzwoliło nowy payment intent. Oba intenty zakończyły się sukcesem w ciągu 200 milisekund od siebie. Ani frontend, ani backend nie miał mechanizmu zapobiegającego drugiej płatności na zamówieniu będącym w trakcie procesu pobierania należności.",{},"\u002Fpl\u002Farticles\u002Fstate-machine",{"x":6600,"y":6601,"depth":4579,"size":950},0.82,0.4,[3244,959],{"title":6021,"description":6596},"order-lifecycle","pl\u002Farticles\u002Fstate-machine",[61,965,5190,6607,6608,6609],"fsm","ddd","order-management","bmYz88WGJ7N36p264rYKjTLdKNeL5yTl_kbYbepPVoU",1779457966327]