// СТАДІЯ 0 · УРОК 0.1

Комп'ютер та ОС

📖 Теорія
🧠

Аналогія:уяви комп'ютер як кухню. Процесор (CPU) — це кухар, який виконує рецепти (інструкції). Оперативна пам'ять (RAM) — робочий стіл: на ньому лежать інгредієнти, з якими кухар працює зараз, але коли вимкнути світло (перезавантажити ПК), стіл прибирають — все зникає. Диск (SSD/HDD) — це холодильник і шафи: там все зберігається постійно, навіть якщо кухня закрита.

Операційна система (Windows, Linux, macOS) — це адміністратор кухні: вона розподіляє, хто з кухарів (програм) коли користується столом (RAM) і плитою (CPU), стежить за чергою і не дає двом кухарям вихопити один й той самий ніж одночасно.

Для розробника важливо розуміти: коли ти запускаєш PHP-скрипт, ОС виділяє йому шматок RAM, дає час CPU, і скрипт читає/пише файли на диску. Якщо скрипт "висить" — можливо, він чекає на диск або мережу, а не на CPU.

Технічніше: на реальному сервері PHP не має необмеженого доступу до RAM — у php.ini є директива memory_limit (типово 128M), і якщо скрипт спробує використати більше, PHP видасть Fatal error: Allowed memory size exhausted. У продакшн-середовищі зазвичай працює PHP-FPM: пул 'воркерів' (окремих процесів), кожен з яких обробляє один запит за раз — тому кількість RAM прямо обмежує, скільки запитів сервер витримає одночасно.

📄 Офіційна документація: PHP: Configuration (php.ini) →
💻 Приклад коду
Термінал (Linux/Mac)
# Приклад: подивитись, скільки ресурсів використовує процес (Linux/Mac термінал)
top
# або конкретний процес
ps aux | grep php

# Скільки вільної памʼяті
free -h
✏️ Завдання
1
Знайди свою конфігурацію
Відкрий 'Диспетчер завдань' (Windows) або 'Моніторинг системи' (Mac/Linux) і запиши: скільки RAM встановлено, яка модель CPU, скільки вільного місця на диску. Це знадобиться коли будеш налаштовувати сервер для сайту.
2
Порівняй з кухнею закріплення
Своїми словами (2-3 речення) поясни другові без технічної освіти, чим RAM відрізняється від диска, використовуючи будь-яку свою аналогію (не кухню).
3
Перевір ресурси перед деплоєм поглиблено
На реальному сервері з PHP-бекендом важливо знати, скільки RAM виділено PHP-FPM воркерам і скільки вільно на диску для логів/кешу. Виконай free -h та df -h на своєму ПК і запиши результат, ніби готуєшся деплоїти невеликий API.
❓ Перевір себе
1. Що зникає одразу після вимкнення живлення комп'ютера?
Дані на SSD
Дані в RAM
Встановлені програми
Файли на робочому столі
RAM (оперативна пам'ять) — енергозалежна, дані живуть там, лише поки є живлення.
2. Роль операційної системи — це насамперед:
Малювати іконки на робочому столі
Розподіляти ресурси (CPU, RAM) між програмами
Прискорювати інтернет
Зберігати паролі в браузері
ОС — це диспетчер ресурсів між усіма запущеними процесами.
3. Де зберігаються файли після перезавантаження ПК?
В RAM
На диску (SSD/HDD)
В кеші процесора
Ніде, вони видаляються
Диск — енергонезалежна пам'ять, дані там залишаються без живлення.
// СТАДІЯ 0 · УРОК 0.2

Термінал та Linux

📖 Теорія
🧠

Аналогія:графічний інтерфейс (вікна, іконки) — це як меню в ресторані з картинками: зручно, але повільно і обмежено вибором, що є на фото. Термінал — це як прямо сказати кухарю: "звари 200г пасти, дуже al dente, без солі" — точно, швидко, і можна попросити те, чого немає на фото в меню.

Термінал — текстовий інтерфейс для команд ОС. У PHP-розробці термінал потрібен постійно: запустити скрипт, встановити пакет через Composer, подивитись логи, підключитись до сервера.

Основні команди, які варто знати напам'ять: pwd (де я зараз), ls (що тут є), cd (перейти в папку), mkdir (створити папку), rm (видалити — обережно!), cat (показати вміст файлу).

Технічніше: кожен файл і папка в Linux має права доступу (permissions), які видно через ls -l: наприклад -rwxr-xr-- означає, що власник може читати/писати/виконувати, група — тільки читати й виконувати, інші — тільки читати. Змінюють права командою chmod (наприклад chmod 755 script.sh). Символ | (pipe) передає вивід однієї команди на вхід іншої, наприклад cat file.log | grep error — знайти лише рядки з 'error' у файлі логів.

📄 Офіційна документація: Bash Reference Manual →
💻 Приклад коду
Термінал
pwd                  # показати поточну директорію
ls -la               # показати всі файли, включно з прихованими
cd projects           # перейти в папку projects
mkdir my-course       # створити нову папку
touch index.php       # створити порожній файл
cat index.php         # вивести вміст файлу в консоль
rm old-file.txt       # видалити файл (без кошика!)
✏️ Завдання
1
Пройди шлях
У терміналі: створи папку 'test-course', зайди в неї, створи файл 'hello.php', впиши туди <?php echo 'Привіт!'; і виведи вміст файлу командою cat.
2
Своя команда закріплення
Знайди в документації (або погугли) команду, яку ще не використовувала (наприклад mv, cp, grep) і опиши своїми словами, для чого вона.
3
Розгорни структуру backend-проєкту поглиблено
Уяви, що тобі дали SSH-доступ до сервера компанії. Створи типову структуру папок backend-проєкту через mkdir -p (src/Controllers, src/Models, storage/logs, public/), а командою touch створи storage/logs/app.log.
❓ Перевір себе
1. Яка команда покаже вміст поточної папки?
cd
ls
pwd
cat
ls (list) виводить список файлів і папок.
2. Що робить команда 'cd ..'
Видаляє поточну папку
Переходить на рівень вище
Створює нову папку
Показує вміст файлу
'..' означає батьківську директорію.
3. Команда rm видаляє файл:
У кошик, можна відновити
Назавжди, без кошика
Тільки тимчасово
Тільки для адміністратора
У терміналі rm видаляє одразу і безповоротно — треба бути обережною.
// СТАДІЯ 0 · УРОК 0.3

Як працює веб

📖 Теорія
🧠

Аналогія:відкриваючи сайт, ти — клієнт у ресторані (браузер), офіціант — це протокол HTTP, а кухня — сервер. Ти робиш замовлення (HTTP-запит: 'дай мені сторінку /menu'), офіціант несе його на кухню, кухня готує (сервер обробляє, часто через PHP), і офіціант приносить готову страву (HTTP-відповідь: HTML-сторінку).

Кожен запит має адресу (URL), метод (GET — 'дай мені', POST — 'ось дані від мене'), і відповідь має код статусу: 200 (все ок), 404 (сторінку не знайдено — 'такої страви немає в меню'), 500 (кухня зламалась — помилка сервера).

PHP якраз і живе 'на кухні' — це серверна мова: код виконується на сервері, а клієнту (браузеру) приходить вже готовий HTML.

Технічніше: окрім GET і POST, HTTP має ще методи PUT (повністю замінити ресурс), PATCH (частково оновити), DELETE (видалити) — разом вони формують основу REST API. Коди статусу групуються по першій цифрі: 2xx — успіх (200 OK, 201 Created), 3xx — перенаправлення (301 Moved Permanently), 4xx — помилка клієнта (400 Bad Request, 401 Unauthorized, 404 Not Found), 5xx — помилка сервера (500 Internal Server Error, 503 Service Unavailable).

📄 Офіційна документація: MDN: HTTP overview →
💻 Приклад коду
index.php
<?php
// Простий приклад: сервер отримує запит і формує відповідь
// Файл index.php на сервері

header('Content-Type: text/html; charset=utf-8');

$name = $_GET['name'] ?? 'Гостю';

echo "<h1>Привіт, $name!</h1>";
// Якщо відкрити: site.com/index.php?name=Аліна
// Браузер отримає: <h1>Привіт, Аліна!</h1>
✏️ Завдання
1
Визнач статус-коди
Знайди в інтернеті значення кодів 301, 403, 500 і запиши своїми словами, в якій ситуації сайт їх поверне.
2
GET чи POST? закріплення
Наведи 2 власні приклади дій на сайті: одну, яка логічно має бути GET-запитом, і одну — POST. Поясни чому.
3
Розбери реальний API-запит поглиблено
Відкрий у браузері DevTools (F12) → вкладку Network, зайди на будь-який сайт і знайди один XHR/Fetch запит. Запиши: який метод (GET/POST), який статус-код прийшов, і що містить відповідь (JSON чи HTML).
❓ Перевір себе
1. HTTP-код 404 означає:
Сервер зламався
Сторінку не знайдено
Успішний запит
Немає доступу
404 Not Found — ресурс за цією адресою не існує.
2. PHP-код виконується:
В браузері користувача
На сервері
В базі даних
В терміналі клієнта
PHP — серверна мова, браузер отримує вже готовий HTML.
3. Метод GET зазвичай використовують для:
Відправки пароля
Отримання/читання даних
Видалення бази даних
Завантаження великих файлів
GET — для запиту/читання даних, параметри видно в URL, тому не для паролів.
// СТАДІЯ 0 · УРОК 0.4

Мережі та інфраструктура

📖 Теорія
🧠

Аналогія:IP-адреса — це адреса будинку ('вулиця, номер'), а порт — номер квартири в цьому будинку. Домен (google.com) — це просто зручна вивіска замість запам'ятовування адреси, і DNS — це 'довідкове бюро', яке за назвою вивіски підкаже реальну адресу (IP).

Коли ти вводиш сайт.com у браузер: 1) браузер питає DNS 'яка IP-адреса у сайт.com?', 2) отримує IP, 3) стукає в двері (порт 80 для HTTP або 443 для HTTPS), 4) сервер відповідає.

HTTPS — це HTTP + шифрування (SSL/TLS): як лист у запечатаному конверті замість листівки, яку може прочитати кожен листоноша по дорозі.

Технічніше: є два основні транспортні протоколи: TCP (гарантує доставку і порядок пакетів — використовує HTTP, SSH) і UDP (швидший, без гарантій — використовує стрімінг, ігри). Файрвол — це 'охоронець на вході': він вирішує, які порти відкриті для зовнішніх з'єднань. Саме тому для backend-сервера на хостингу доводиться явно відкривати потрібні порти (наприклад 443 для HTTPS, 3306 для MySQL) у налаштуваннях файрвола.

📄 Офіційна документація: MDN: HTTP status codes →
💻 Приклад коду
Термінал
# Перевірити, яка IP-адреса у домену
ping google.com

# Подивитись весь шлях пакету до сервера
traceroute google.com   # Linux/Mac
tracert google.com      # Windows
✏️ Завдання
1
Знайди IP свого сайту
Виконай ping для learn.nwstmsteam.workers.dev (або будь-якого сайту) і запиши, яку IP-адресу він показав.
2
HTTP vs HTTPS закріплення
Своїми словами поясни, чому вводити пароль на сайті з http:// (без S) небезпечніше, ніж на https://.
3
Перевір доступність бекенд-сервера поглиблено
Запусти php -S localhost:8000, а потім у новому вікні термінала виконай curl -I http://localhost:8000 — подивись, які HTTP-заголовки повертає твій власний сервер.
❓ Перевір себе
1. DNS потрібен, щоб:
Шифрувати трафік
Перетворювати домен на IP-адресу
Прискорювати завантаження сторінок
Зберігати паролі
DNS — це 'телефонна книга' інтернету: ім'я → IP.
2. Стандартний порт для HTTPS:
80
21
443
3306
443 — стандартний порт для захищеного HTTPS-з'єднання (80 — для звичайного HTTP).
3. HTTPS відрізняється від HTTP тим, що:
Працює швидше завжди
Додає шифрування трафіку
Не потребує домену
Працює тільки на мобільних
S у HTTPS = Secure, дані шифруються між браузером і сервером.
// СТАДІЯ 0 · УРОК 0.5

Git та VCS

📖 Теорія
🧠

Аналогія:Git — це машина часу для коду. Уяви, що пишеш курсову і зберігаєш 'курсова_v1.doc', 'курсова_final.doc', 'курсова_final_ТОЧНО.doc' — плутанина. Git замість цього робить 'знімки' (коміти) всього проєкту в певний момент, і ти завжди можеш повернутись до будь-якого знімку або порівняти, що змінилось.

