`DateTime` змінює сам себе на кожному `modify()`/`add()`, тому одна змінна тихо міняється в усіх місцях, де на неї посилаються; `DateTimeImmutable` має той самий API, але повертає новий екземпляр. Час зберігають як UTC-мітку, а пояс користувача застосовують лише на виводі через `setTimezone()` і завжди за ідентифікатором зони, а не за зсувом.
Питання на співбесіду PHP-розробника
181 питання з відповідями, флеш-картками й тестом. Фреймворки, SQL, суміжні технології: те, що реально питають на співбесідах в українських компаніях.
Eloquent і Query Builder генерують той самий SQL, різниця починається після того, як рядки прийшли: `get()` на моделі створює обʼєкт на рядок, копіює атрибути в `$original` і тримає масив `$relations`, а базовий білдер віддає `stdClass`. Для звітів і вивантажень беруть `toBase()` або `DB::table()`, агрегують у SQL, а великі набори проганяють через `chunkById()`/`lazyById()`, не через `chunk()`.
Працюють у короткій feature-гілці від main, підтягують чужі зміни через rebase локально, у main вливають через pull request і merge, а втрачені після rebase чи force push коміти дістають із git reflog.
Бізнес-логіка живе в шарі, який нічого не знає про HTTP: у моделі домену або в use case (сервісі), який модель оркеструє. Контролеру лишаються переклад запиту в дані, виклик одного use case і вибір відповіді, бо контролер прив'язаний до транспорту і тому непридатний для повторного використання й дорогий у тестуванні.
Тема відповідає за вивід, плагін - за дані й поведінку: усе, що має жити довше за оформлення (CPT, шорткоди, інтеграції, cron), винось у плагін, бо functions.php вимикається разом зі зміною теми.
composer.json описує допустимі діапазони версій, composer.lock фіксує точні версії, які реально встановлені; install ставить строго з lock і дає однакові залежності всюди, update перераховує діапазони й переписує lock.
Оновлення теми видаляє її теку цілком і розпаковує нову, тому будь-яка правка всередині батьківської теми зникає; дочірня тема живе в окремій теці, яку оновлювач не чіпає, і має містити мінімум власного коду, бо кожен скопійований шаблон стає форком, що застаріває.
Зовнішній ключ - це перевірка, яку робить сама база на кожному записі, тому посилання на неіснуючий рядок неможливе навіть із консолі чи з чужого скрипта. CASCADE беруть для рядків, які не живуть без батька, RESTRICT - коли видалення має бути помилкою, SET NULL - для необовʼязкового звʼязку в nullable-колонці.
Фасад це статичний фасад над контейнером: `__callStatic` бере обʼєкт із контейнера за рядковим акцесором і викликає на ньому звичайний метод, тому `swap()`, `shouldReceive()` і всі `fake()` працюють. У Laravel-склейці (контролери, middleware, роути, Blade) фасад економить друкування, а в доменному коді залежності мають бути в конструкторі, бо саме сигнатура класу є списком того, що йому потрібно.
WHERE фільтрує рядки до групування й не бачить агрегатів, GROUP BY згортає рядки в групи, HAVING фільтрує вже готові групи за значенням агрегату.
Усі вони test doubles, обʼєкти, що в тесті замінюють справжню залежність. Dummy лише заповнює параметр, stub повертає заготовлені відповіді, spy ще й записує виклики для перевірки після дії, mock отримує очікування до дії й сам падає, якщо їх не виконано, fake це робоча спрощена реалізація, як репозиторій на масиві. Stub і fake ведуть до перевірки стану, spy і mock до перевірки поведінки.
Оператор == порівнює значення з приведенням типів, === порівнює і значення, і тип без жодних перетворень.
INNER JOIN лишає лише ті рядки, для яких знайшлася пара в обох таблицях; LEFT JOIN лишає всі рядки лівої таблиці, підставляючи NULL там, де пари немає.
Action виконує побічну дію в певній точці життєвого циклу й нічого не повертає; filter отримує значення, змінює його й обовʼязково повертає.
Інтерфейс — це публічний контракт без стану, і їх можна реалізувати скільки завгодно; абстрактний клас — частково готова реалізація зі станом, конструктором і protected-методами, але успадкувати можна лише один.
Рядок у PHP - це послідовність байтів без інформації про кодування, тому `strlen` рахує байти, а кирилиця в UTF-8 займає по два байти на літеру. Символи рахують і ріжуть `mb_`-функції (`mb_strlen`, `mb_substr`, `mb_strtolower`), а регулярні вирази переводить у режим символів модифікатор `u`.
Float у PHP - це двійковий double за IEEE 754, у якому 0.1, 0.2 чи 19.99 не мають точного представлення, тому суми накопичують похибку. Гроші тримають у цілих копійках (int) або в рядках з DECIMAL/numeric у базі, рахують через bcmath чи `BcMath\Number` з PHP 8.4, а валюту й правило округлення вважають частиною значення.
CSRF змушує браузер залогіненої людини відправити запит на ваш сайт зі сторінки зловмисника: cookie сесії браузер додає сам, і сервер бачить легітимного користувача. Laravel перевіряє кожен POST, PUT, PATCH і DELETE у групі `web`: запит проходить, якщо `Sec-Fetch-Site` каже `same-origin` або якщо токен з `_token`, `X-CSRF-TOKEN` чи `X-XSRF-TOKEN` збігається з токеном сесії, інакше 419. Токен не потрібен там, де автентифікація не їде в cookie, наприклад з Bearer-токеном у заголовку.
XSS трапляється, коли дані користувача потрапляють у HTML-сторінку як розмітка або код і браузер їх виконує від імені того, хто сторінку відкрив. Лікується екрануванням на виводі під конкретний контекст: `{{ }}` у Blade викликає `htmlspecialchars()` з `ENT_QUOTES`, `{!! !!}` не екранує нічого, в атрибут `href` треба пускати лише `http(s)`-посилання, а в `<script>` дані передають через `Js::from()` або `@json`. CSP додає другий шар, якщо перший десь протік.
Enum (PHP 8.1+) — тип із фіксованим переліком case-ів; pure enum не має скалярного значення, backed enum привʼязаний до int або string і вміє from()/tryFrom().
Mass assignment трапляється, коли модель заповнюється масивом із запиту цілком: `User::create($request->all())` запише і `is_admin`, і `company_id`, якщо користувач їх дописав. Захист у два шари: модель дозволяє масове заповнення лише полів із `$fillable` (або `#[Fillable]` у Laravel 13), а контролер передає в неї `$request->validated()`, тобто лише поля, для яких є правила. Значення, які виставляє сервер (роль, власник, статус), присвоюють явно.
Prepared statement - це запит із плейсхолдерами (`?` або `:name`), у якому дані передаються окремо від тексту SQL, тому вони ніколи не парсяться як команда. Захист працює саме через це розділення, а не через екранування лапок; у PDO MySQL за замовчуванням увімкнена емуляція, тому перевіряйте `PDO::ATTR_EMULATE_PREPARES` і завжди задавайте `charset` у DSN.
readonly дозволяє записати типізовану властивість рівно один раз зі скоупу класу; з PHP 8.2 модифікатор можна поставити на весь клас.
Scope - це іменований шматок запиту: локальний ви викликаєте самі (`Post::published()`), глобальний Eloquent додає в кожен запит моделі, як `SoftDeletes`. Accessor через `Attribute::get()` і cast працюють уже після вибірки, у PHP: каст перетворює значення колонки в обидва боки, аксесор рахує похідний атрибут, якого в таблиці немає, тому фільтрувати й сортувати за ним не можна, а `$appends` додає його в `toArray()` і `toJson()`.
Контейнер відповідає за створення обʼєктів і підстановку залежностей: читає типи параметрів конструктора через рефлексію і рекурсивно створює їх.
Впровадження залежностей означає, що клас отримує готові співпрацівники ззовні (найчастіше через конструктор), а не створює їх сам. Контейнер це фабрика з реєстром, яка вміє зібрати граф обʼєктів за типами параметрів конструктора і підставити реалізацію там, де оголошено інтерфейс.
Транзакція - це група команд, які застосовуються цілком або не застосовуються зовсім; ACID гарантує атомарність, дотримання оголошених обмежень, ізоляцію на заданому рівні й збереження закомічених даних після падіння, але за умовчанням ізоляція не серіалізовна (PostgreSQL - READ COMMITTED, MySQL - REPEATABLE READ), а ROLLBACK не скасовує нічого поза базою.
Індекс - це окрема відсортована структура (зазвичай B-tree), яка дає базі знайти потрібні рядки без повного скану. Він виграє на селективних умовах і на ORDER BY, а програє на колонках з кількома значеннями, бо кожен індекс сповільнює запис і займає місце.
NULL — це «невідомо», а не значення: будь-яке порівняння з ним дає UNKNOWN і рядок не проходить WHERE, агрегати мовчки пропускають NULL замість того щоб рахувати їх нулями, а UNIQUE вважає різні NULL різними, тому дублікати проходять.
HttpKernel перетворює Request на Response, дорогою розсилаючи події: kernel.request (маршрутизація), kernel.controller і kernel.controller_arguments (резолвінг контролера й аргументів), kernel.view (якщо контролер повернув не Response), kernel.response, kernel.terminate після відправлення відповіді.
З PHP 7 усе, що можна кинути, реалізує інтерфейс `Throwable`, під яким дві незалежні гілки: `Exception` (очікувані ситуації рантайму) і `Error` (помилки програміста: `TypeError`, `ValueError`, `DivisionByZeroError`). `finally` виконується завжди, окрім `exit()` і фатального завершення, а warnings лишилися для ситуацій, які PHP історично вміє пережити, і перетворюються на винятки через `set_error_handler`.
Yii2-застосунок - це один обʼєкт `Yii::$app`, створений вхідним скриптом `web/index.php` з масиву конфігурації: він піднімає аліаси й error handler, виконує `bootstrap`, просить `UrlManager` розібрати URL на маршрут, за маршрутом знаходить модуль, контролер і дію, а результат віддає через компонент `response`.
Doctrine це Data Mapper: сутність - звичайний PHP-клас без save() і find(), а стан у базу переносить EntityManager (persist реєструє новий об'єкт, flush виконує SQL), тоді як репозиторій відповідає лише за запити. В Active Record модель сама знає про з'єднання, тому домен і таблиця злиті в один клас.
Compose описує сервіси локального стенду: PHP (php-fpm поруч з nginx або один контейнер FrankenPHP), база, кеш; код заходить через bind mount, а vendor і дані БД живуть у named volumes, бо саме bind mount повільний на macOS та Windows; Xdebug ставлять в образ, але тримають у режимі off і вмикають змінною XDEBUG_MODE на час сесії.
Команда - це сервіс з атрибутом #[AsCommand]: у Symfony 7.3+ достатньо класу з методом __invoke() і параметрами під #[Argument] та #[Option], вивід іде через SymfonyStyle, тест пишеться на CommandTester, а від паралельних запусків з cron захищає Lock.
Обидва шляхи запускають один і той самий Validator: `$request->validate()` робить це всередині методу контролера, Form Request — під час резолву аргументів, ще до входу в метод, і додає authorize(), prepareForValidation() і власні повідомлення. Інлайн — для двох-трьох простих правил, Form Request — щойно зʼявляються авторизація, нормалізація вхідних даних або повторне використання правил.
Хешувати треба `password_hash()` і перевіряти `password_verify()`: функція сама генерує криптостійку сіль і кладе її разом з алгоритмом і вартістю в один рядок. Окрема колонка для солі не потрібна, а параметри піднімають через `password_needs_rehash()` під час успішного логіну.
ActiveRecord у Yii2 — це надбудова над Query Builder: `ActiveQuery` успадковує `yii\db\Query`, будує той самий SQL, але замість масивів повертає обʼєкти моделей із валідацією, подіями, поведінками й звʼязками; Query Builder беруть там, де обʼєкти не потрібні — агрегати, звіти, масові операції.
Autowiring підставляє залежності за типом параметра конструктора, autoconfigure навішує теги за реалізованим інтерфейсом чи атрибутом; обидва мовчать там, де тип нічого не каже: скаляри й env, класи з vendor, дві реалізації одного інтерфейсу та теги з атрибутами вимагають явного запису в services.yaml або атрибута #[Autowire].
Замикання — це обʼєкт класу Closure; `function` захоплює зовнішні змінні лише явно через `use` (за значенням або за посиланням через `&`), а стрілочна функція `fn` (PHP 7.4+) захоплює їх автоматично й завжди за значенням.
`rules()` описує, що і як перевіряти, `scenarios()` - які атрибути активні в поточному сценарії й котрі з них safe; `load()` присвоює лише safe-атрибути, тому поле без жодного правила (або з `!` у сценарії) у модель просто не потрапить.
У cookie браузер тримає лише ідентифікатор (типово `PHPSESSID`), а самі дані `$_SESSION` лежать на сервері: у файлах `session.save_path`, у Redis або у власному `SessionHandlerInterface`. Cookie сесії має бути `HttpOnly`, `Secure` і `SameSite`, а API замість сесії краще авторизувати токеном у заголовку `Authorization`, бо cookie шлеться браузером автоматично й тягне за собою CSRF і прив'язку до сервера.
Composer реєструє свій завантажувач через `spl_autoload_register`, і при першій згадці класу PSR-4 перетворює його повне імʼя на шлях: найдовший збіг префікса замінюється базовою текою, решта імені стає підтеками й файлом. Classmap натомість заздалегідь сканує теки й будує таблицю «клас - файл», а `dump-autoload -o` перетворює PSR-4-правила на таку саму таблицю, щоб прибрати пошук по файловій системі.
Роутер зіставляє URI з маршрутами в порядку реєстрації, а перетворення `{post}` на об'єкт робить не контролер, а middleware `SubstituteBindings`: воно бере `getRouteKeyName()`, робить `firstOrFail()` і кидає 404 ще до вашого коду. Неявне зв'язування шукає по всій таблиці й ігнорує ієрархію URL, поки не ввімкнути `scopeBindings()`, а `SoftDeletes` мовчки вирізає видалені записи, доки на маршруті не стоїть `withTrashed()`.
Після резолву запиту wp-includes/template-loader.php перебирає умовні теги (is_404, is_search, is_front_page, is_home, is_singular, is_archive…) і для першого збігу будує список кандидатів від найконкретнішого до index.php; locate_template() шукає кожного кандидата спершу в child theme, потім у parent, тому перекриття — це файл із такою самою назвою в дочірній темі.
public/index.php підключає автолоадер і bootstrap/app.php, зібраний застосунок віддає запит HTTP-ядру: воно виконує бутстрапери (env, config, фасади, провайдери), проганяє запит через глобальні middleware, роутер знаходить маршрут, запускає його middleware й контролер, а відповідь повертається тим самим ланцюгом назад до send() і terminate().
Вид тесту визначає, скільки справжнього коду бере участь у перевірці: unit бере клас у памʼяті, integration додає одну справжню залежність (базу, кеш, файли), feature проганяє HTTP-запит крізь увесь застосунок, e2e клікає в браузері. Піраміда радить багато unit-тестів і мало повільних, трофей ставить у центр інтеграційні; пропорцію обирають за тим, де в проєкті живуть баги.
Магічні методи - це хуки, які движок кличе замість помилки: `__get`/`__set`/`__call` підміняють доступ до неоголошених членів класу, `__invoke` робить обʼєкт callable, `__toString` дає рядкове представлення, `__construct` і `__destruct` тримають життєвий цикл. За `__get`/`__set`/`__call` платять автодоповненням, типами й рефакторингом, тому їх лишають для проксі та ORM, а в звичайному коді оголошують властивості й методи явно.
Тип звʼязку визначає не «логіка», а те, де лежить ключ: hasOne/hasMany — ключ у дочірній таблиці, belongsTo — у поточній, belongsToMany — у проміжній, morph* — пара колонок *_id і *_type, коли дитина належить кільком типам батьків.
Адаптер підганяє один чужий обʼєкт під інтерфейс, якого вже чекає ваш код, і поведінки не змінює. Фасад вводить новий, простіший вхід до кількох сервісів підсистеми й сам знає порядок їх виклику. Фасади Laravel (`Cache::`, `Mail::`) - це статичні проксі до одного сервісу з контейнера, підсистеми за ними немає, тож з GoF Facade їх повʼязує лише назва.
Структура в них однакова: контекст тримає обʼєкт за інтерфейсом і делегує йому роботу. Різниця в тому, хто і коли цей обʼєкт міняє: стратегію обирає клієнт або конфіг, і вона нічого не знає про інші, а стан обʼєкт перемикає сам після дії, і кожен стан знає, куди з нього можна перейти.
PHP 8.0 дав union (`int|string`), `mixed` як верхній тип і `static` як тип повернення; 8.1 додав `never` і intersection (`A&B`); 8.2 дозволив DNF - комбінації виду `(A&B)|null`. Повернення в нащадку можна звужувати, параметри розширювати, типи властивостей мають лишатися незмінними.
Query Builder передає через плейсхолдери лише значення: аргументи `where()`, `whereIn()`, `insert()`, `update()`. Усе, що ви пишете рядком у `DB::raw()`, `whereRaw()`, `selectRaw()`, `orderByRaw()`, `havingRaw()`, `DB::select()` чи `DB::statement()`, іде в SQL як є, тому дані туди передають лише масивом біндінгів. Імена колонок і напрям сортування не біндяться взагалі, для них потрібен перелік дозволених значень.
Flaky-тест то проходить, то падає на тому самому коді. Найчастіші причини: прихований час (`now()`, `new DateTimeImmutable()`), випадкові дані, залежність від порядку тестів і спільного стану, мережа й паралельність. Час роблять явною залежністю (порт Clock, PSR-20, `FrozenClock`) або зупиняють через `travelTo()`/`freezeTime()`, випадковість передають як `Random\Randomizer` з seed, а порядок перевіряють запуском з `--order-by=random`.
Репозиторій виправданий, коли він виглядає як колекція агрегатів у мові домену й тримає доменний шар без Illuminate чи Doctrine; він шкодить, коли перетворюється на generic CRUD-обгортку, що повертає той самий Builder або модель і лише додає шар.
PostgreSQL дає транзакційний DDL, багату систему типів (масиви, діапазони, справжні ENUM-типи), суворі правила GROUP BY і порівняння типів, RETURNING та екосистему розширень; MySQL фіксує кожен DDL негайно, мовчки приводить типи й порівнює рядки без урахування регістру.
`self::` і `parent::` - посилання на клас, зафіксовані на етапі компіляції за місцем написання коду, а `static::` резолвиться в рантаймі в клас, на якому виклик почався. Це і є late static binding (PHP 5.3+): завдяки йому `new static()` у батьківській фабриці створює нащадка, а `static::CONST` бере константу нащадка.
Підміняють те, що повільне, недетерміноване або має побічні ефекти: базу, мережу, пошту, час, випадковість. Value objects і доменні класи беруть справжні. Чужі бібліотеки не мокають напряму: їх загортають у власний вузький порт, домен тестують з фейком цього порту, а сам адаптер integration-тестом проти справжньої бібліотеки чи записаних відповідей.
Рівні 0-10 вмикають правила порціями: 0-3 ловлять неіснуючі символи й неузгоджені return-типи, 5 перевіряє типи аргументів, 6 вимагає типізації, 8 забороняє звертатися до методів на nullable, 9-10 закривають mixed. У легасі це вводять baseline: заморозити наявні помилки у phpstan-baseline.neon, поставити аналіз у CI на червоне для нового коду і піднімати рівень по одному.
Messenger розділяє три речі. Повідомлення тримає лише дані, логіка живе в обробнику з #[AsMessageHandler] і методом __invoke(), а транспорт описується DSN у конфігу. Де саме обробник виконається, вирішує секція routing: без маршруту він спрацює синхронно в тому ж запиті, з маршрутом на doctrine/amqp/redis його підхопить воркер messenger:consume, а в тестах той самий код ганяють через sync:// або in-memory://.
8.0 дав match, іменовані аргументи, promotion, union-типи і `?->`; 8.1 - enum, readonly, first-class callable, Fibers; 8.2 - readonly-класи і DNF-типи; 8.3 - типізовані константи класу і `#[\Override]`; 8.4 - property hooks, асиметричну видимість і `new Foo()->method()` без дужок. Спільна лінія цих релізів: більше типів у сигнатурах і менше способів зіпсувати стан обʼєкта ззовні.
На практиці найчастіше працюють два принципи: єдина відповідальність тримає класи малими й зрозумілими, інверсія залежностей робить код тестованим; решта є наслідками, і сліпе дотримання всіх пʼяти породжує зайві абстракції.
`declare(strict_types=1)` — це директива рівня одного файлу, яка вимикає приведення скалярів для викликів, зроблених із цього файлу: без неї PHP у coercive-режимі намагається сконвертувати аргумент до оголошеного типу (з PHP 8.0 нечислові рядки — це `TypeError`, а числові з «хвостом» — `int` плюс `E_WARNING`), зі strict приймається лише точний тип, з єдиним винятком `int` → `float`.
Провайдер - єдине штатне місце для збирання застосунку: `register()` кладе привʼязки в контейнер, `boot()` викликається вже після того, як зареєструвалися всі провайдери. `bind` дає новий обʼєкт на кожне розвʼязання, `singleton` тримає один на весь життєвий цикл контейнера, а `scoped` - той самий singleton, який Octane і черговий воркер скидають між запитами та задачами.
IDOR (insecure direct object reference) виникає, коли застосунок знаходить обʼєкт за ідентифікатором із запиту і не перевіряє, чи належить він поточному користувачу. `auth` відповідає лише на питання «хто ви», тому для кожного обʼєкта потрібна окрема перевірка: шукати через звʼязок власника (`$user->resumes()->findOrFail($id)`), через policy або через `scopeBindings()` для вкладених маршрутів. UUID ускладнює перебір, але доступ не перевіряє.
TDD це цикл з трьох кроків: написати маленький тест, що падає з очікуваної причини (red), найпростішим кодом зробити його зеленим (green), прибрати дублювання й покращити дизайн під захистом тестів (refactor). Головна користь у фідбеку на дизайн, покриття тут лише побічний ефект: код, який важко протестувати першим, зазвичай має забагато залежностей. Найкраще працює для доменної логіки з чіткими правилами, гірше для дослідницького коду й UI.
Це можливість оголосити get і set логіку прямо на властивості, без окремих методів і без магії __get/__set.
Value object — незмінний обʼєкт без ідентичності, який описується лише своїм значенням: він валідує себе в конструкторі, тому в системі не існує невалідного екземпляра, і порівнюється за вмістом, а не за посиланням.
Віконна функція рахує агрегат або позицію по групі рядків (вікні), не згортаючи їх у один рядок: ROW_NUMBER і RANK дають нумерацію всередині PARTITION BY, LAG бере значення попереднього рядка, SUM() OVER (ORDER BY …) дає running total — усе це замінює корельовані підзапити й самоджойни одним проходом.
Генератор — це функція з `yield`, яка при виклику не виконує жодного рядка тіла, а повертає обʼєкт `Generator` (реалізує `Iterator`): значення обчислюються по одному на вимогу, тому пікова памʼять не залежить від обсягу даних; ціна — один-єдиний прохід без перемотки, без `count()` і без доступу за індексом.
Доменна подія - це зафіксований факт у минулому часі, названий мовою бізнесу (`OrderPaid`), який сутність записує в себе, а застосунковий шар публікує після коміту. Події фреймворку (`kernel.request`, `eloquent.updated`, `postPersist`) описують технічний життєвий цикл запиту чи рядка, а інтеграційна подія - це окремий публічний контракт для інших сервісів, з власною версійованою схемою, який виїжджає через outbox і доставляється at-least-once.
nginx сам PHP не виконує: він упаковує запит у FastCGI-записи й віддає їх у сокет пулу PHP-FPM, де вільний воркер виконує скрипт із `SCRIPT_FILENAME`. Маршрутизацію задає `try_files`, пропускну здатність - `pm` і `pm.max_children`, який вважають від реально вільної памʼяті, а видимість проблем дають `slowlog` і `request_terminate_timeout`.
Розширення, імʼя і MIME-тип із запиту задає клієнт, тож тип файлу визначають за вмістом (`finfo`, правило `mimes` у Laravel), імʼя і розширення генерують самі, а файл кладуть поза публічною текою, де вебсервер не виконає PHP. Віддають файли через контролер з перевіркою доступу або з окремого домену, а розмір обмежують на рівні nginx, PHP і валідації.
У Laravel «unit» і «feature» — це не «клас проти роуту», а «без завантаженого застосунку проти з ним»: у стандартному `tests/Pest.php` `Tests\TestCase` підключається лише до теки `Feature`, тому в `Unit` немає ні контейнера, ні фасадів, ні бази. Логіку без Illuminate тестують швидкими unit-тестами, все інше — feature-тестами через HTTP-межу з фабриками для даних і фейками (`Queue`, `Mail`, `Http`, `Storage`) для зовнішніх меж; фейк доводить лише факт виклику, тому job чи mailable потребують власного тесту.
Guard відповідає на питання «як пізнати користувача в цьому запиті» (session, token, sanctum), provider - «де його взяти» (eloquent-модель або таблиця через `database`). Sanctum закриває обидва типові випадки: персональні токени в `personal_access_tokens` для мобільних і сторонніх скриптів та звичайну cookie-сесію для свого SPA на тому ж домені. Passport потрібен лише тоді, коли ви самі стаєте OAuth2-провайдером із клієнтами, екраном згоди й refresh-токенами, а готовий фронтенд для входу беруть зі стартових наборів Laravel 12 (React, Vue, Livewire), бо Breeze і Jetstream уже не той шлях, який радить документація.
Усі чотири механізми - це один `Gate`: `Gate::authorize()` шукає policy за першим аргументом (`App\Models\Post` -> `App\Policies\PostPolicy`), інакше падає на іменовану здібність з `Gate::define()`, а `can` middleware, `$this->authorize()`, `authorize()` у Form Request і `@can` у Blade лише викликають його в різних точках запиту. Відмова кидає `AuthorizationException` -> 403, а `Response::denyAsNotFound()` перетворює її на 404, коли сам факт існування запису розголошувати не можна.
Будь-яка перевірка проходить через `isGranted($attribute, $subject)`: AccessDecisionManager опитує всі voter'и й зводить їхні голоси стратегією (за замовчуванням affirmative). Ролі перевіряє RoleHierarchyVoter, який розгортає `role_hierarchy`, а права на конкретний обʼєкт — власний Voter, підключений атрибутом `#[IsGranted('EDIT', subject: 'post')]`.
N+1 виникає, коли для колекції з N записів виконується ще N запитів на звʼязки; виправляється eager loading, а ловиться через preventLazyLoading, Debugbar або Telescope.
Nonce (wp_nonce_field + check_admin_referer) захищає від CSRF, current_user_can з конкретною здатністю — від перевищення прав, esc_* на виводі — від XSS, $wpdb->prepare — від SQL-інʼєкції. Це чотири різні шари, і жоден не замінює інший.
Шаблони перевизначають копією у `wp-content/themes/{тема}/woocommerce/` зі збереженням шляху й контролем `@version`, логіку чіпляють на хуки WooCommerce, а дані пишуть через CRUD (`$product->update_meta_data()`, `$order->update_meta_data()`), бо при HPOS замовлення більше не живуть у `wp_posts`. Класичний і блоковий checkout мають різні точки розширення.
Кількість спроб задають `$tries` (або `$maxExceptions`), паузу між ними — `$backoff` чи метод `backoff()`, дедлайн — `retryUntil()`, а фінальний обробник — `failed(Throwable $e)`. Ключова пастка не в цих властивостях, а в тому, що `retry_after` у config/queue.php має бути більшим за `$timeout` job, інакше воркер підхопить ще працюючий job і виконає його вдруге. Horizon дає ті самі налаштування на рівні супервізора, дашборд і теги; `ShouldBeUnique`, `Bus::batch()` і `Bus::chain()` керують не повторами, а тим, що взагалі потрапить у чергу і в якому порядку.
Multi-stage: у білдерах Composer і збірка ассетів, у фінальному образі лише runtime, vendor без dev-залежностей і код; шари впорядковані так, щоб зміна коду не інвалідувала шар із залежностями.
Middleware — це шари Pipeline навколо контролера: код до `$next($request)` бачить запит, код після — уже готову відповідь. Спершу йдуть глобальні (у порядку реєстрації, ще до роутера), потім групові й маршрутні — але ці фреймворк переставляє за списком пріоритетів, а `terminate()` викликається окремо, вже після відправки відповіді.
Контейнер компілюється в PHP-клас на етапі cache warmup: autowiring читає типи в конструкторах, compiler passes змінюють визначення сервісів, а результат — статичний код без рефлексії в рантаймі.
CPT — це рядок у wp_posts із власним post_type, таксономія — нормалізована багато-до-багатьох звʼязка через term_relationships; таксономія годиться для фільтрів і категоризації, meta — для атрибутів запису, а власна таблиця потрібна, коли даних мільйони або потрібні складні фільтри й свої індекси.
Таблиця `migrations` зберігає лише імена вже виконаних файлів, тому правка старої міграції на проді не виконується ніколи і дає розʼїзд схеми між середовищами: зміну оформлюють новим файлом. Без простою це три окремі кроки в різних релізах - nullable-колонка, бекфіл порціями поза міграцією, і аж потім NOT NULL через `CHECK ... NOT VALID` та індекс `->online()` у міграції з `$withinTransaction = false`.
Дивіться на тип доступу до таблиці, оцінку рядків проти фактичної кількості і найдорожчі вузли плану; розбіжність оцінки й факту вказує на застарілу статистику.
Робочий набір у PHP невеликий: Strategy за інтерфейсом із вибором у фабриці, Decorator для наскрізних дрібниць типу кешу й логів, Observer для реакцій на зміну стану. Фреймворки вже зібрані з них (драйвери кешу і сесій, `#[AsDecorator]`, Eloquent-обсервери), а Singleton у PHP-застосунку реалізує контейнер через `singleton()` чи shared-сервіс, не сам клас.
Laravel 11 прибрав з нового скелета `Http/Kernel`, `Console/Kernel`, `Exceptions/Handler`, половину конфігів і теку `lang`, перенісши налаштування в `bootstrap/app.php` і в дефолти фреймворка; 12 і 13 структуру не чіпали, тож оновлення між ними - це бамп версій у `composer.json` плюс вичитаний `UPGRADE.md`, а не переписування застосунку.
Новий реліз збирається і прогрівається в окремій теці, трафік перемикається однією атомарною заміною симлінка (або поступовою заміною контейнерів за readiness-пробою), після чого скидаються realpath cache й OPcache і перезапускаються воркери; відкат - та сама операція у зворотний бік.
Робочий стандарт - 3НФ: кожен факт живе в одному місці, решта таблиць посилається на нього ключем. Денормалізують після 3НФ і під конкретний вимір читання, залишаючи нормалізовані дані джерелом істини, з якого дублікат можна перерахувати.
Pest це надбудова над PHPUnit: з кожного файлу з `it()`/`test()` він генерує клас, що успадковує `TestCase`, і запускає його раннером PHPUnit з тим самим `phpunit.xml`. Тому класові тести працюють поруч без змін, асерти PHPUnit доступні, а Pest додає `expect()`-ланцюжки, datasets, хуки, `arch()`, мутаційне тестування й профілювання.
`Collection` - це обгортка над масивом, який уже цілком у памʼяті, тож кожен `map()`/`filter()`/`groupBy()` робить повний прохід і створює новий масив, а `Model::all()->where(...)` спершу читає всю таблицю. Фільтрувати, рахувати й групувати має база; `LazyCollection` через `cursor()`, `lazy()` і `lazyById()` потрібна тоді, коли рядків більше, ніж влазить у памʼять.
Трейт — механізм горизонтального повторного використання коду: набір методів і властивостей, який компілятор копіює в клас.
Twig за замовчуванням екранує кожен вивід `{{ }}`, але стратегію обирає за розширенням файлу шаблону (`.html.twig` - html, `.js.twig` - js, `.txt.twig` - без екранування), а не за місцем виразу в розмітці; тому в атрибутах, URL-параметрах і в `<script>` потрібні явні `|e('html_attr')`, `|e('url')`, `|e('js')`, а `|raw` лишають тільки для HTML, який згенерував сам застосунок або пропустив через санітайзер.
Контент усіх типів лежить в одному wp_posts і розрізняється колонкою post_type, довільні поля в wp_postmeta за схемою EAV, налаштування в wp_options; рядки з autoload читаються одним запитом на кожен запит до сайту, тому роздутий autoload гальмує весь сайт.
Компонент - це ізольований шматок розмітки з явним контрактом: `@props` (або конструктор класу) описують очікувані дані й забирають ці ключі з `$attributes`, `$slot` приймає вкладений вміст, а решта переданих атрибутів осідає в `ComponentAttributeBag` і виводиться через `$attributes->merge()`, де `class` склеюється, а інші атрибути перекриваються викликом. `@include` навпаки не має ні контракту, ні слотів: він просто вставляє файл, якому видно всі змінні батьківської сторінки.
Масив передається за значенням, але фізична копія не створюється: PHP збільшує лічильник посилань на внутрішню структуру і копіює її лише при першому записі (copy-on-write). Обʼєкт copy-on-write не має - копіюється лише ідентифікатор, тому зміни властивостей видно з обох боків.
Роль - це лише іменований набір capabilities у таблиці options; перевіряти завжди треба capability через current_user_can, meta-можливості типу edit_post - разом з ID обʼєкта, а власні ролі додавати в register_activation_hook і прибирати в uninstall.php.
Git далеко не єдиний шлях, яким течуть секрети. `APP_DEBUG=true` показує будь-кому, хто спричинить помилку, заголовки запиту з cookies, тіло запиту з паролем, SQL з підставленими значеннями і код. Корінь сайту, налаштований на теку проєкту замість `public/`, віддає `.env` і `.git`. Логи і трекери помилок зберігають усе, що їм передали, а змінні з префіксом `VITE_` потрапляють у JavaScript-бандл. Захист: `APP_DEBUG=false`, корінь лише `public/`, явний перелік полів у логах, `#[\SensitiveParameter]` і жодних секретів у фронтенді.
Воркер - це звичайний PHP-процес, який рано чи пізно завершиться, тому поруч має бути менеджер процесів: supervisor із `autorestart=true` і `numprocs`, systemd-юніт із `Restart=always` або сам оркестратор контейнерів. Ключове налаштування - час очікування після SIGTERM (`stopwaitsecs`, `TimeoutStopSec`), більший за найдовшу задачу.
Хороший unit-тест перевіряє одну поведінку через публічний інтерфейс, складається з трьох частин (arrange, act, assert), має назву-речення про правило, не залежить від часу, бази й інших тестів і падає лише тоді, коли поведінка справді зламалась.
Обидва патерни передають запит послідовністю обробників, але з різним наміром: у Chain of Responsibility запит обробляє перша ланка, яка за нього взялася, і далі він не йде, а в Pipeline працює кожен крок і передає свій результат наступному. Middleware в Laravel і Symfony Messenger поєднує обидва: зазвичай шар передає запит далі й може змінити відповідь на зворотному шляху, але будь-який шар може зупинити обробку, повернувши відповідь сам.
Factory Method відповідає на питання «який клас створити»: базовий код просить продукт у власного методу, а конкретний клас визначає підклас, як `createRedisDriver()` у нащадках `Illuminate\Support\Manager`. Builder відповідає на питання «як зібрати один складний обʼєкт»: кроки з назвами й перевірка всіх правил разом у `build()`. Model factories у Laravel з Factory Method мають спільну лише назву: за змістом це незмінний будівельник тестових даних.
Named arguments (PHP 8.0) дозволяють передавати аргументи за іменем параметра й пропускати необовʼязкові, а constructor property promotion (PHP 8.0) оголошує властивість прямо в сигнатурі конструктора; ціна — імена параметрів стають частиною публічного API, а промотована властивість ініціалізується ще до тіла конструктора.
Стиль API вибирається за клієнтами і за тим, що ви готові втратити: REST дає безкоштовне HTTP-кешування й прозорі логи, але ліпить over-fetching і зайві раунд-трипи; GraphQL забирає вибірку полів у клієнта, натомість вимикає кеш на CDN і переносить N+1 та авторизацію в резолвери; RPC виграє у внутрішній комунікації зі строгим контрактом. Для одного власного фронтенду GraphQL зазвичай зайвий - дешевше sparse fieldsets, include і BFF-ендпоінт під екран.
Конфігурація приходить у процес через змінні оточення й відрізняється по середовищах, код лишається однаковим; .env — це локальна зручність розробника, яку не комітять, а на продакшені значення дає pool php-fpm, оркестратор або менеджер секретів. У Laravel env() дозволений лише у файлах config/, бо після config:cache .env не завантажується взагалі.
Слухач і підписник - це один і той самий механізм із різним місцем конфігурації: підписник оголошує події й пріоритети всередині класу, слухач отримує їх ззовні (атрибут, YAML), і це зручніше для чужих подій та для налаштувань, що змінюються без правки коду. У `kernel.exception` головне - пріоритет (нижче нуля, щоб не з'їсти логування), `setThrowable()` для перетворення доменного винятку на HTTP-виняток і памʼять про те, що `setResponse()` зупиняє поширення події.
Обсервер - це набір слухачів, підвішених до подій збереження конкретної моделі, тож він мовчить на будь-якій масовій операції через білдер і за замовчуванням виконується всередині транзакції, ще до коміту. Подія зі слухачем описує бізнес-факт, диспатчиться явно з одного місця й дочекається коміту, якщо реалізує `ShouldDispatchAfterCommit`.
Yii3 переписали з нуля: кілька десятків незалежних пакетів `yiisoft/*` замість монолітного фреймворка, DI-контейнер і конструктори замість глобального `Yii::$app`, PSR-15 middleware замість фільтрів-поведінок, PSR-3/6/7/11/14/15/16/17/18 замість власних інтерфейсів. Частина пакетів давно стабільна й працює в будь-якому PHP-проєкті, але повного стека зі стабільним релізом станом на зараз немає, тому будувати плани продукту навколо очікування Yii3 не варто.
`match` (PHP 8.0+) — це вираз, який повертає значення й порівнює строго через `===` без провалювання між гілками, а якщо жодна умова не збіглася і немає `default` — кидає `UnhandledMatchError`; `switch` — оператор, який порівнює нежорстко через `==`, потребує `break` і при відсутності збігу мовчки нічого не робить.
Event описує факт, що щось сталося; listener реагує на нього; job — це одиниця відкладеної роботи в черзі, яку хтось явно поставив.
OFFSET не вміє «стрибати»: база читає й відкидає всі пропущені рядки, тому час зростає лінійно з номером сторінки. Keyset (seek method) замість номера сторінки передає ключ останнього прочитаного рядка й читає рівно LIMIT записів за постійний час.
Встановлення залежностей з кешем, перевірка стилю, статичний аналіз, тести на реальній базі й збірка артефакту; швидкі кроки першими, щоб пайплайн падав рано.
WP-CLI завантажує WordPress у CLI-процесі без HTTP, тому дає скриптований доступ до ядра, плагінів і бази: перенесення через search-replace з коректною обробкою serialize(), запуск WP-Cron із системного crontab і власні команди через WP_CLI::add_command з нормальними exit-кодами для CI.
Покриття рахує, які рядки виконались під час тестів, але не чи помітив би тест їхню зміну: тест без перевірок дає 100%. Мутаційне тестування вносить у код дрібні зміни (мутанти: `>=` на `>`, `+` на `-`, `true` на `false`) і проганяє тести; мутант, на якому все зелене, вижив і показує неперевірене місце. У PHP це Infection і вбудований у Pest `--mutate`; частку вбитих мутантів показує MSI.
CQRS — це розділення операцій зміни стану (команди) і операцій читання (запити) на різні моделі: команди йдуть через доменні агрегати з інваріантами, читання — окремими DTO або навіть сирим SQL. Event sourcing і окрема база для читання — необовʼязкові додатки, а не частина визначення.
CTE - це іменований набір рядків у межах одного запиту: WITH розкладає складну логіку на читабельні кроки, WITH RECURSIVE обходить ієрархію довільної глибини. З PostgreSQL 12 CTE з одним посиланням вбудовується в план, тож барʼєром оптимізації він більше не є за умовчанням.
`Fiber` (PHP 8.1+) — це низькорівневий примітив кооперативної багатозадачності: блок коду з власним стеком, який можна призупинити з будь-якої глибини викликів через `Fiber::suspend()` і відновити через `resume()`. Планувальника й циклу подій у ядрі немає — їх дають revolt/event-loop та AMPHP v3.
Doctrine відслідковує всі керовані обʼєкти й накопичує зміни в памʼяті; на flush() вона обчислює change set і виконує запити в правильному порядку.
Event — точка розширення в `yii\base\Component`: клас викликає `trigger()`, а зовнішній код підписується через `on()`. Behavior — обʼєкт, який прикріплюється до компонента в рантаймі, підписує свої методи на події через `events()` і додає компоненту власні властивості й методи через `__get`/`__call`. Разом вони дають повторне використання поведінки без успадкування.
Архітектурний тест перевіряє будову коду, а не поведінку: хто кого імпортує, чи класи final, чи є strict_types, чи не лишився `dd()`. У Pest це `arch()->expect('App\Domain')->not->toUse('Illuminate')`: файли читаються парсером, код не виконується. Ними охороняють межі шарів і модулів, які інакше тихо зникають, а наявні порушення вносять у `ignoring()` явним списком.
Атрибути (PHP 8.0) - це структуровані метадані `#[Attr(...)]`, які компілятор зберігає разом із класом, методом, властивістю чи параметром і які читають через `getAttributes()` у Reflection. Сам атрибут нічого не робить: його треба знайти рефлексією і перетворити на обʼєкт через `newInstance()`, тому продакшн-код робить це один раз під час прогріву кешу, а не на кожен запит.
Порт - це інтерфейс, який оголошує саме ядро застосунку під свою потребу, адаптер - реалізація цього інтерфейсу в інфраструктурі поверх конкретної технології. Домен і Application залежать лише від портів, зв'язування адаптерів робить service provider у Laravel або services.yaml у Symfony, а в тестах ті самі порти закриваються in-memory адаптерами.
Складений індекс працює за принципом лівого префікса: індекс (a, b, c) допомагає умовам по a, по a і b, по a, b і c, але не по b чи c окремо.
Фіксація сесії: зловмисник заздалегідь підсовує жертві відомий йому id сесії, жертва входить, і якщо id після входу не змінився, зловмисник уже в акаунті. Тому при кожній зміні рівня доступу id регенерують: `session_regenerate_id(true)` у PHP, а в Laravel 13 це робить сам `Auth::login()` і `Auth::attempt()`. При виході сесію інвалідовують (`invalidate()` і `regenerateToken()`), а викрадення cookie ускладнюють прапорцями `Secure`, `HttpOnly`, `SameSite` і префіксом `__Host-`.
Токен скидання генерують криптостійким генератором (`random_bytes()`, `Str::random()`), у базі зберігають лише його хеш, дають короткий термін дії і видаляють після використання. Порівнюють секрети через `hash_equals()`, бо `===` зупиняється на першому відмінному байті. Відповідь форми однакова для відомого і невідомого email, а час відповіді вирівнюють, щоб не можна було перевірити, хто зареєстрований.
У Yii2 два різні реєстри: `Yii::$container` (DI-контейнер, резолвить конструктори через рефлексію і за замовчуванням віддає новий обʼєкт) і ServiceLocator застосунку з секції `components` (ліниві синглтони на запит, доступні як `Yii::$app->db`). Обидва створюють обʼєкти через `Yii::createObject()`, тому конфіг у Yii2 це масив із `class` плюс публічні властивості, а не набір closure-привʼязок, як у Laravel.
Модель серіалізується через `toArray()`, тож форма відповіді — це набір колонок таблиці плюс випадково завантажені звʼязки й `$appends`: нова колонка публікується сама, а `$hidden` захищає лише те, що ви згадали. `JsonResource` — це білий список полів у явному класі, де `whenLoaded()` прибирає ключ замість ліниво тягнути звʼязок, а `Resource::collection($paginator)` сам додає `links` і `meta`.
Кожен тест має сам створювати рівно ті дані, від яких залежить його перевірка, і показувати їх у тексті тесту. Для моделей це фабрики зі станами (`Vacancy::factory()->published()`), для доменних обʼєктів у unit-тестах builder або Object Mother з розумними значеннями за замовчуванням. Спільні фікстури на весь набір зручні лише для довідників, бо інакше тести неявно залежать від чужих даних і ламаються від кожної зміни.
register_rest_route на хуку rest_api_init: namespace/версія, methods, callback, обовʼязковий permission_callback з current_user_can і args зі sanitize_callback та validate_callback; помилки повертаються як WP_Error зі status.
Перебір закривають обмеженням частоти: у Laravel це `RateLimiter::for()` з ключем, що поєднує email і IP, плюс `throttle` middleware, який віддає 429 з `Retry-After`. Ліміт тримається в спільному кеші (Redis), а `$request->ip()` має бути справжньою адресою клієнта, тобто `TrustProxies` налаштований. Від credential stuffing ліміти допомагають мало, тому додають перевірку злитих паролів, 2FA і моніторинг, а замість постійного блокування акаунта використовують тимчасові затримки.
У PostgreSQL зберігайте jsonb і індексуйте його GIN для пошуку по довільних ключах або B-tree на виразі `(data->>'key')` для конкретного поля; у MySQL індекс на JSON можливий лише через згенеровану колонку, функціональний індекс (8.0.13+) або multi-valued index (8.0.17+). Помилка — тримати в JSON поля, які є в кожному рядку і за якими фільтрують чи джойнять: там потрібні звичайні колонки з типом, NOT NULL і зовнішнім ключем.
Кеш стає застарілим з двох причин: ключ не враховує всього, від чого залежить результат, або інвалідація не спрацювала (масовий update без подій моделі, forget усередині ще не закомміченої транзакції). Лікується не меншим TTL, а тегами й обсерверами з `$afterCommit`, а одночасний промах гарячого ключа — `Cache::lock()` або `Cache::flexible()`.
Логи пишемо структурованим JSON у stdout одним рядком на подію й віддаємо збирачу, помилки дублюємо в Sentry, де вони групуються за fingerprint і мають реліз та контекст, а сигналом «щось не так» служать не логи, а метрики RED (rate, errors, duration) з алертами на симптоми, які бачить користувач.
З боку бази N+1 виглядає не як повільний запит, а як один нормалізований запит із величезним calls і мікроскопічним mean_exec_time; лікується збором ключів і другим запитом з IN або = ANY, а JOIN підходить лише там, де він не множить рядки.
Сучасний шлях — block.json і register_block_type з шляхом до директорії блоку: метадані, скрипти й стилі описуються декларативно, а PHP-рендер задається через render або render_callback.
DB::transaction($callback, $attempts) обгортає замикання в транзакцію, відкочує її на будь-якому Throwable і повторює лише при deadlock чи lock wait timeout; lockForUpdate() потрібен там, де ви читаєте значення, щоб на його основі писати, і блокування тримається до COMMIT — тому має сенс тільки всередині транзакції.
Форма — це двонаправлений маппер HTTP-даних на обʼєкт: submit прогонить дані через трансформери, покладе їх у модель і лише потім викличе Validator, який валідує обʼєкт, а не поля; в JSON-API цей шар зайвий, бо `handleRequest` читає `$request->request`, а не тіло запиту, і краще брати `#[MapRequestPayload]`.
Розклад живе в коді, а cron має один рядок `* * * * * php artisan schedule:run`: цей процес щохвилини порівнює cron-вираз кожної задачі з поточною хвилиною і запускає лише ті, що настали. Від дублів захищають два різні локи в кеші - `withoutOverlapping()` не дає задачі наздогнати саму себе на одному вузлі, `onOneServer()` віддає хвилину одному серверу з кластера, - а справжню гарантію дає ідемпотентність самої операції.
Спершу профілювання через Query Monitor, потім persistent object cache у Redis, правильні аргументи WP_Query, індекси або власні таблиці замість meta_query і повне кешування сторінок на рівні nginx.
RBAC у Yii2 будується на ієрархії ролей і дозволів з правилами: ієрархія зберігається в БД або файлі, перевірка йде через Yii::$app->user->can(), а правила додають динамічні умови на кшталт авторства.
Lazy-проксі та PersistentCollection довантажують дані окремим запитом на кожну сутність; лікується fetch join (`addSelect` приєднаної асоціації), а для пагінації — Doctrine Paginator, який ріже сутності, а не рядки.
У Yii2 чотири рівні: кеш даних через компонент `cache` (`get`/`set`/`getOrSet`), кеш фрагментів через `beginCache()`/`endCache()`, кеш сторінок фільтром `yii\filters\PageCache` і HTTP-кеш фільтром `yii\filters\HttpCache` з `Last-Modified`/`ETag`. Інвалідація - або за часом, або через обʼєкт `Dependency`: `DbDependency` перевіряє результат дешевого SQL, `TagDependency` позначає групу ключів застарілою через `TagDependency::invalidate()`.
Мінімум - `IteratorAggregate` (обхід у `foreach` через `getIterator()`), `Countable` (`count()`), `JsonSerializable` (контроль над `json_encode()`) і `Stringable` (типізація `string|Stringable`). `Iterator` пишуть вручну лише коли треба керувати станом обходу, а `ArrayAccess` дає синтаксис `$obj['key']`, який ламає типізацію і статичний аналіз.
Command пакує намір разом з усіма даними в обʼєкт, щоб виконати його зараз, пізніше, повторно або з іншої точки входу. Command bus додає одне місце, через яке проходять усі команди: він знаходить обробник за класом команди й загортає виконання в middleware для транзакцій, логування й валідації. Queued job у Laravel - це команда й обробник в одному класі з доставкою через чергу, а Symfony Messenger тримає повідомлення й обробник окремо і вирішує, синхронно чи через транспорт, у конфігу `routing`.
За кодом їх не відрізнити: обидва реалізують той самий інтерфейс і тримають посилання на справжній обʼєкт. Декоратор додає поведінку (кеш, лог, повтори) і складається шарами там, де збирають обʼєкт, а проксі керує доступом до справжнього обʼєкта: відкладає його створення, перевіряє права або пересилає виклик кудись далі. Кешувальна обгортка репозиторію працює, лише якщо інвалідація живе в тій самій обгортці й спрацьовує після коміту.
Вибір визначають три речі, а не смак: чи є другий клієнт (мобілка, партнери) — тоді API все одно доведеться зробити, і SPA стає майже безкоштовним; яка зернистість взаємодій — Livewire платить мережевим round-trip за кожен крок, тому годиться для форм, таблиць і фільтрів, а не для перетягування, канви чи офлайну; і хто це супроводжуватиме. Livewire тримає стан на сервері й ганяє снапшот компонента туди-сюди, SPA тримає стан у браузері й вимагає версіонованого контракту; Inertia — середина, де є клієнтські компоненти, але немає публічного API.
API Platform виправданий, коли потрібен машиночитний контракт (OpenAPI/JSON-LD), фільтри, пагінація й HTTP-кеш «з коробки»; головна архітектурна зміна — контролер зникає, а його місце займають state provider і processor, тому ресурс має бути окремим DTO, а не Doctrine-сутністю.
DDD має сенс там, де складність саме в домені: багато правил, які часто змінюються, і потрібна спільна мова з бізнесом; для CRUD-адмінки чи інтеграційного сервісу агрегати й репозиторії додають шарів без вигоди.
Headless виправданий, коли контент споживає більше ніж один клієнт або команда вже живе в JS-екосистемі; платити за це доводиться прев'ю, редиректами, формами, CSS блоків, меню, SEO-мета й власною інвалідацією кешу — усім, що раніше безкоштовно робила тема.
Multisite виправданий, коли багато однотипних сайтів живуть на одному коді, одній базі й під одною командою; він дає спільні wp_users і спільний набір плагінів, але й спільну долю - оновлення, падіння, бекап і безпека стають мережевими, а не посайтовими.
Партиціонування потрібне тоді, коли дані природно ріжуться по ключу (найчастіше по часу) і старі шматки треба видаляти чи архівувати цілком: DROP/DETACH PARTITION — це операція над метаданими замість мільйонів DELETE. Прискорення читання — побічний ефект, і лише коли ключ партиціонування є в WHERE.
WeakReference (PHP 7.4+) і WeakMap (PHP 8.0+) тримають обʼєкт, не збільшуючи його refcount: як тільки обʼєкт знищується, посилання стає null, а запис у WeakMap зникає сам — це штатний спосіб робити привʼязані до обʼєктів кеші й реєстри в довгоживучих процесах.
Модулі й сервіси розділяє не код, а межа процесу: усередині моноліта виклик модуля — це синхронний метод у спільній транзакції, між сервісами — мережа з таймаутами, частковими збоями й eventual consistency. Тому за замовчуванням беруть модульний моноліт із твердими межами, а окремий сервіс виділяють лише там, де є конкретна причина — незалежний профіль навантаження, окремий цикл релізу під окрему команду або інший рантайм — і платять за неї розподіленими транзакціями та експлуатацією.
`unserialize()` створює обʼєкти будь-якого класу, який може завантажити автозавантажувач, з властивостями, які задав автор рядка, і потім PHP сам викликає їхні магічні методи: `__wakeup()`, `__unserialize()`, `__destruct()`, `__toString()`. Ланцюжок таких методів із класів у `vendor/` (gadget chain) дає запис файлів чи виконання коду. Чужі дані розбирають через `json_decode()`, а якщо формат PHP неминучий, то з `allowed_classes => false`, `max_depth` і перевіркою HMAC до розбору.
Octane тримає застосунок у памʼяті між запитами через Swoole, FrankenPHP або RoadRunner: бутстрап відбувається один раз, але стан більше не скидається сам.
PHP 8.5 (реліз 20 листопада 2025) додав оператор конвеєра `|>`, синтаксис `clone $obj with {...}`, атрибут `#[\NoDiscard]` з приведенням `(void)`, вбудоване розширення URI з класами `Uri\Rfc3986\Uri` та `Uri\WhatWg\Url`, а також дрібніші зручності — `array_first()`/`array_last()`, атрибути на константах, замикання в константних виразах і бектрейси для фатальних помилок.
SSRF (server-side request forgery) трапляється, коли сервер іде за адресою, яку задав користувач, і стукає туди, куди сам користувач дістатись не може: у метадані хмари на `169.254.169.254`, у Redis на localhost чи в адмінку у внутрішній мережі. Захист: дозволяти лише `http(s)`, самим резолвити хост і відкидати непублічні адреси, підключатися саме до перевіреної IP, не йти за редіректами без повторної перевірки, а надійніше пустити такі запити через окремий egress-проксі.
Outbox pattern прибирає проблему подвійного запису: подію зберігають рядком у таблиці `outbox` тією ж транзакцією, що й зміну стану, а окремий процес-релей уже після коміту читає ці рядки й публікує в брокер. База і подія стають атомарними, ціною чого є доставка at-least-once — споживач зобовʼязаний бути ідемпотентним.
Агрегат - це найменша група об'єктів, яку доводиться читати й писати разом, щоб гарантувати бізнес-правило: межа проходить там, де є інваріант, який мусить виконуватися після кожного коміту. Корінь агрегату - єдина точка входу, одна транзакція змінює один екземпляр, а зв'язки з іншими агрегатами тримаються за ідентифікатором; що більший агрегат, то більше конкурентних записів він серіалізує.
Треба винести назовні весь стан, який зараз лежить на локальному диску чи в памʼяті процесу: сесії — у Redis/БД, завантажені файли — в обʼєктне сховище, кеш і локи — у спільний Redis, черги й планувальник — в окремі процеси з дедуплікацією; липкі сесії це не рішення, а спосіб не помітити проблему.
Схема змінюється у фази expand і contract: спершу зміни, сумісні зі старим кодом, потім деплой коду, який працює з обома варіантами, і лише після повного викочування прибирання старого.
Версія - це ярлик на контракті, а сумісність визначається поведінкою: ламає не номер, а звужений тип, нове значення enum, суворіша валідація чи змінений статус-код. Робочий підхід - additive-зміни всередині версії, нова major лише на справжній злам, машиночитані заголовки Deprecation і Sunset плюс телеметрія по клієнтах перед вимкненням.
diff дає єдине джерело істини для схеми, але його результат треба перечитувати й перегенеровувати після rebase; розходження ловиться в CI прогоном міграцій на порожній базі плюс doctrine:schema:validate, а перенесення даних живе в окремих міграціях на сирому SQL, не в тих, що змінюють схему.
Межа контексту проходить там, де слово змінює значення і де змінюється склад людей, які замовляють зміни, а не там, де закінчується таблиця. Спільні дані розвʼязуються правилом одного власника факту: решта контекстів читають через контракт або тримають власну проєкцію, спільний код лишається мікроскопічним ядром без бізнес-правил, а чужа модель заходить усередину тільки через антикорупційний шар.
Deadlock — це цикл очікувань блокувань: база сама його виявляє й відкочує одну транзакцію (MySQL 1213/40001, PostgreSQL 40P01), тому застосунок зобовʼязаний повторити її, а зменшують частоту єдиним порядком блокування, короткими транзакціями та індексами під UPDATE.
Три рівні захисту: песимістичний замок SELECT FOR UPDATE серіалізує доступ до рядка, колонка version із перевіркою кількості оновлених рядків ловить конфлікт без замка, а унікальне обмеження в базі тримає інваріант навіть тоді, коли обидва механізми обійшли.
Спершу розкласти час: браузерний waterfall відділяє TTFB від фронтенду, а далі Debugbar/Clockwork/Telescope локально й Blackfire або Pulse у проді показують, куди йдуть мілісекунди всередині PHP. Оптимізують за спаданням внеску: кількість запитів (N+1) → важкі запити з `EXPLAIN` → синхронні зовнішні виклики в чергу → гігієна деплою (`opcache`, `php artisan optimize`, `composer -o`), і лише потім кеш і Octane.
Три сигнали налаштовують як один набір даних, зшитий спільним trace_id: OpenTelemetry дає trace через PECL-розширення й auto-instrumentation, той самий trace_id підмішується процесором у кожен запис структурованого лога, а метрики через shared-nothing модель PHP збирає не сам процес, а локальний Collector; алерти вішають лише на SLI з бюджетом помилок, решта живе на дашборді.
Транспорт налаштовується з retry_strategy: кількість спроб, початкова затримка й множник; вичерпані повідомлення йдуть у failed transport, а логічні помилки відкидаються одразу через UnrecoverableMessageHandlingException.
Спершу зафіксувати те, що код робить зараз, а не те, що він мав би робити: characterization-тести (Майкл Фезерс) або golden master на рівні HTTP чи консольної команди. Далі знайти шви (seams), через які можна підставити залежності, мінімальними безпечними рефакторингами винести логіку в класи, що тестуються, і лише тоді міняти поведінку. Нову логіку пишуть поруч і з тестами (sprout method, wrap method), а не всередині старого моноліту.
OPcache тримає в спільній памʼяті вже скомпільовані опкоди, прибираючи парсинг і компіляцію з кожного запиту — це і є основний виграш; preload (PHP 7.4+) додатково тримає звʼязані класи фреймворку, а JIT (PHP 8.0+, вимкнений за замовчуванням через `opcache.jit_buffer_size=0`) компілює гарячі опкоди в машинний код і майже нічого не дає типовому веб-застосунку, бо той упирається в I/O, а не в CPU.
PHP звільняє памʼять через підрахунок посилань, а окремий збирач циклів періодично прибирає обʼєкти, які посилаються одне на одного.
Найбезпечніший підхід — strangler pattern: новий застосунок ставиться поруч, роутинг поступово перекидається на нього, спільною лишається база даних; переписувати все одразу означає роками тримати дві версії продукту.
Клієнт передає idempotency key, сервер зберігає його разом із результатом операції під унікальним індексом і на повторний запит повертає збережений результат замість другого платежу.
Стандартної структури вистачає, доки логіка сценарію вміщається в контролер плюс модель; далі вузьке місце — не назви тек, а те, що бізнес-правило не має одного власника. Дія з одним публічним методом і незмінним DTO на вході дає цього власника без DDD, а тактичні патерни DDD додають лише там, де є інваріанти, які треба захищати, і мова, якою говорить бізнес.
Спершу виміряти (`pest --profile`, час по джобах CI), потім прибрати дешеві втрати: хешування паролів, міграції на кожен тест, справжні `sleep()` і мережу, зайві дані з фабрик. Далі паралелити (`--parallel`, шардування на кілька машин), тримаючи тести ізольованими: своя база на процес, без спільних файлів і статики. Надійність тримається на правилі «flaky-тест це баг з власником», а не на автоматичних повторах.
Під FPM памʼять прибирає смерть процесу наприкінці запиту, у worker mode цього прибирання немає: усе, що лишилось у статиці, контейнері чи реєстрі слухачів, накопичується. Робочий набір це гігієна стану між ітераціями, явний запуск збирача циклів за порогом і перезапуск воркера за лімітом памʼяті або кількості запитів.
Колонка `tenant_id` дає спільні міграції та один пул зʼєднань, але ізоляція живе в кожному `WHERE`, і будь-який забутий фільтр стає витоком. База (або схема) на тенанта переносить межу на рівень конекшна ціною N міграцій, N бекапів і пулу зʼєднань. Глобальний scope закриває лише читання через Eloquent: `DB::table()`, правила `unique`/`exists` і відновлення моделі в черзі його не бачать, а кеш, файли, сесії та scheduler треба розводити по тенантах окремо.
MySQL за замовчуванням REPEATABLE READ, PostgreSQL — READ COMMITTED; рівень визначає, які аномалії можливі, але втрачене оновлення закривають не рівнем, а атомарним UPDATE або SELECT FOR UPDATE.