GitHub — це як Google Drive для Git-проєктів: там зберігаються твої репозиторії онлайн, і кілька людей можуть працювати над одним кодом одночасно, кожен у своїй 'гілці' (branch), а потім зливати (merge) зміни докупи.

Базовий цикл роботи: git add (додати зміни у 'список на коміт'), git commit (зробити знімок з описом), git push (відправити знімки на GitHub).

Технічніше: файл .gitignore каже Git, які файли/папки ніколи не комітити (наприклад vendor/, .env з паролями, node_modules/). git diff показує точні рядки, які змінились, ще до коміту. git pull = git fetch (завантажити зміни з GitHub) + git merge (влити їх у твою гілку) — це два окремі кроки, об'єднані в одну зручну команду.

📄 Офіційна документація: Git — офіційна документація →
💻 Приклад коду
Термінал
git init                          # почати новий репозиторій
git status                        # що змінилось
git add index.php                 # додати файл до коміту
git commit -m "Додав головну сторінку"
git push origin main              # відправити на GitHub

git checkout -b feature/login     # створити нову гілку
git log --oneline                 # історія комітів коротко
✏️ Завдання
1
Перший репозиторій
Створи локально папку, ініціалізуй git (git init), створи файл README.md, зроби перший коміт з повідомленням 'Початковий коміт'.
2
Поясни гілку (branch) закріплення
Своїми словами і власним прикладом (не з курсу) поясни, навіщо потрібні гілки, якщо можна просто комітити все в один потік.
3
Feature-branch для нового ендпоінту поглиблено
Створи гілку feature/user-api, додай туди файл UserController.php із заглушкою класу, закомить з повідомленням 'Add user controller stub', і опиши, як би ти зробила merge в main після рев'ю.
❓ Перевір себе
1. Git commit — це:
Видалення файлу
Знімок стану проєкту з описом
Відправка на GitHub
Створення нової папки
Коміт фіксує поточний стан змінених файлів локально.
2. Щоб відправити зміни на GitHub, треба:
git commit
git push
git status
git init
git push відправляє локальні коміти у віддалений репозиторій.
3. Гілка (branch) потрібна, щоб:
Видалити стару версію
Працювати над функцією окремо, не чіпаючи основний код
Прискорити git
Створити резервну копію диска
Гілки дозволяють ізольовано розробляти нову фічу без ризику для main.
// СТАДІЯ 0 · УРОК 0.6

PhpStorm та IDE

📖 Теорія
🧠

Аналогія:писати код у Блокноті — як шити одяг голкою вручну в темряві. IDE (PhpStorm, VS Code) — це швейна машинка зі світлом: підказує помилки одразу (підкреслює червоним), автоматично довершує слова (автодоповнення), дозволяє миттєво перейти до визначення функції, і має вбудований дебагер — 'уповільнювач часу', який дозволяє зупинити виконання коду в певному рядку і подивитись, що відбувається всередині змінних.

Найкорисніші функції IDE для початківця: підсвітка синтаксису, автодоповнення (Ctrl+Space), перехід 'до визначення' (Ctrl+клік по функції), пошук по всьому проєкту (Shift+Shift у PhpStorm), і вбудований термінал.

Технічніше: PhpStorm інтегрується з Xdebug — розширенням PHP, яке і дає змогу реально зупиняти виконання на breakpoint і бачити стек викликів. Також є вбудована перевірка стилю коду (Code Sniffer / PSR-12), яка одразу підкреслює порушення стандартів жовтим, ще до того, як ти комітнеш код.

📄 Офіційна документація: PhpStorm — офіційна документація →
💻 Приклад коду
example.php
// Приклад того, що підказує IDE:
function calculateTotal(array $items): float {
    // IDE підкаже помилку типу, якщо повернути не float
    return array_sum($items);
}

// Автодоповнення підкаже методи класу після ->
$user = new User();
$user-> // тут IDE покаже список усіх доступних методів
✏️ Завдання
1
Налаштуй дебагер
У своєму IDE постав breakpoint (точку зупинки) на будь-якому рядку простого PHP-скрипта і запусти дебаг. Подивись, як виглядають значення змінних під час зупинки.
2
Три улюблені шорткати закріплення
Знайди і випробуй 3 гарячі клавіші у своєму IDE, яких ще не використовувала, і запиши, що вони роблять.
3
Дебаг backend-контролера поглиблено
Створи простий PHP-файл з функцією, що імітує обробку запиту (приймає масив 'user' і повертає його ім'я). Постав breakpoint усередині функції і в дебагері подивись, що саме приходить у параметр.
❓ Перевір себе
1. Головна перевага IDE над блокнотом:
Менший розмір файлу
Підказки помилок та автодоповнення
Швидший інтернет
Безкоштовність завжди
IDE аналізує код у реальному часі й підказує помилки та варіанти написання.
2. Breakpoint (точка зупинки) потрібна щоб:
Видалити рядок коду
Зупинити виконання і подивитись стан змінних
Автоматично виправити помилку
Закоментувати код
Breakpoint зупиняє виконання в дебагері саме на цьому рядку для аналізу.
// СТАДІЯ 0 · УРОК 0.7

Markdown та документація

📖 Теорія
🧠

Аналогія:Markdown — це як писати записку олівцем з простими позначками замість складного форматування у Word. Пишеш **жирний** і воно стає жирний — швидко і без мишки.

README.md у кожному GitHub-проєкті — це 'обкладинка книги', перше, що бачить людина: що за проєкт, як встановити, як запустити.

Технічніше: GitHub використовує розширену версію Markdown (GFM), яка додає таблиці (| Колонка 1 | Колонка 2 |), чекбокси завдань (- [ ] завдання / - [x] виконано) і автоматичні згадки issue/PR через #123.

📄 Офіційна документація: GitHub Flavored Markdown Spec →
💻 Приклад коду
README.md
# Заголовок проєкту

## Опис
Це навчальний PHP-проєкт.

## Встановлення
1. `composer install`
2. `php -S localhost:8000`

**Важливо:** потрібен PHP 8.1+

- пункт списку 1
- пункт списку 2

[Посилання на документацію](https://php.net)

```php
echo 'приклад коду в блоці';
```
✏️ Завдання
1
Напиши README
Створи файл README.md для будь-якого свого проєкту (навіть навчального) із заголовком, описом, списком кроків встановлення і хоч одним блоком коду.
2
Своя шпаргалка закріплення
Створи короткий .md файл-шпаргалку із 5 найкорисніших для тебе PHP-функцій, використавши заголовки та списки.
3
README для API поглиблено
Напиши README.md для уявного проєкту 'User API' зі списком ендпоінтів у форматі таблиці: GET /users, POST /users, GET /users/{id}, кожен з коротким описом що він робить.
❓ Перевір себе
1. Як зробити текст жирним у Markdown?
<b>текст</b>
**текст**
[текст]
# текст
Два зірочки з обох боків роблять текст жирним.
2. Символ # на початку рядка в Markdown означає:
Коментар
Заголовок
Посилання
Список
# — заголовок, кількість # визначає рівень (# найбільший, ### менший).
// СТАДІЯ 0 · УРОК 0.8

Англійська для розробника

📖 Теорія
🧠

Аналогія:англійська в IT — як латина в медицині: практично вся документація, повідомлення про помилки і назви функцій написані нею. Не треба говорити вільно — треба вміти читати технічний текст і розуміти помилки.

Найважливіші слова, які варто знати напам'ять: undefined (не визначено), expected (очікувалось), argument (аргумент), deprecated (застаріле, скоро приберуть), exception (виключення/помилка), array, string, boolean, return, throw.

Технічніше: ще кілька термінів, які варто розрізняти: deprecated (застаріле, але поки працює) ≠ removed (вже видалено і викличе помилку); immutable (об'єкт, який не можна змінити після створення — як readonly-властивості); idempotent (операція, яку можна повторити багато разів з тим самим результатом — наприклад HTTP PUT, на відміну від POST).

📄 Офіційна документація: PHP: Errors (термінологія) →
💻 Приклад коду
Повідомлення про помилку
// Типова помилка PHP англійською
Fatal error: Uncaught TypeError: strlen(): 
Argument #1 ($string) must be of type string, 
array given

// Переклад: Помилка типу: strlen() очікує, 
// що аргумент #1 буде рядком (string), 
// а йому передали масив (array)
✏️ Завдання
1
Розшифруй помилку
Знайди (або згадай) реальне повідомлення про помилку PHP, яке бачила на роботі, і перекладеш/поясниш українською кожне слово в ньому.
2
Термінологічний список закріплення
Склади список із 10 англійських IT-термінів (не з цього уроку) з перекладом, які часто зустрічаються у твоїй роботі.
3
Розбери реальний stack trace поглиблено
Знайди (або згадай) повний stack trace помилки з роботи (Fatal error з кількома 'in ... on line ...' рядками) і поясни українською, з якого файлу й рядка почалась проблема і через що вона 'пройшла' до кінця.
❓ Перевір себе
1. 'Undefined variable' означає:
Змінна видалена
Змінна ще не оголошена/не має значення
Змінна занадто велика
Змінна є константою
Undefined = не визначено — PHP не знає такої змінної.
2. 'Deprecated' щодо функції означає:
Функція зламана
Функція застаріла і скоро буде видалена
Функція найновіша
Функція працює тільки в тестах
Deprecated — застаріла функціональність, варто замінити на сучасний аналог.
// СТАДІЯ 0 · УРОК 0.9

Soft skills та звички розробника

📖 Теорія
🧠

Аналогія:код читають набагато частіше, ніж пишуть — як книгу читають частіше, ніж редагують. Тому звичка писати зрозумілі назви змінних і невеликі функції — це подарунок собі-майбутній та колегам.

Ключові звички: 1) Гугли помилку перед тим, як питати — часто відповідь вже є на Stack Overflow. 2) Розбивай велику задачу на маленькі кроки (як у цьому курсі — одна тема за раз). 3) Не соромся питати, коли справді застрягла — краще 5 хвилин ніяковості, ніж 5 годин у глухому куті. 4) Коміть часто, маленькими шматками — легше знайти, де зламалось.

Технічніше: два принципи, які часто згадують у код-рев'ю: DRY (Don't Repeat Yourself — не дублюй один і той же код у кількох місцях) і KISS (Keep It Simple, Stupid — не ускладнюй рішення там, де можна простіше). Обидва прямо пов'язані з тим, чому важливі зрозумілі назви й невеликі функції.

📄 Офіційна документація: PHP: The Right Way →
💻 Приклад коду
habits.php
// Погана звичка: незрозумілі назви
function f($a, $b) {
    return $a * $b * 0.2;
}

// Хороша звичка: зрозумілі назви = код сам себе документує
function calculateDiscount(float $price, int $quantity): float {
    $discountRate = 0.2;
    return $price * $quantity * $discountRate;
}
✏️ Завдання
1
Рефактор власного коду
Знайди будь-який свій старий скрипт з незрозумілими назвами змінних ($a, $x, $tmp) і перепиши з людськими назвами.
2
Щоденник помилок закріплення
Почни (або опиши план) короткий особистий список: помилка → як загуглила → рішення. Це стане твоєю особистою базою знань.
3
Code review культура поглиблено
Уяви, що твій колега надіслав Pull Request з новим API-ендпоінтом, але без жодних коментарів у коді і з назвами $a, $data2. Напиши 2-3 ввічливих, конструктивних коментарі до рев'ю, які ти залишила б.
❓ Перевір себе
1. Чому важливо давати змінним зрозумілі назви?
Це вимога PHP
Код читається частіше, ніж пишеться, і зрозумілі назви економлять час
Це прискорює виконання коду
Це зменшує розмір файлу
Зрозумілі назви — це інвестиція в майбутнє читання коду (своє й чуже).
// СТАДІЯ 1 · УРОК 1.1

Встановлення та оточення

📖 Теорія
🧠

Аналогія:PHP-інтерпретатор — це перекладач, який стоїть на сервері і перекладає твій код (.php файл) на дії, зрозумілі комп'ютеру. Без встановленого PHP файл .php — просто текст.

Найпростіший спосіб для розробки backend локально — вбудований сервер PHP: одна команда, і твій API вже доступний на localhost, без потреби налаштовувати Apache/Nginx.

SAPI (Server API): PHP може працювати в різних режимах — CLI (командний рядок, php script.php), вбудований dev-сервер, або PHP-FPM (для продакшн-серверів з Nginx). Кожен SAPI може мати трохи різні налаштування php.ini.

Ключові директиви php.ini для розробки: display_errors = On і error_reporting = E_ALL показують усі помилки прямо в консолі чи браузері (в продакшені їх вимикають, а помилки пишуть у лог-файл, щоб не показувати внутрішні деталі стороннім). memory_limit обмежує, скільки RAM може використати один скрипт.

📄 Офіційна документація: PHP: Встановлення та конфігурація →
💻 Приклад коду
Термінал
# Перевірити версію PHP
php -v

# Запустити вбудований dev-сервер API
php -S localhost:8000

# Виконати файл напряму без сервера (CLI SAPI)
php script.php

# Подивитись, який php.ini реально використовується
php --ini
✏️ Завдання
1
Перший API-ендпоінт
Створи файл api.php з echo json_encode(['status' => 'ok', 'service' => 'shop-api']); і header('Content-Type: application/json'). Запусти php -S localhost:8000 і відкрий localhost:8000/api.php у браузері.
2
Знайди свій php.ini закріплення
Виконай php --ini в терміналі, знайди шлях до файлу php.ini і подивись, яке значення має memory_limit і display_errors.
3
CLI-скрипт для фонових задач поглиблено
Створи файл cleanup.php, що приймає аргумент з командного рядка через $argv (наприклад кількість днів), і виводить 'Видаляю замовлення старші за N днів...'. Запусти php cleanup.php 30 — так у backend роблять cron-скрипти для фонових задач.
❓ Перевір себе
1. Команда для запуску вбудованого сервера PHP:
php run
php -S localhost:8000
php start
php serve
php -S host:port запускає вбудований dev-сервер.
2. php.ini — це файл, що містить:
Код проєкту
Налаштування PHP-інтерпретатора
Список пакетів Composer
Логи помилок
php.ini визначає поведінку інтерпретатора.
3. SAPI (наприклад CLI чи PHP-FPM) — це:
Тип бази даних
Різні режими/інтерфейси, в яких може працювати PHP-інтерпретатор
Назва IDE
Формат файлу конфігурації
CLI, вбудований сервер, PHP-FPM -- різні SAPI з можливо різними налаштуваннями.
// СТАДІЯ 1 · УРОК 1.2

Синтаксис та типи

📖 Теорія
🧠

Аналогія:змінна — це підписана коробка. $price = 1899.00 означає 'постав у коробку з ярликом price значення 1899.00'. PHP — мова з динамічною типізацією.

Основні типи: int, float, string, bool, array, null. З PHP 7+ можна вказувати типи явно (type hinting).

Type juggling (жонглювання типами): без strict_types=1 PHP автоматично приводить типи одне до одного: '5' + 3 дасть 8, функція, що очікує int, мовчки прийме рядок '5'. Це джерело неочевидних багів у продакшн-коді — тому в сучасному PHP майже завжди варто вмикати declare(strict_types=1); на початку файлу.

Порівняння == vs ===: == порівнює значення ПІСЛЯ приведення типів (нестроге), === порівнює і значення, і тип (строге). Класичний приклад проблеми: до PHP 8 вираз 0 == 'abc' повертав true. Завжди використовуй ===, якщо немає конкретної причини для ==.

📄 Офіційна документація: PHP: Типи даних →
💻 Приклад коду
types.php
<?php
declare(strict_types=1);

function calculateTotal(float $price, int $quantity, bool $hasDiscount): float
{
    $total = $price * $quantity;

    if ($hasDiscount) {
        $total *= 0.9;
    }

    return round($total, 2);
}

$sku = 'SKU-1001';        // string
$price = 1899.00;         // float
$quantity = 2;             // int
$inStock = true;           // bool
$tags = ['keyboard', 'mechanical']; // array
$discountCode = null;      // null

echo calculateTotal($price, $quantity, true); // 3418.2

var_dump(5 === '5');   // false (різні типи)
var_dump(5 == '5');    // true (нестроге порівняння)
✏️ Завдання
1
Функція розрахунку суми замовлення
Напиши функцію calculateOrderTotal(float $price, int $quantity, float $taxRate): float з strict_types=1, яка рахує суму з урахуванням податку. Виклич з кількома прикладами і виведи var_dump() результату, щоб побачити тип float.
2
Перевірка типів вхідних даних закріплення
Напиши функцію isValidPrice(mixed $value): bool, яка через is_float()/is_int() і перевірку > 0 визначає, чи є значення коректною ціною. Протестуй на рядку, від'ємному числі і нормальній ціні.
3
Типізований DTO без класів (масив зі строгою перевіркою) поглиблено
Напиши функцію parseProductInput(array $data): array, яка через strict_types перевіряє, що $data['price'] — float, $data['quantity'] — int, і кидає TypeError вручну (через явну перевірку), якщо тип не збігається — прообраз того, чим у Стадії 2 стануть DTO-класи.
❓ Перевір себе
1. Що виведе gettype(4.5)?
float
double
number
decimal
Історично PHP називає float-тип 'double' у функції gettype().
2. declare(strict_types=1) означає:
Заборонити змінні
Вмикає сувору перевірку типів аргументів функцій
Прискорює виконання коду
Забороняє масиви
Без strict_types PHP намагається автоматично конвертувати типи.
3. $isInStock = true; — це змінна типу:
string
int
bool
null
true/false — значення типу bool.
4. Чому 5 === '5' повертає false, а 5 == '5' повертає true?
=== порівнює тільки числа
=== враховує тип значення, а == приводить типи одне до одного перед порівнянням
Це помилка PHP
== працює тільки з рядками
Строге порівняння (===) вимагає однакового типу і значення.
// СТАДІЯ 1 · УРОК 1.3

Керуючі конструкції

📖 Теорія
🧠

Аналогія:if/else — розвилка: 'якщо роль адмін — дозволь видалення, інакше — заборони'. Цикли (for, while, foreach) — конвеєр: повторюй дію, поки виконується умова.

for — коли знаєш точну кількість повторів. while — коли повторюєш, поки умова істинна. foreach — для перебору масивів, найчастіше використовується в backend для обробки результатів запиту до БД.

match (з PHP 8) — сучасніша й безпечніша заміна switch: суворо порівнює значення (як ===) і одразу повертає результат без 'провалювання' між case.

Оператор ?? (null coalescing): $role = $request['role'] ?? 'guest'; — 'взяти значення, якщо воно існує і не null, інакше — значення за замовчуванням'. Це найпоширеніший спосіб обробки необов'язкових параметрів API-запиту. Скорочений тернарний ?: перевіряє на falsy (0, '', false), а не саме на null.

📄 Офіційна документація: PHP: Керуючі конструкції →
💻 Приклад коду
access.php
<?php
declare(strict_types=1);

function checkAccess(string $role): bool
{
    return match($role) {
        'admin', 'manager' => true,
        'customer' => false,
        default => false,
    };
}

$request = ['user_id' => 42];
$role = $request['role'] ?? 'guest'; // немає ключа 'role' -- беремо 'guest'

if (checkAccess($role)) {
    echo "Доступ дозволено\n";
} else {
    echo "Доступ заборонено, роль: $role\n";
}

// foreach -- типова обробка результатів запиту до БД
$orders = [
    ['id' => 1, 'status' => 'paid'],
    ['id' => 2, 'status' => 'pending'],
];
foreach ($orders as $order) {
    echo "Замовлення #{$order['id']}: {$order['status']}\n";
}
✏️ Завдання
1
Перевірка ролі перед доступом до API
Напиши функцію checkAccess(string $role), яка через match() повертає true/false: тільки 'admin' і 'manager' мають доступ до видалення записів, 'customer' — тільки до читання.
2
Обробка списку замовлень закріплення
Створи масив із 5 асоціативних масивів-замовлень (id, status). Через foreach виведи тільки ті, у яких status === 'paid', разом з номером по порядку.
3
Роутер на основі match() поглиблено
Напиши функцію route(string $method, string $path): string, яка через match() з кількома умовами повертає назву 'контролера' для комбінацій методу і шляху (наприклад 'GET /products' → 'ProductController@index') — так спрощено влаштовані роутери в backend-фреймворках.
❓ Перевір себе
1. foreach найчастіше використовують для:
Умовних розгалужень
Перебору масивів
Оголошення функцій
Підключення файлів
foreach спеціально створений для ітерації по масивах чи об'єктах Traversable.
2. Чим match() відрізняється від switch()?
Match повільніший
Match використовує строге порівняння (===) і не потребує break
Match не може мати default
Match працює тільки з числами
match суворо порівнює значення і автоматично повертає результат без 'провалювання' між case.
3. $role = $request['role'] ?? 'guest'; -- оператор ?? означає:
Побітове АБО
Взяти значення, якщо воно існує і не null, інакше -- значення за замовчуванням
Перевірку на рівність
Циклічний оператор
Null coalescing -- найпоширеніший спосіб обробки необов'язкових параметрів запиту.
4. Цикл for найкраще підходить, коли:
Кількість повторів невідома
Кількість повторів відома заздалегідь
Треба перебрати асоціативний масив
Треба порівняти типи
for ідеальний, коли є лічильник із чіткою кількістю ітерацій.
// СТАДІЯ 1 · УРОК 1.4

Масиви та колекції

📖 Теорія
🧠

Аналогія:масив — це шафа з шухлядками. У backend результат запиту до бази даних майже завжди приходить саме масивом асоціативних масивів — кожен елемент представляє один рядок таблиці.

Найкорисніші функції: array_map (застосувати функцію до кожного елемента), array_filter (залишити тільки те, що підходить під умову), array_reduce (звести весь масив до одного значення).

array_merge vs оператор +: array_merge($a, $b) перезаписує однакові текстові ключі значеннями з другого масиву і переіндексовує числові ключі. Оператор $a + $b залишає значення з першого масиву, якщо ключ уже є в ньому — це не одне й те саме, і плутанина тут -- часте джерело багів.

Деструктуризація: ['id' => $id, 'price' => $price] = $product; дозволяє одразу 'розпакувати' асоціативний масив у окремі змінні -- зручно для обробки одного запису з результату запиту.

📄 Офіційна документація: PHP: Масиви →
💻 Приклад коду
products.php
<?php
declare(strict_types=1);

$products = [
    ['sku' => 'SKU-1001', 'price' => 1899.00, 'inStock' => true],
    ['sku' => 'SKU-2005', 'price' => 350.00, 'inStock' => false],
    ['sku' => 'SKU-3010', 'price' => 799.00, 'inStock' => true],
];

// array_filter: тільки товари в наявності
$available = array_filter($products, fn($p) => $p['inStock']);

// array_map: витягнути тільки ціни
$prices = array_map(fn($p) => $p['price'], $available);

// array_reduce: сума всіх цін товарів у наявності
$total = array_reduce($prices, fn($carry, $p) => $carry + $p, 0.0);

echo count($available); // 2
print_r($prices);
echo $total; // 2698

// Деструктуризація одного запису
['sku' => $sku, 'price' => $price] = $products[0];
echo "$sku коштує $price грн\n";
✏️ Завдання
1
Обробка списку товарів зі 'складу'
Створи масив із 5 асоціативних масивів товарів (sku, price, inStock). Через array_filter знайди тільки товари в наявності, через array_map витягни їхні ціни, через array_reduce порахуй загальну суму.
2
Деструктуризація результату запиту закріплення
Візьми один запис-замовлення (масив з ключами id, customerEmail, total) і деструктуризуй його в окремі змінні одним рядком. Виведи форматоване повідомлення з цими змінними.
3
Групування замовлень по статусу поглиблено
Напиши функцію groupByStatus(array $orders): array, яка приймає масив замовлень (кожне з ключем status) і повертає асоціативний масив, де ключі — статуси, а значення — масиви замовлень цього статусу. Це типова задача для API-звіту 'скільки замовлень у кожному статусі'.
❓ Перевір себе
1. Асоціативний масив відрізняється від звичайного тим, що:
Не може містити рядки
Ключі — це імена/рядки, а не тільки числа за замовчуванням
Завжди сортований
Має фіксовану довжину
В асоціативному масиві ключами можуть бути довільні рядки.
2. array_filter() повертає:
Суму елементів
Новий масив з елементами, що пройшли умову
Кількість елементів
Відсортований масив
array_filter залишає тільки ті елементи, для яких callback повернув true.
3. array_reduce() використовують, щоб:
Видалити дублікати
Звести масив до одного підсумкового значення
Розбити масив на частини
Відсортувати за ключем
reduce 'згортає' весь масив в одне значення через акумулятор.
4. Чим array_merge($a, $b) відрізняється від $a + $b?
Це повні синоніми
array_merge перезаписує спільні текстові ключі значеннями з $b, а + залишає значення з $a
+ працює тільки з числовими масивами
array_merge працює тільки з JSON
Це поширене джерело плутанини — поведінка при спільних ключах абсолютно різна.
// СТАДІЯ 1 · УРОК 1.5

Рядки та Regex

📖 Теорія
🧠

Аналогія:рядок — це нитка з літер. Функції роботи з рядками — ножиці й голка: обрізати (substr), склеїти (implode), знайти шматок (strpos), замінити (str_replace).

Регулярні вирази (regex) — потужніший 'пошуковий шаблон', незамінний для валідації вхідних даних API (email, телефон, SKU) і складного пошуку/заміни.

Форматований вивід: sprintf('Ціна: %.2f грн', $price) повертає рядок за шаблоном — зручно для формування текстових повідомлень API-відповіді чи логів.

Multibyte-функції: якщо працюєш з кирилицею чи іншими не-ASCII символами, звичайні strlen/substr рахують БАЙТИ, а не символи, і можуть 'розрізати' UTF-8 символ навпіл. Для коректної роботи з такими рядками є mb_-функції (mb_strlen, mb_substr).

📄 Офіційна документація: PHP: Рядки →
💻 Приклад коду
strings.php
<?php
declare(strict_types=1);

$sku = '  sku-1001  ';
$cleanSku = strtoupper(trim($sku)); // 'SKU-1001'

// Regex: валідація email при реєстрації
$email = 'client@shop.com';
if (preg_match('/^[\w.+-]+@[\w-]+\.[a-z]{2,}$/i', $email)) {
    echo "Email валідний\n";
}

// Regex: перевірка формату SKU (3 літери - 4 цифри)
$validSkuPattern = '/^[A-Z]{3}-\d{4}$/';
var_dump(preg_match($validSkuPattern, 'SKU-1001')); // 1 (валідний)
var_dump(preg_match($validSkuPattern, 'sku1001'));  // 0 (невалідний)

// sprintf для форматованого API-повідомлення
$message = sprintf('Товар %s додано до кошика, ціна: %.2f грн', $cleanSku, 1899.0);
echo $message;
✏️ Завдання
1
Валідація email при реєстрації
Напиши функцію isValidEmail(string $email): bool, яка через preg_match перевіряє формат email. Протестуй на 5 різних рядках (валідних і невалідних).
2
Нормалізація SKU товару закріплення
Напиши функцію normalizeSku(string $sku): string, яка обрізає пробіли (trim) і робить усі літери великими (strtoupper) — так вхідні дані API 'очищають' перед збереженням у базу.
3
Валідація формату номера телефону поглиблено
Напиши функцію isValidPhone(string $phone): bool, яка через regex перевіряє формат +380XXXXXXXXX. Додай функцію formatOrderMessage(string $sku, float $price): string через sprintf для формування тексту SMS-сповіщення про замовлення.
❓ Перевір себе
1. Функція strpos() повертає:
Довжину рядка
Позицію першого входження підрядка (або false)
Новий рядок у верхньому регістрі
Кількість слів
strpos шукає позицію підрядка; якщо не знайдено — повертає false.
2. preg_match() використовують для:
Склеювання рядків
Перевірки рядка на відповідність регулярному виразу
Видалення пробілів
Конвертації типів
preg_match перевіряє/шукає збіг за шаблоном регулярного виразу.
3. Чому для рядків з кирилицею краще використовувати mb_strlen, а не strlen?
mb_strlen швидший завжди
strlen рахує байти, а не символи, і може порахувати кирилицю неправильно
strlen не працює з рядками взагалі
mb_strlen -- єдина функція, що приймає UTF-8
Кожен кириличний символ у UTF-8 займає 2 байти, тому strlen('Привіт') поверне 12, а не 6.
// СТАДІЯ 1 · УРОК 1.6

Функції та замикання

📖 Теорія
🧠

Аналогія:функція — це багаторазовий інструмент: даєш йому вхідні дані (аргументи), він щось робить, повертає результат. Один раз написав функцію calculateTax() — використовуєш її в будь-якому місці backend-коду.

Анонімна функція (closure) — 'одноразовий' інструмент без назви, часто передається як аргумент іншій функції (наприклад у array_map). use ($var) дозволяє замиканню 'запам'ятати' зовнішню змінну.

Стрілочні функції fn($x) => $x * 2 — коротший запис closure, автоматично 'бачать' зовнішні змінні без явного use().

Variadic-функції: function sum(int ...$nums) приймає будь-яку кількість аргументів, зібраних у масив. First-class callable syntax (PHP 8.1+): замість 'strlen' чи [$obj, 'method'] можна писати strlen(...) — створює Closure напряму з існуючої функції.

📄 Офіційна документація: PHP: Функції →
💻 Приклад коду
functions.php
<?php
declare(strict_types=1);

function calculateTax(float $amount, float $rate = 0.2): float
{
    return round($amount * $rate, 2);
}

// Closure з use() -- 'запам'ятовує' зовнішню знижку
$discountRate = 0.1;
$applyDiscount = function(float $price) use ($discountRate): float {
    return round($price * (1 - $discountRate), 2);
};
echo $applyDiscount(1899.00); // 1709.1

// Стрілочна функція -- автоматично 'бачить' зовнішні змінні
$shippingCost = 50.0;
$withShipping = fn($total) => $total + $shippingCost;
echo $withShipping(1899.00); // 1949

// Variadic: сума будь-якої кількості товарів у кошику
function cartTotal(float ...$prices): float
{
    return array_sum($prices);
}
echo cartTotal(1899.00, 350.00, 799.00); // 3048

// First-class callable syntax (PHP 8.1+)
$mapper = strtoupper(...);
echo $mapper('sku-1001'); // SKU-1001
✏️ Завдання
1
Функція розрахунку суми з податком
Напиши функцію calculateTax(float $amount, float $rate = 0.2): float. Виклич з різними ставками, зокрема без явного другого аргументу (щоб перевірити значення за замовчуванням).
2
Closure для застосування знижки за промокодом закріплення
Створи closure, яке 'запам'ятовує' конкретний відсоток знижки через use() і застосовує його до переданої ціни. Створи два різні closure для різних промокодів (10% і 20%).
3
Middleware-функція перевірки токена поглиблено
Напиши closure $authMiddleware = function(array $request) use ($validTokens) {...}, яка перевіряє, чи є токен запиту в масиві дозволених токенів, і повертає true/false. Це базовий принцип middleware в backend-фреймворках (Laravel, Symfony).
❓ Перевір себе
1. use ($var) у closure потрібен, щоб:
Оголосити нову змінну
'Захопити' зовнішню змінну всередину анонімної функції
Видалити змінну після виконання
Зробити змінну глобальною назавжди
use() передає значення зовнішньої змінної всередину тіла closure.
2. Стрілочна функція fn($x) => $x*2 автоматично:
Не має доступу до зовнішніх змінних
Має доступ до зовнішніх змінних без use()
Повертає void
Працює тільки з масивами
На відміну від звичайного function(), fn() автоматично захоплює зовнішній контекст.
3. function sum(int ...$nums) дозволяє:
Прийняти лише один параметр
Прийняти будь-яку кількість аргументів, зібраних у масив $nums
Прийняти тільки масиви
Заборонити виклик з параметрами
Три крапки перед параметром -- variadic-функція.
4. strtoupper(...) (з трьома крапками, PHP 8.1+) створює:
Масив рядків
Closure з існуючої функції (first-class callable syntax)
Копію рядка у верхньому регістрі
Помилку синтаксису
Це скорочений спосіб створити Closure з існуючої функції чи методу.
// СТАДІЯ 1 · УРОК 1.7

Файли та формати (JSON, CSV)

📖 Теорія
🧠

Аналогія:файл на диску — аркуш паперу в шафі. file_get_contents — 'дістати й прочитати', file_put_contents — 'записати і покласти назад'.

JSON — універсальний формат для обміну структурованими даними між системами. PHP-масив ↔ JSON конвертується через json_encode / json_decode. Це саме той формат, у якому реальні API повертають відповіді клієнту.

Формування коректної API-відповіді: header('Content-Type: application/json') + http_response_code($code) + echo json_encode($data) — це стандартний 'скелет' відповіді бекенду будь-якого REST API, написаного на чистому PHP.

Безпечна робота з файлами: перед читанням файлу варто перевірити його існування (file_exists()), а для великих файлів рядок за рядком краще використовувати fopen() + fgets() у циклі, щоб не завантажувати весь файл у пам'ять одразу.

📄 Офіційна документація: PHP: Файлова система →
💻 Приклад коду
api.php
<?php
declare(strict_types=1);

function jsonResponse(array $data, int $statusCode = 200): void
{
    http_response_code($statusCode);
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}

$products = [
    ['sku' => 'SKU-1001', 'price' => 1899.00],
    ['sku' => 'SKU-2005', 'price' => 350.00],
];

// Зберегти 'кеш' товарів у JSON-файл
file_put_contents('products_cache.json', json_encode($products));

// Прочитати назад
$cached = json_decode(file_get_contents('products_cache.json'), true);
print_r($cached);

// Типова API-відповідь
jsonResponse(['products' => $products, 'count' => count($products)]);

// CSV-експорт замовлень
$fp = fopen('orders_export.csv', 'w');
fputcsv($fp, ['order_id', 'total']);
fputcsv($fp, [4521, 1899.00]);
fclose($fp);
✏️ Завдання
1
Функція jsonResponse для API
Напиши функцію jsonResponse(array $data, int $statusCode = 200): void, яка виставляє http_response_code($statusCode), header('Content-Type: application/json') і echo json_encode($data) — саме так формують відповідь у реальних API.
2
Кешування списку товарів у JSON закріплення
Створи масив 3 товарів, збережи його у products_cache.json через file_put_contents+json_encode, потім прочитай назад через file_get_contents+json_decode і виведи через print_r.
3
Експорт замовлень у CSV поглиблено
Напиши функцію exportOrdersToCsv(array $orders, string $filename): void, яка через fopen/fputcsv записує масив замовлень (order_id, customer, total) у CSV-файл — типова задача 'вивантажити звіт' для бухгалтерії чи адмінки.
❓ Перевір себе
1. json_decode($json, true) з параметром true повертає:
Об'єкт stdClass
Асоціативний масив
Рядок JSON без змін
Число
Другий аргумент true каже PHP повернути асоціативний масив замість об'єкта.
2. header('Content-Type: application/json') потрібен, щоб:
Прискорити відповідь сервера
Повідомити клієнту (браузеру/додатку), що тіло відповіді -- це JSON, а не HTML
Зашифрувати дані
Створити новий файл
Content-Type -- це метадані відповіді, які кажуть клієнту, як інтерпретувати тіло відповіді.
3. Для читання ВЕЛИКОГО файлу рядок за рядком (без завантаження всього у пам'ять) краще використати:
file_get_contents повністю
fopen() + fgets() у циклі
json_decode напряму
print_r
file_get_contents завантажує весь файл одразу в пам'ять, що може перевищити memory_limit для дуже великих файлів.
// СТАДІЯ 1 · УРОК 1.8

Обробка помилок

📖 Теорія
🧠

Аналогія:try/catch — страхувальна сітка. 'Спробуй (try) виконати запит до бази даних; якщо стався збій (виключення) — сітка (catch) тебе спіймає', і API поверне зрозумілу помилку клієнту, а не 'впаде' повністю (500 Internal Server Error без пояснень).

Ієрархія Throwable (офіційно, php.net): усі помилки й виключення в PHP реалізують інтерфейс Throwable, який розділяється на дві гілки: Error (внутрішні помилки PHP -- TypeError, DivisionByZeroError) і Exception (виключення, які кидає код розробника, наприклад InvalidArgumentException, RuntimeException). Можна перехопити обидві гілки одним catch (Throwable $e).

finally виконується завжди — і при успіху, і при помилці (наприклад, щоб гарантовано закрити з'єднання з базою даних незалежно від результату).

Типова backend-практика: у контролері API перехоплюють виключення і повертають клієнту зрозумілий JSON з кодом статусу (404, 400, 500), а не 'сирий' текст помилки PHP, який може розкрити внутрішню структуру системи.

📄 Офіційна документація: PHP: Виключення →
💻 Приклад коду
errors.php
<?php
declare(strict_types=1);

class OrderNotFoundException extends RuntimeException {}

function fetchOrderById(int $id, array $database): array
{
    if (!isset($database[$id])) {
        throw new OrderNotFoundException("Замовлення #$id не знайдено");
    }
    return $database[$id];
}

function jsonResponse(array $data, int $status = 200): void
{
    http_response_code($status);
    echo json_encode($data);
}

$database = [4521 => ['id' => 4521, 'total' => 1899.00]];

try {
    $order = fetchOrderById(9999, $database);
    jsonResponse(['order' => $order]);
} catch (OrderNotFoundException $e) {
    jsonResponse(['error' => $e->getMessage()], 404);
} catch (Throwable $e) {
    jsonResponse(['error' => 'Внутрішня помилка сервера'], 500);
} finally {
    echo "\n--- Запит оброблено ---";
}
✏️ Завдання
1
Обробка помилки 'запис не знайдено'
Створи власний клас OrderNotFoundException extends RuntimeException. Напиши функцію fetchOrderById(int $id, array $database), яка кидає це виключення, якщо запису немає. Обгорни виклик у try/catch і при помилці поверни jsonResponse(['error'=>...], 404).
2
Валідація ставки з кількома типами помилок закріплення
Напиши функцію validateOrderAmount(float $amount), яка кидає InvalidArgumentException при від'ємній сумі, і окремий RuntimeException при перевищенні ліміту 100000. Обробай обидва типи окремими catch-блоками.
3
Глобальний обробник помилок API поглиблено
Напиши функцію handleApiException(Throwable $e): void, яка визначає код статусу залежно від типу виключення (InvalidArgumentException -> 400, RuntimeException -> 500, інше -> 500) і формує єдиний формат jsonResponse(['error' => $e->getMessage()], $code) -- так у реальних API централізують обробку помилок в одному місці.
❓ Перевір себе
1. Блок finally виконується:
Тільки при помилці
Тільки при успіху
Завжди, незалежно від результату try/catch
Ніколи, якщо є catch
finally виконується завжди -- гарне місце для 'прибирання' ресурсів.
2. Throwable в PHP розділяється на дві гілки:
Warning і Notice
Error і Exception
Fatal і NonFatal
Static і Dynamic
Error -- внутрішні помилки PHP, Exception -- виключення, які кидає код розробника.
3. Чому в API-контролері краще перехоплювати виключення, а не давати PHP 'впасти' з 500 помилкою?
Це заборонено PHP
Це дозволяє повернути клієнту зрозумілий, безпечний JSON замість розкриття внутрішньої структури системи
Це прискорює виконання коду
Try/catch обов'язковий за стандартом PSR
Централізована обробка виключень -- стандартна практика для будь-якого production API.
// СТАДІЯ 1 · УРОК 1.9

Composer та пакети

📖 Теорія
🧠

Аналогія:Composer — менеджер продуктів для кухні backend-проєкту: замість самій писати логіку роботи з датами чи JWT-токенами з нуля, замовляєш готовий, перевірений пакет із каталогу Packagist.

composer.json — список 'покупок' (залежностей), composer.lock — точний чек з версіями, які реально встановлені у ВСІХ розробників команди і на сервері однаково.

Версійні обмеження: ^2.5 означає 'будь-яка версія від 2.5 до наступної мажорної (3.0 виключно)' (сумісні зміни), ~2.5 — вужче, тільки патч-версії в межах 2.5.x.

Автозавантаження (PSR-4 autoload): замість ручного require кожного файлу, PHP сам знаходить потрібний клас за неймспейсом через require 'vendor/autoload.php' — цей файл Composer генерує автоматично при кожному встановленні пакета.

📄 Офіційна документація: Composer — офіційна документація →
💻 Приклад коду
Термінал
# Ініціалізувати проєкт backend API
composer init

# Встановити пакет для роботи з датами
composer require nesbot/carbon

# Встановити пакет для JWT-токенів авторизації
composer require firebase/php-jwt

# Встановити пакет тільки для розробки (тести)
composer require --dev phpunit/phpunit
✏️ Завдання
1
Встанови перший пакет для API
Створи новий проєкт, виконай composer init, потім встанови nesbot/carbon і використай його для форматування дати створення замовлення в PHP-файлі.
2
Подивись автозавантаження закріплення
Відкрий папку vendor/composer/ у своєму проєкті й знайди файл autoload_psr4.php — подивись, як він 'мапить' неймспейси на папки.
3
JWT-пакет для авторизації API поглиблено
Погугли пакет firebase/php-jwt на Packagist, подивись README і опиши своїми словами (без встановлення), які два основні методи (encode/decode) він дає для роботи з токенами авторизації в API.
❓ Перевір себе
1. composer.json містить:
Скомпільований код
Список залежностей проєкту та їх версії
Логи помилок
HTML-шаблони
composer.json -- це маніфест проєкту зі списком потрібних пакетів.
2. require vendor/autoload.php потрібен, щоб:
Запустити сервер
Автоматично підключити всі встановлені через Composer класи
Видалити пакети
Оновити PHP
Autoload дозволяє використовувати класи з пакетів без ручного require кожного файлу.
3. Версійне обмеження ^2.5 у composer.json означає:
Точно версія 2.5, без винятків
Будь-яка версія від 2.5 до наступної мажорної (3.0 виключно)
Будь-яка версія 2.x чи новіша
Тільки застарілі версії
Каретка (^) дозволяє сумісні оновлення в межах тієї ж мажорної версії.
// СТАДІЯ 1 · УРОК 1.10

PSR-стандарти

📖 Теорія
🧠

Аналогія:PSR (PHP Standards Recommendations) — єдиний стандарт розеток: якщо всі розробники дотримуються одного стилю коду, будь-яка бібліотека 'підійде' до будь-якого проєкту без 'перехідників'.

PSR-1 / PSR-12 — стандарти стилю коду: класи — PascalCase, методи — camelCase, відступ — 4 пробіли, файли з класами не повинні мати 'побічних ефектів' (echo, вивід html) при простому підключенні — тільки оголошення.

PSR-4 — стандарт автозавантаження: неймспейс класу відповідає реальному шляху файлу. Якщо в composer.json прописано "App\\": "src/", то клас App\Controllers\ProductController Composer шукатиме у файлі src/Controllers/ProductController.php.

Дотримання PSR робить код зрозумілим для будь-якого іншого PHP-розробника і критично важливе, коли над одним backend-проєктом працює команда.

📄 Офіційна документація: PHP-FIG: PSR стандарти →
💻 Приклад коду
ProductController.php
<?php
declare(strict_types=1);

namespace App\Controllers;

use App\Repositories\ProductRepository;

// PSR-1/12: клас -- PascalCase, файл = ім'я класу (ProductController.php)
class ProductController
{
    public function __construct(private readonly ProductRepository $repository) {}

    // PSR-12: метод -- camelCase, відступ 4 пробіли
    public function index(): array
    {
        $products = $this->repository->all();

        return [
            'status' => 'ok',
            'products' => $products,
        ];
    }

    public function show(int $id): array
    {
        $product = $this->repository->find($id);

        return $product !== null
            ? ['status' => 'ok', 'product' => $product]
            : ['status' => 'error', 'message' => 'Product not found'];
    }
}
✏️ Завдання
1
Контролер за PSR-12
Напиши клас ProductController з методом index(): array (імітує GET /products) за всіма правилами PSR-12: namespace App\Controllers, PascalCase клас, camelCase метод, 4 пробіли відступ.
2
Приведи код до PSR-12 закріплення
Візьми будь-який свій старий файл із неправильними відступами/назвами (snake_case для методів) і перепиши за правилами PSR-12.
3
Структура проєкту за PSR-4 поглиблено
Опиши (текстом, не обов'язково створювати реально) структуру папок для composer.json з autoload "App\\": "src/" — де саме повинні лежати файли ProductController.php (неймспейс App\Controllers) і ProductRepository.php (неймспейс App\Repositories), щоб PSR-4 автозавантаження знайшло їх коректно.
❓ Перевір себе
1. За PSR-12, назва класу повинна бути в стилі:
snake_case
camelCase
PascalCase
kebab-case
Класи іменуються у PascalCase, наприклад ProductController.
2. За PSR-12, назва методу класу повинна бути в стилі:
PascalCase
camelCase
UPPER_CASE
kebab-case
Методи й змінні за конвенцією camelCase, наприклад calculateTotal().
3. Якщо composer.json має "App\\": "src/", де PSR-4 шукатиме клас App\Controllers\ProductController?
src/ProductController.php
src/Controllers/ProductController.php
app/Controllers/ProductController.php
controllers/App/ProductController.php
Неймспейс після App\ (Controllers\ProductController) відповідає шляху всередині src/.
// СТАДІЯ 2 · УРОК 2.1

Класи, об'єкти, конструктори, readonly

📖 Теорія
🧠

Аналогія:клас — це креслення (blueprint) товару на складі: воно описує, які поля обов'язкові (назва, ціна, SKU) і що з товаром можна робити. Об'єкт — це конкретний товар на полиці, створений за цим кресленням. Кожен товар має власні значення (своя назва, своя ціна), хоча структура в усіх однакова.

Що таке клас формально: клас — це визначення типу даних, що об'єднує стан (властивості) і поведінку (методи) в одну сутність. Створення об'єкта — інстанціювання — відбувається через new ClassName(...), і в цей момент PHP автоматично викликає метод __construct(), якщо він визначений.

Constructor Promotion (з PHP 8.0): замість того, щоб окремо оголошувати властивість і окремо присвоювати її в тілі конструктора, можна оголосити властивість прямо в списку параметрів конструктора, вказавши модифікатор доступу. Це не 'магія' — компілятор PHP просто розгортає це в звичайне оголошення властивості + присвоєння, це чистий синтаксичний цукор, який економить рядки коду і зменшує дублювання назви поля.

readonly (з PHP 8.1): властивість, позначена readonly, обов'язково повинна мати вказаний тип, не може мати значення за замовчуванням у оголошенні, і присвоїти їй значення можна рівно один раз — з коду всередині того самого класу (найчастіше в конструкторі). Будь-яка повторна спроба запису викличе Error: Cannot modify readonly property. Це ідеально підходить для DTO (Data Transfer Object) і Value Object — сутностей, які логічно не повинні змінюватись після створення, наприклад товар з фіксованим SKU чи сума платежу.

Типова помилка початківців: забувати вказувати тип для readonly-властивості (readonly $price без типу — фатальна помилка компіляції) або намагатись задати значення за замовчуванням (public readonly float $price = 0.0 — теж помилка, бо це вже вважається 'записом').

📄 Офіційна документація: PHP: Властивості класів →
💻 Приклад коду
Product.php
<?php
declare(strict_types=1);

final class Product
{
    public function __construct(
        public readonly string $sku,
        public readonly string $name,
        private float $price,
    ) {}

    public function getPrice(): float
    {
        return $this->price;
    }

    public function applyDiscount(float $percent): void
    {
        $this->price = round($this->price * (1 - $percent / 100), 2);
    }
}

$product = new Product('SKU-1001', 'Механічна клавіатура', 1899.00);
echo $product->name;
echo $product->getPrice();

$product->applyDiscount(10);
echo $product->getPrice();

// $product->sku = 'SKU-9999'; // Fatal error: Cannot modify readonly property
✏️ Завдання
1
Клас Product для API інтернет-магазину
Створи клас Product з readonly-властивостями sku і name (через Constructor Promotion), і приватною властивістю price (не readonly, бо ціну можна міняти). Додай методи getPrice(): float та applyDiscount(float $percent): void. Створи 2 товари й перевір застосування знижки.
2
DTO для запиту створення замовлення закріплення
Створи клас CreateOrderRequest — типовий DTO (Data Transfer Object), яким API приймає вхідні дані. Властивості: readonly string $customerEmail, readonly string $productSku, readonly int $quantity — усі через Constructor Promotion. Додай метод toArray(): array.
3
Валідація перед створенням DTO поглиблено
У реальному API вхідні дані спочатку валідують, а вже потім створюють DTO. Напиши функцію createOrderRequestFromArray(array $data): CreateOrderRequest, що перевіряє наявність усіх ключів і кидає InvalidArgumentException при відсутності email чи sku, перш ніж створити об'єкт.
❓ Перевір себе
1. Клас відрізняється від об'єкта тим, що:
Клас — конкретний екземпляр, об'єкт — шаблон
Клас — шаблон/опис, об'єкт — конкретний екземпляр цього шаблону
Це одне й те саме
Клас можна створити тільки один раз
Клас описує структуру, об'єкт (instance) — це реальний 'екземпляр', створений через new.
2. readonly властивість можна змінити:
Скільки завгодно разів
Один раз, зазвичай у конструкторі, потім вона незмінна
Тільки через геттер
Тільки в дочірньому класі
readonly дозволяє присвоїти значення один раз; повторна спроба викличе помилку.
3. Constructor Promotion дозволяє:
Викликати конструктор без new
Оголосити і одразу ініціалізувати властивість прямо в параметрах конструктора
Створити кілька конструкторів
Успадкувати клас без extends
Замість окремого оголошення властивості + $this->prop = $prop, все пишеться в сигнатурі конструктора.
4. Що станеться, якщо оголосити readonly-властивість БЕЗ типу?
Все спрацює як звичайно
Фатальна помилка компіляції — readonly вимагає типу
Властивість стане static
PHP автоматично встановить тип mixed
readonly-властивість обов'язково повинна мати вказаний тип — це офіційне обмеження мови.
// СТАДІЯ 2 · УРОК 2.2

Модифікатори доступу та інкапсуляція

📖 Теорія
🧠

Аналогія:уяви кошик покупок (shopping cart) в інтернет-магазині. Ти не можеш напряму 'залізти рукою' і виправити цифру суми замовлення в базі даних — ти можеш тільки викликати дії, які магазин дозволяє: 'додати товар', 'видалити товар', 'застосувати промокод'. Внутрішня логіка підрахунку суми — це приватна деталь, схована від зовнішнього світу.

Три рівні видимості: public — доступно звідусіль. protected — доступно всередині класу і всіх його нащадків, але не ззовні. private — доступно ТІЛЬКИ всередині того самого класу, де оголошено.

Навіщо взагалі ховати дані: якщо властивість $total кошика публічна, будь-який код ззовні може встановити $cart->total = -500, зламавши бізнес-логіку. Якщо ж total обчислюється приватним методом на основі приватного масиву товарів, зовнішній код фізично не може поставити його в некоректний стан.

Геттери й сеттери: це публічні методи, що дають контрольований доступ до приватних даних. Хороший сеттер не просто присвоює значення, а перевіряє його коректність.

Типова помилка: робити геттери й сеттери для КОЖНОЇ приватної властивості 'про всяк випадок' — це фактично робить приватність марною. Інкапсуляція працює, тільки коли сеттер реально щось перевіряє або обчислює.

📄 Офіційна документація: PHP: Видимість (Visibility) →
💻 Приклад коду
ShoppingCart.php
<?php
declare(strict_types=1);

class ShoppingCart
{
    private array $items = [];

    public function addItem(string $sku, int $quantity): void
    {
        if ($quantity <= 0) {
            throw new InvalidArgumentException('Кількість повинна бути додатньою');
        }
        $this->items[$sku] = ($this->items[$sku] ?? 0) + $quantity;
    }

    public function removeItem(string $sku): void
    {
        unset($this->items[$sku]);
    }

    public function getItemCount(): int
    {
        return array_sum($this->items);
    }

    public function getItems(): array
    {
        return $this->items;
    }
}

$cart = new ShoppingCart();
$cart->addItem('SKU-1001', 2);
$cart->addItem('SKU-2005', 1);
echo $cart->getItemCount();
// $cart->items = []; // Помилка! items -- private
✏️ Завдання
1
Клас ShoppingCart із захищеним станом
Реалізуй ShoppingCart (як у прикладі): private масив items, публічні addItem/removeItem/getItemCount. У addItem кинь InvalidArgumentException, якщо кількість <= 0.
2
Клас Customer з захищеним балансом лояльності закріплення
Створи клас Customer з private властивістю loyaltyPoints. Додай публічні методи addPoints(int $points) і redeemPoints(int $points) — другий повинен кидати виключення, якщо балів не вистачає.
3
Промокод із захищеною логікою знижки поглиблено
Додай до ShoppingCart приватний метод applyPromoCode(string $code), доступний лише зсередини applyDiscount(). Публічний метод applyDiscount() перевіряє код через private-мапу дозволених кодів і застосовує знижку, не даючи зовнішньому коду напряму змінити суму.
❓ Перевір себе
1. private властивість доступна:
Звідусіль
Тільки всередині того самого класу
Тільки в дочірніх класах
Тільки в глобальній області видимості
private обмежує доступ виключно кодом усередині того ж класу, де оголошено властивість.
2. protected відрізняється від private тим, що:
protected взагалі не можна використовувати
protected доступний ще й дочірнім класам
protected доступний всюди в проєкті
protected можна змінити тільки один раз
protected видно і в самому класі, і в усіх класах, що його успадковують.
3. Навіщо потрібна інкапсуляція?
Щоб код виконувався швидше
Щоб приховати внутрішню реалізацію і захистити дані від некоректних змін ззовні
Щоб зменшити кількість файлів
Щоб автоматично генерувати документацію
Інкапсуляція захищає внутрішній стан об'єкта, дозволяючи змінювати його лише через контрольовані методи.
4. Чому геттер/сеттер для КОЖНОЇ private-властивості 'про всяк випадок' — погана практика?
Це заборонено синтаксисом PHP
Це фактично зводить нанівець приватність, якщо сеттер нічого не перевіряє
PHP не дозволяє більше 5 геттерів у класі
Це уповільнює виконання коду в 10 разів
Якщо сеттер просто копіює значення без перевірки, властивість знову можна вільно змінити ззовні.
// СТАДІЯ 2 · УРОК 2.3

Наслідування

📖 Теорія
🧠

Аналогія:у системі сповіщень інтернет-магазину є спільна логіка для будь-якого типу сповіщення (сформувати текст, залогувати відправку), але конкретний спосіб доставки різний: email, SMS чи push. Наслідування дозволяє один раз описати спільне в батьківському класі, а кожен конкретний канал — тільки те, чим він унікальний.

Синтаксис: class EmailNotification extends Notification означає, що EmailNotification автоматично отримує всі public/protected властивості й методи класу Notification. parent::method() дозволяє викликати оригінальну версію методу зсередини перевизначеного методу дочірнього класу.

Перевизначення (override): дочірній клас може оголосити метод з тим самим ім'ям — PHP використає версію дочірнього класу. Сигнатура перевизначеного методу повинна бути сумісною з батьківською, інакше PHP видасть помилку 'Declaration must be compatible'.

Обмеження: PHP підтримує лише одиночне наслідування класів. Якщо потрібна поведінка з кількох незалежних джерел, використовують інтерфейси або трейти.

Типова помилка: будувати занадто глибокі ланцюжки наслідування — це ускладнює розуміння коду. Правило 'composition over inheritance' часто рекомендує композицію замість глибокого extends.

📄 Офіційна документація: PHP: Наслідування →
💻 Приклад коду
Notification.php
<?php
declare(strict_types=1);

abstract class Notification
{
    public function __construct(protected string $recipient, protected string $message) {}

    public function log(): void
    {
        echo "[LOG] Надсилаємо сповіщення до {$this->recipient}\n";
    }

    abstract public function send(): bool;
}

class EmailNotification extends Notification
{
    public function send(): bool
    {
        $this->log();
        echo "Email до {$this->recipient}: {$this->message}\n";
        return true;
    }
}

class SmsNotification extends Notification
{
    public function __construct(string $recipient, string $message, private int $maxLength = 160)
    {
        parent::__construct($recipient, $message);
    }

    public function send(): bool
    {
        parent::log();
        $text = mb_substr($this->message, 0, $this->maxLength);
        echo "SMS до {$this->recipient}: $text\n";
        return true;
    }
}

$email = new EmailNotification('client@shop.com', 'Ваше замовлення #4521 оплачено');
$email->send();
✏️ Завдання
1
Ієрархія способів сповіщень
Створи abstract class Notification (як у прикладі) з методом log() і абстрактним send(). Створи EmailNotification і PushNotification, кожен зі своєю реалізацією send(), яка викликає parent::log() перед відправкою.
2
Базовий і спеціалізований репозиторій закріплення
Створи клас Repository з protected властивістю items (масив) і методом all(): array. Створи ProductRepository extends Repository, який додає findBySku(string $sku): ?array.
3
Ієрархія контролерів API поглиблено
Створи abstract class BaseController з захищеним методом jsonResponse(array $data, int $status = 200) (просто echo json_encode для прикладу). Створи ProductController extends BaseController з методом index(), що викликає $this->jsonResponse(['products' => [...]]).
❓ Перевір себе
1. parent::__construct() використовують, щоб:
Створити новий об'єкт батьківського класу
Викликати конструктор батьківського класу всередині дочірнього
Видалити батьківський клас
Заборонити наслідування
parent::__construct() дозволяє дочірньому класу доручити частину ініціалізації батьківському конструктору.
2. Якщо дочірній клас перевизначає метод батька, і хоче ще й виконати оригінальну логіку, він викликає:
self::method()
static::method()
parent::method()
this::method()
parent::method() викликає версію методу з батьківського класу.
3. Клас у PHP може успадковувати:
Кілька класів одночасно
Тільки один клас (extends одне ім'я)
Жодного класу
Тільки самого себе
PHP підтримує лише одиночне наслідування класів.
4. Чому занадто глибокі ланцюжки наслідування вважаються поганою практикою?
PHP обмежує глибину до 2 рівнів
Це ускладнює розуміння коду -- треба переглянути весь ланцюжок
Це вповільнює виконання в 100 разів
Це неможливо синтаксично
Принцип 'composition over inheritance' саме тому й існує.
// СТАДІЯ 2 · УРОК 2.4

Абстрактні класи та інтерфейси

📖 Теорія
🧠

Аналогія:у backend інтернет-магазину потрібно приймати оплату різними способами, але код, що ОБРОБЛЯЄ замовлення, не повинен знати конкретних деталей кожного платіжного провайдера — йому важливо тільки, що будь-який спосіб оплати вміє 'списати суму'. Інтерфейс — це формальний контракт без деталей реалізації.

Інтерфейс формально: оголошується через interface Name { public function method(): type; } — містить ТІЛЬКИ сигнатури методів. Клас 'підписується' під контракт через implements, і зобов'язаний реалізувати абсолютно всі методи — інакше PHP видасть фатальну помилку ще на етапі оголошення класу. Один клас може реалізовувати кілька інтерфейсів одночасно.

Абстрактний клас формально: оголошується через abstract class Name, може містити і звичайні (реалізовані), і абстрактні методи. Не можна створити об'єкт абстрактного класу напряму.

Коли що обирати: інтерфейс — коли класи логічно не пов'язані спільним походженням, але дотримуються одного контракту. Абстрактний клас — коли є і спільний контракт, і спільна готова логіка.

Типова помилка: плутати мету інтерфейсу з мутою 'просто зробити менше коду'. Якщо потрібна спільна логіка — це абстрактний клас або трейт.

📄 Офіційна документація: PHP: Абстрактні класи та Інтерфейси →
💻 Приклад коду
PaymentGateway.php
<?php
declare(strict_types=1);

interface PaymentGateway
{
    public function charge(float $amount): bool;
}

abstract class BasePaymentGateway implements PaymentGateway
{
    protected function validateAmount(float $amount): void
    {
        if ($amount <= 0) {
            throw new InvalidArgumentException('Сума платежу повинна бути додатньою');
        }
    }

    abstract public function charge(float $amount): bool;
}

class StripeGateway extends BasePaymentGateway
{
    public function charge(float $amount): bool
    {
        $this->validateAmount($amount);
        echo "Stripe: списано $amount грн\n";
        return true;
    }
}

class CashOnDeliveryGateway extends BasePaymentGateway
{
    public function charge(float $amount): bool
    {
        $this->validateAmount($amount);
        echo "Готівка при отриманні: очікується $amount грн\n";
        return true;
    }
}

function processOrder(PaymentGateway $gateway, float $total): void
{
    $gateway->charge($total);
}

processOrder(new StripeGateway(), 1899.00);
// new BasePaymentGateway(); // Помилка! Не можна створити об'єкт абстрактного класу
✏️ Завдання
1
Interface + abstract для платежів
Створи interface PaymentGateway з методом charge(float $amount): bool. Створи abstract class BasePaymentGateway implements PaymentGateway з готовим validateAmount() і абстрактним charge(). Реалізуй StripeGateway і CashOnDeliveryGateway.
2
Інтерфейс для експорту звітів закріплення
Створи interface Exportable з методом toArray(): array. Реалізуй його у двох класах Order і Product. Напиши функцію exportAll(array $items), яка викликає toArray() кожного елемента.
3
Абстрактний Repository з готовою пагінацією поглиблено
Створи interface RepositoryInterface з методом find(int $id): ?array. Створи abstract class BaseRepository implements RepositoryInterface з готовим методом paginate(int $page, int $perPage): array (працює з protected масивом items), і абстрактним find(). Реалізуй ProductRepository.
❓ Перевір себе
1. Можна створити об'єкт абстрактного класу напряму (new AbstractClass())?
Так, завжди
Ні, тільки через дочірній клас, що реалізував усі abstract-методи
Тільки в PHP 8
Тільки якщо клас final
Абстрактний клас — це лише шаблон; створювати об'єкти можна тільки з конкретних нащадків.
2. Головна відмінність інтерфейсу від абстрактного класу:
Інтерфейс не може мати жодної реалізації методів, лише сигнатури
Інтерфейс можна створити через new
Абстрактний клас не може мати властивостей
Інтерфейс успадковується тільки один раз
Інтерфейс — це чистий контракт, абстрактний клас може містити готову логіку поряд з абстрактними методами.
3. Скільки інтерфейсів може реалізовувати один клас?
Тільки один
Кілька одночасно (implements A, B, C)
Жодного
Тільки якщо клас абстрактний
Інтерфейсів можна реалізовувати скільки завгодно.
4. Коли варто обрати інтерфейс замість абстрактного класу?
Коли класи мають спільне походження і спільний код
Коли класи логічно не пов'язані, але повинні дотримуватись одного контракту без спільної реалізації
Коли потрібна швидкість виконання
Коли клас має тільки одну властивість
Інтерфейс саме для непов'язаних класів з однаковим контрактом.
// СТАДІЯ 2 · УРОК 2.5

Трейти (Traits)

📖 Теорія
🧠

Аналогія:у backend-проєкті логування потрібне і класу Order, і класу Product, і класу User — але вони жодним чином не пов'язані спільним батьківським класом. Трейт — це 'домішка' готової поведінки, яку можна підключити в будь-який клас незалежно від його місця в ієрархії наслідування.

Синтаксис: trait Loggable { public function log(string $msg): void {...} }, підключення — class Order { use Loggable; }. Трейт не можна інстанціювати самостійно.

Вирішення конфліктів (офіційний синтаксис php.net): якщо два трейти визначають метод з однаковою назвою, PHP видасть фатальну помилку, доки конфлікт не вирішено явно через insteadof (обрати конкретну версію) і as (дати іншому методу нове ім'я замість втрати доступу до нього).

Трейт vs абстрактний клас: трейт НЕ бере участі в перевірці типів ($order instanceof Loggable поверне false, якщо Loggable — трейт) — це головна відмінність. Трейт — суто механізм повторного використання коду, а не контракт типів.

Типова помилка: зловживати великою кількістю трейтів в одному класі 'про запас'. Розумний ліміт — 1-3 добре названі трейти на клас.

📄 Офіційна документація: PHP: Трейти →
💻 Приклад коду
Loggable.php
<?php
declare(strict_types=1);

trait Loggable
{
    public function log(string $message): void
    {
        echo '[' . static::class . "] $message\n";
    }
}

trait Timestampable
{
    private ?string $createdAt = null;

    public function markCreated(): void
    {
        $this->createdAt = date('Y-m-d H:i:s');
    }

    public function getCreatedAt(): ?string
    {
        return $this->createdAt;
    }
}

class Order
{
    use Loggable, Timestampable;

    public function __construct(public readonly int $id) {}
}

class Product
{
    use Loggable;
}

$order = new Order(4521);
$order->markCreated();
$order->log('Замовлення створено');
echo $order->getCreatedAt();

$product = new Product();
$product->log('Товар додано на склад');
✏️ Завдання
1
Трейт логування для декількох сутностей
Створи trait Loggable (як у прикладі) і 'приклей' його до двох непов'язаних класів: Order і Customer, викликавши log() в обох.
2
Вирішення конфлікту трейтів закріплення
Створи два трейти StripeLoggerTrait і DatabaseLoggerTrait, обидва з методом record(). Підключи їх в одному класі PaymentProcessor через insteadof і as.
3
Трейт валідації запиту API поглиблено
Створи trait ValidatesRequest з методом requireFields(array $data, array $required): void, який кидає InvalidArgumentException, якщо в $data відсутній хоч один ключ з $required. Використай трейт у двох різних контролерах (OrderController, ProductController).
❓ Перевір себе
1. Трейт відрізняється від наслідування тим, що:
Трейт можна 'приклеїти' до будь-якого класу незалежно від ієрархії наслідування
Трейт можна використати тільки в одному класі
Трейт — це те саме, що інтерфейс
Трейт не може містити методи
Трейти не залежать від ланцюжка extends.
2. Що відбувається, якщо два трейти в одному класі мають метод з однаковою назвою?
PHP автоматично обирає перший трейт
Виникає конфлікт, який треба вирішити вручну (insteadof/as)
Другий метод просто ігнорується без помилок
Клас автоматично стає абстрактним
PHP вимагає явно вказати, який саме метод використовувати при конфлікті імен.
3. $obj instanceof SomeTrait поверне:
true, якщо клас використовує трейт
false — трейт не бере участі в перевірці типів
Помилку компіляції
true тільки для абстрактних трейтів
instanceof працює з класами та інтерфейсами, а трейт — лише механізм переносу коду.
// СТАДІЯ 2 · УРОК 2.6

Статичні члени та константи класу

📖 Теорія
🧠

Аналогія:номер замовлення в інтернет-магазині повинен бути унікальним і послідовним для ВСІХ замовлень системи — це не властивість одного замовлення, а спільний лічильник, що належить самому класу Order.

Статичні властивості й методи: оголошуються з static, належать класу в цілому — усі екземпляри 'бачать' одне значення. Доступ — через ім'я класу і ::, без потреби створювати об'єкт.

self:: проти static:: (пізнє статичне зв'язування): self:: завжди звертається до класу, де НАПИСАНИЙ код. static:: звертається до класу, який РЕАЛЬНО викликається під час виконання. Різниця критична, коли дочірній клас перевизначає статичний метод.

Константи класу: public const NAME = value;, доступ — ClassName::NAME. На відміну від static-властивостей, константи не можна змінити після оголошення.

Типова помилка: плутати статичну властивість із 'спільною за замовчуванням, але кожен об'єкт може мати свою'. Static-властивість справді ОДНА на весь клас.

📄 Офіційна документація: PHP: Static Keyword →
💻 Приклад коду
Order.php
<?php
declare(strict_types=1);

class Order
{
    public const MIN_AMOUNT = 50.0;
    public const MAX_AMOUNT = 100000.0;

    private static int $totalCreated = 0;
    private int $orderNumber;

    public function __construct(private readonly float $amount)
    {
        if ($amount < self::MIN_AMOUNT || $amount > self::MAX_AMOUNT) {
            throw new InvalidArgumentException('Сума поза дозволеним діапазоном');
        }
        self::$totalCreated++;
        $this->orderNumber = 4500 + self::$totalCreated;
    }

    public function getOrderNumber(): int
    {
        return $this->orderNumber;
    }

    public static function getTotalCreated(): int
    {
        return self::$totalCreated;
    }
}

$order1 = new Order(1899.00);
$order2 = new Order(350.00);

echo $order1->getOrderNumber();
echo $order2->getOrderNumber();
echo Order::getTotalCreated();
echo Order::MIN_AMOUNT;
✏️ Завдання
1
Лічильник замовлень
Додай до класу Order статичну властивість totalCreated, яка збільшується в конструкторі, і статичний метод getTotalCreated(). Створи 3 замовлення і виведи загальну кількість.
2
Клас Config зі статичним доступом закріплення
Створи клас Config зі статичним приватним масивом налаштувань і статичним методом get(string $key): mixed. Виклич Config::get('tax_rate') без створення об'єкта.
3
Фабрика номерів рахунків (invoice) поглиблено
Створи клас InvoiceNumberGenerator зі статичним методом next(): string, що повертає номер виду 'INV-2026-0001', 'INV-2026-0002' і т.д., рахуючи через static-лічильник — типова задача при генерації унікальних ідентифікаторів документів у backend.
❓ Перевір себе
1. Статична властивість (static) відрізняється від звичайної тим, що:
Вона доступна тільки в дочірніх класах
Вона спільна для всіх об'єктів класу, а не унікальна для кожного
Вона автоматично readonly
Її не можна змінити взагалі
static-властивість належить самому класу, а не окремому об'єкту.
2. Щоб викликати статичний метод класу, використовують:
$object->method()
ClassName::method()
new ClassName()->method()
method::ClassName()
Статичні методи викликаються через ім'я класу і ::.
3. У чому головна відмінність self:: від static::?
self:: працює тільки з константами
self:: завжди звертається до класу де написаний код, static:: -- до реально викликаного класу
Це два синоніми, різниці немає
static:: не можна використовувати в статичних методах
Ця відмінність критична при перевизначенні статичних методів у дочірніх класах.
4. Константу класу (public const) можна змінити після оголошення?
Так, через сеттер
Ні, це справжнє незмінне значення
Так, але тільки в дочірньому класі
Так, через static::
Константи класу незмінні за визначенням.
// СТАДІЯ 2 · УРОК 2.7

Поліморфізм

📖 Теорія
🧠

Аналогія:у backend оплати підключено кілька платіжних провайдерів. Код, що обробляє замовлення, викликає $gateway->charge($amount) для будь-якого з них ОДНАКОВО — не перевіряючи спочатку 'а це часом не Stripe?'. Кожен провайдер сам знає, як саме списати кошти.

Формальне визначення: поліморфізм — здатність різних класів, що реалізують один інтерфейс, мати власну реалізацію одного й того ж методу-контракту, при цьому виклик коду залишається однаковим.

Принцип підстановки Лісков (Liskov Substitution Principle, 'L' у SOLID): дочірній клас повинен можна використати всюди, де очікується базовий тип, не ламаючи коректність програми.

Як це працює технічно: коли пишеш function processOrder(PaymentGateway $gateway), рішення про те, ЯКИЙ саме метод charge() виконати, приймається динамічно під час виконання, на основі реального класу об'єкта.

Типова помилка: писати if ($gateway instanceof StripeGateway) {...} elseif (...) замість того, щоб покластись на поліморфізм. Такий код ламається щоразу, коли додається новий шлюз.

📄 Офіційна документація: PHP: Object Interfaces →
💻 Приклад коду
payments.php
<?php
declare(strict_types=1);

interface PaymentGateway
{
    public function charge(float $amount): bool;
}

class StripeGateway implements PaymentGateway
{
    public function charge(float $amount): bool
    {
        echo "Stripe: списано $amount грн через картку\n";
        return true;
    }
}

class PayPalGateway implements PaymentGateway
{
    public function charge(float $amount): bool
    {
        echo "PayPal: перевід $amount грн\n";
        return true;
    }
}

class CashOnDeliveryGateway implements PaymentGateway
{
    public function charge(float $amount): bool
    {
        echo "Готівка: кур'єр отримає $amount грн\n";
        return true;
    }
}

function processOrder(PaymentGateway $gateway, float $total): void
{
    $gateway->charge($total);
}

$gateways = [new StripeGateway(), new PayPalGateway(), new CashOnDeliveryGateway()];
foreach ($gateways as $gateway) {
    processOrder($gateway, 1899.00);
}
✏️ Завдання
1
Поліморфна обробка платежів
Створи interface PaymentGateway з методом charge(float $amount): bool, реалізуй його в 3 класах з різним текстом виводу. Напиши функцію processOrder(PaymentGateway $gateway, float $total).
2
Поліморфні експортери звітів закріплення
Створи interface Reportable з методом toReport(): string. Реалізуй його у Order і Product. Напиши функцію printReports(array $items), яка виводить toReport() без жодного if/instanceof.
3
Кілька способів доставки, один інтерфейс поглиблено
Створи interface ShippingMethod з методом calculateCost(float $weight): float. Реалізуй StandardShipping, ExpressShipping, PickupShipping (0 грн). Напиши функцію checkout(ShippingMethod $method, float $weight), яка не знає конкретного типу.
❓ Перевір себе
1. Поліморфізм дозволяє:
Створювати кілька об'єктів одного класу
Різним класам мати власну реалізацію одного й того ж методу-контракту
Автоматично видаляти невикористаний код
Прискорити виконання foreach
Однакова 'команда' дає різний результат залежно від конкретного класу об'єкта.
2. Що дозволяє написати foreach ($gateways as $g) { $g->charge($x); } без перевірки типу кожного $g?
Магічні методи
Поліморфізм через спільний інтерфейс
Статичні властивості
Трейти
Якщо всі об'єкти реалізують один інтерфейс, код може викликати метод однаково для всіх типів.
3. Принцип підстановки Лісков (LSP) каже:
Дочірній клас повинен мати більше методів, ніж батьківський
Дочірній/реалізацію можна підставити замість базового типу без порушення коректності програми
Клас не може мати більше 5 методів
Інтерфейси не можна успадковувати
LSP -- одна з літер SOLID, гарантує 'безпечність' поліморфізму.
4. Написання if ($gateway instanceof StripeGateway) {...} elseif замість поліморфного виклику вважається:
Найкращою практикою завжди
Антипатерном -- код 'знає забагато' і ламається при додаванні нових типів
Обов'язковим у строгому режимі PHP
Швидшим за виклик методу напряму
Новий платіжний шлюз не повинен вимагати правок у processOrder().
// СТАДІЯ 2 · УРОК 2.8

Магічні методи

📖 Теорія
🧠

Аналогія:магічні методи — це 'гачки' (hooks), які PHP автоматично викликає в певних ситуаціях. Наприклад, коли backend логує об'єкт Money (сума + валюта) у файл, зручно, щоб echo $money; сам сформував рядок '1899.00 UAH'.

Основні магічні методи:

  • __construct() / __destruct() — при створенні/знищенні об'єкта.
  • __toString(): string — коли об'єкт намагаються використати як рядок.
  • __get($name) / __set($name, $value) — при зверненні до неіснуючої властивості ззовні.
  • __call($name, $args) / __callStatic() — при виклику неіснуючого методу.
  • __invoke(...$args) — дозволяє викликати об'єкт як функцію.
  • __clone() — при clone $obj.
  • __isset() / __unset() — для isset()/unset() на недоступних властивостях.

Коли НЕ варто використовувати магічні методи: __get/__set роблять код важчим для аналізу IDE і складнішим для дебагу. У сучасному backend-коді їх використовують точково (наприклад в ORM), а не як основний спосіб роботи з властивостями.

📄 Офіційна документація: PHP: Магічні методи →
💻 Приклад коду
Money.php
<?php
declare(strict_types=1);

final class Money
{
    public function __construct(private readonly int $cents, private readonly string $currency = 'UAH') {}

    public function __toString(): string
    {
        return number_format($this->cents / 100, 2) . ' ' . $this->currency;
    }

    public function __invoke(int $addCents): self
    {
        return new self($this->cents + $addCents, $this->currency);
    }
}

$price = new Money(189900);
echo $price;

$withShipping = $price(5000);
echo $withShipping;

function logOrder(Money $total): void
{
    error_log("Сума замовлення: $total");
}
✏️ Завдання
1
__toString для об'єкта Money
Створи клас Money (як у прикладі) з приватним cents і __toString(), що форматує суму у вигляді '1899.00 UAH'. Перевір, що echo $money; працює без явного виклику методів.
2
__invoke для калькулятора знижок закріплення
Створи клас DiscountCalculator з readonly-властивістю percent і методом __invoke(float $price): float. Створи $tenPercentOff = new DiscountCalculator(10), і виклич $tenPercentOff(1899.00).
3
__toString для логування моделі API поглиблено
Додай __toString() до класу Order, щоб error_log($order); одразу виводило рядок виду 'Order#4521 (1899.00 UAH, paid)' — саме так реально логують об'єкти в продакшн backend-коді.
❓ Перевір себе
1. __toString() викликається автоматично, коли:
Об'єкт створюється
Об'єкт намагаються використати як рядок (наприклад echo)
Об'єкт видаляється
Викликається будь-який метод
PHP автоматично звертається до __toString, коли треба перетворити об'єкт на текст.
2. __invoke() дозволяє:
Клонувати об'єкт
Використовувати об'єкт так, ніби він функція: $obj()
Автоматично видаляти об'єкт
Порівнювати два об'єкти
Якщо клас має __invoke, об'єкт можна викликати круглими дужками.
3. __destruct() викликається:
При кожному виклику методу
Коли об'єкт знищується / завершується скрипт
При зверненні до неіснуючої властивості
При клонуванні об'єкта
__destruct спрацьовує, коли об'єкт більше не потрібен.
4. Чому __get/__set вважають ризикованими для 'звичайних' власних класів?
Вони заборонені у strict_types=1
Вони ускладнюють автодоповнення IDE і дебаг
Вони працюють тільки в PHP 7
Вони вимагають окремого трейту
IDE не бачить 'віртуальних' властивостей, тому магію варто застосовувати точково.
// СТАДІЯ 2 · УРОК 2.9

SPL інтерфейси: Countable, ArrayAccess, IteratorAggregate

📖 Теорія
🧠

Аналогія:результат запиту 'знайти всі замовлення клієнта' в реальному backend часто повертають не голий масив, а спеціальний клас-колекцію — так можна одразу дати колекції власні методи (totalAmount(), filterPaid()), при цьому ЗОВНІ вона поводиться так само зручно, як звичайний масив.

Countable (офіційна сигнатура): один метод count(): int.

ArrayAccess (офіційні сигнатури, php.net): чотири методи: offsetExists(mixed $offset): bool, offsetGet(mixed $offset): mixed, offsetSet(mixed $offset, mixed $value): void, offsetUnset(mixed $offset): void.

IteratorAggregate: один метод getIterator(): Traversable, найчастіше повертає ArrayIterator.

Готові класи SPL 'з коробки': ArrayObject вже реалізує всі три інтерфейси одразу, SplStack/SplQueue дають стек/чергу, SplObjectStorage — 'мапу' з об'єктами як ключами.

📄 Офіційна документація: PHP: SPL — ArrayAccess / Countable / IteratorAggregate →
💻 Приклад коду
OrderCollection.php
<?php
declare(strict_types=1);

class OrderCollection implements Countable, ArrayAccess, IteratorAggregate
{
    private array $orders = [];

    public function count(): int
    {
        return count($this->orders);
    }

    public function offsetExists(mixed $offset): bool
    {
        return isset($this->orders[$offset]);
    }
    public function offsetGet(mixed $offset): mixed
    {
        return $this->orders[$offset] ?? null;
    }
    public function offsetSet(mixed $offset, mixed $value): void
    {
        if ($offset === null) {
            $this->orders[] = $value;
        } else {
            $this->orders[$offset] = $value;
        }
    }
    public function offsetUnset(mixed $offset): void
    {
        unset($this->orders[$offset]);
    }

    public function getIterator(): Iterator
    {
        return new ArrayIterator($this->orders);
    }
}

$orders = new OrderCollection();
$orders[] = 1899.00;
$orders[] = 350.00;

echo count($orders);
foreach ($orders as $order) {
    echo $order . "\n";
}
✏️ Завдання
1
Клас OrderCollection (базове завдання)
Реалізуй OrderCollection, що реалізує Countable, ArrayAccess та IteratorAggregate, як у прикладі. Додай метод totalAmount(), що рахує суму всіх замовлень у колекції.
2
ProductCatalog з фільтрацією закріплення
Створи власний клас ProductCatalog, що теж реалізує ці три інтерфейси. Додай метод filterInStock(), який повертає НОВИЙ ProductCatalog тільки з товарами в наявності.
3
Пагінація колекції результатів поглиблено
Додай до OrderCollection метод paginate(int $page, int $perPage): OrderCollection, що повертає новий об'єкт лише з потрібним 'зрізом' замовлень — типова задача для API-ендпоінту зі списком (GET /orders?page=2).
❓ Перевір себе
1. Щоб count($obj) працював на власному класі, треба реалізувати:
ArrayAccess
Countable
IteratorAggregate
Iterator
Countable вимагає лише один метод count(): int.
2. Скільки методів треба реалізувати для ArrayAccess?
1
2
4
6
offsetExists, offsetGet, offsetSet, offsetUnset — усі чотири обов'язкові.
3. getIterator() у IteratorAggregate повинен повертати:
Масив
Рядок
Щось, що реалізує Traversable (наприклад ArrayIterator)
Boolean
IteratorAggregate делегує ітерацію об'єкту типу Traversable.
4. $obj[] = 'value'; на об'єкті з ArrayAccess викликає:
offsetGet з offset=null
offsetSet з offset=null
offsetExists
offsetUnset
Порожні квадратні дужки [] означають offset=null у offsetSet.
5. Який готовий SPL-клас вже реалізує Countable, ArrayAccess та IteratorAggregate одночасно?
SplStack
ArrayObject
SplQueue
SplFileObject
ArrayObject із SPL вже реалізує всі три інтерфейси.
// СТАДІЯ 2 · УРОК 2.10

Enum та фінальні класи

📖 Теорія
🧠

Аналогія:статус замовлення в інтернет-магазині має чітко обмежений набір варіантів: 'очікує оплати', 'оплачено', 'відправлено', 'скасовано'. Якщо зберігати статус як звичайний рядок, ніщо не завадить одруківці. Enum (з PHP 8.1) робить такі одруківки неможливими на рівні мови.

Pure enum vs backed enum: 'чистий' enum не має скалярного значення. 'Backed' enum (enum Status: string) прив'язує кожному case конкретне значення, доступне через ->value — потрібно для збереження статусу в базу даних чи JSON.

Обмеження enum: enum МОЖЕ мати методи, константи і реалізовувати інтерфейси, але НЕ може мати звичайні мутабельні властивості.

final клас/метод: final class забороняє успадкування взагалі; final public function забороняє перевизначення конкретного методу.

PHP 8.2 -- readonly class: якщо ВСІ властивості класу readonly, можна позначити readonly одразу весь клас.

📄 Офіційна документація: PHP: Enumerations (Enum) →
💻 Приклад коду
OrderStatus.php
<?php
declare(strict_types=1);

enum OrderStatus: string
{
    case Pending = 'pending';
    case Paid = 'paid';
    case Shipped = 'shipped';
    case Cancelled = 'cancelled';

    public function canBeCancelled(): bool
    {
        return match($this) {
            self::Pending, self::Paid => true,
            self::Shipped, self::Cancelled => false,
        };
    }

    public function label(): string
    {
        return match($this) {
            self::Pending => 'Очікує оплати',
            self::Paid => 'Оплачено',
            self::Shipped => 'Відправлено',
            self::Cancelled => 'Скасовано',
        };
    }
}

final readonly class Invoice
{
    public function __construct(
        public int $orderId,
        public float $amount,
        public OrderStatus $status,
    ) {}
}

$invoice = new Invoice(4521, 1899.00, OrderStatus::Paid);
echo $invoice->status->value;
echo $invoice->status->label();
var_dump($invoice->status->canBeCancelled());
✏️ Завдання
1
Enum статусу замовлення
Створи enum OrderStatus: string з варіантами Pending, Paid, Shipped, Cancelled. Додай canBeCancelled(): bool і label(): string з людськими назвами українською.
2
final readonly клас Invoice закріплення
Створи final readonly class Invoice з властивостями orderId, amount, status. Спробуй успадкувати цей клас і переконайся, що PHP видасть помилку через final.
3
Enum ролі користувача API поглиблено
Створи enum UserRole: string з варіантами Customer, Manager, Admin. Додай метод canDeleteOrders(): bool (тільки Manager і Admin) — типова перевірка прав доступу перед виконанням дії в API.
❓ Перевір себе
1. Головна перевага enum перед звичайним рядком для статусу:
Enum працює швидше в 10 разів
Enum обмежує значення тільки реально існуючими варіантами, помилки ловляться на етапі типів
Enum автоматично зберігається в базу даних
Enum можна використовувати тільки в масивах
Enum не дозволить передати неіснуючий варіант.
2. final клас означає:
Клас, що не може мати методів
Клас, який не можна успадкувати
Клас, який можна створити тільки один раз
Клас без властивостей
final забороняє створювати дочірні класи від цього класу.
3. enum OrderStatus: string — це:
Звичайний клас
backed enum (кожен case має своє строкове значення через ->value)
Трейт
Інтерфейс
backed enum прив'язує кожному case конкретне скалярне значення.
4. Чи може enum мати звичайну мутабельну властивість?
Так, без обмежень
Ні -- enum може мати лише cases, методи й константи
Так, але тільки static
Так, тільки protected
Enum описує фіксований набір значень.