Learn/flight_vs_laravel
Flight vs Laravel
Що таке Laravel?
Laravel — це повнофункціональний фреймворк, який має всі дзвіночки та свистки та чудову екосистему, орієнтовану на розробників, але за ціною в продуктивності та складності. Мета Laravel — забезпечити розробнику найвищий рівень продуктивності та полегшити виконання звичайних завдань. Laravel — чудовий вибір для розробників, які прагнуть побудувати повнофункціональний корпоративний веб-додаток. Це супроводжується певними компромісами, зокрема в плані продуктивності та складності. Вивчення основ Laravel може бути легким, але досягнення майстерності у фреймворку може зайняти деякий час.
Існує також так багато модулів Laravel, що розробники часто відчувають, ніби єдиний спосіб вирішити проблеми — це через ці модулі, тоді як насправді ви могли б просто використати іншу бібліотеку або написати власний код.
Переваги порівняно з Flight
- Laravel має величезну екосистему розробників і модулів, які можна використовувати для вирішення звичайних проблем.
- Laravel має повнофункціональний ORM, який можна використовувати для взаємодії з вашою базою даних.
- Laravel має божевільну кількість документації та навчальних матеріалів, які можна використовувати для вивчення фреймворку. Це може бути добре для занурення в деталі або погано, бо є стільки всього, через що потрібно пройти.
- Laravel має вбудовану систему аутентифікації, яку можна використовувати для забезпечення безпеки вашого додатка.
- Laravel має подкасти, конференції, зустрічі, відео та інші ресурси, які можна використовувати для вивчення фреймворку.
- Laravel орієнтований на досвідченого розробника, який прагне побудувати повнофункціональний корпоративний веб-додаток.
Недоліки порівняно з Flight
- Laravel має набагато більше процесів під капотом, ніж Flight. Це відбувається за драматичної ціни в плані продуктивності. Дивіться бенчмарки TechEmpower для отримання додаткової інформації.
- Flight орієнтований на розробника, який прагне побудувати легкий, швидкий і простий у використанні веб-додаток.
- Flight орієнтований на простоту та легкість використання.
- Одна з ключових особливостей Flight полягає в тому, що він робить усе можливе для збереження зворотної сумісності. Laravel викликає велике роздратування між основними версіями.
- Flight призначений для розробників, які вперше занурюються в світ фреймворків.
- Flight не має залежностей, тоді як Laravel має жахливу кількість залежностей
- Flight також може створювати корпоративні додатки, але він не має стільки шаблонного коду, як Laravel. Він також вимагатиме більшої дисципліни з боку розробника, щоб тримати все організованим і добре структурованим.
- Flight надає розробнику більше контролю над додатком, тоді як Laravel має купу магії за лаштунками, яка може бути роздратовуючою.
Learn/migrating_to_v3
Міграція до v3
Зворотна сумісність у більшості випадків збережена, але є деякі зміни, про які ви повинні знати під час міграції з v2 до v3. Є деякі зміни, які надто суперечили шаблонам проєктування, тому довелося внести корективи.
Поведінка буферизації виводу
v3.5.0
Буферизація виводу — це процес, коли вивід, згенерований PHP-скриптом, зберігається в буфері (внутрішньому для PHP) перед відправкою клієнту. Це дозволяє модифікувати вивід перед його відправкою клієнту.
У MVC-додатку Контролер є "менеджером" і керує тим, що робить подання. Генерація виводу поза контролером (або в випадку Flight іноді анонімною функцією) порушує шаблон MVC. Ця зміна спрямована на більшу відповідність шаблону MVC і робить фреймворк більш передбачуваним та легшим у використанні.
У v2 буферизація виводу оброблялася таким чином, що вона не послідовно закривала власний буфер виводу, що ускладнювало юніт-тестування та стримінг. Для більшості користувачів ця зміна може не вплинути на вас. Однак, якщо ви виводите вміст поза викликами функцій та контролерами (наприклад, у хуку), ви, ймовірно, зіткнетеся з проблемами. Виведення вмісту в хуках та перед фактичним виконанням фреймворку могло працювати раніше, але надалі не працюватиме.
Де ви можете зіткнутися з проблемами
// index.php
require 'vendor/autoload.php';
// just an example
define('START_TIME', microtime(true));
function hello() {
echo 'Hello World';
}
Flight::map('hello', 'hello');
Flight::after('hello', function(){
// this will actually be fine
echo '<p>This Hello World phrase was brought to you by the letter "H"</p>';
});
Flight::before('start', function(){
// things like this will cause an error
echo '<html><head><title>My Page</title></head><body>';
});
Flight::route('/', function(){
// this is actually just fine
echo 'Hello World';
// This should be just fine as well
Flight::hello();
});
Flight::after('start', function(){
// this will cause an error
echo '<div>Your page loaded in '.(microtime(true) - START_TIME).' seconds</div></body></html>';
});
Увімкнення поведінки рендерингу v2
Чи можете ви залишити старий код як є без переписування для сумісності з v3? Так, можете! Ви можете увімкнути поведінку рендерингу v2, встановивши опцію конфігурації flight.v2.output_buffering на true. Це дозволить вам продовжувати використовувати стару поведінку рендерингу, але рекомендується виправити це надалі. У v4 фреймворку це буде видалено.
// index.php
require 'vendor/autoload.php';
Flight::set('flight.v2.output_buffering', true);
Flight::before('start', function(){
// Now this will be just fine
echo '<html><head><title>My Page</title></head><body>';
});
// more code
Зміни в Dispatcher
v3.7.0
Якщо ви безпосередньо викликали статичні методи для Dispatcher, такі як Dispatcher::invokeMethod(), Dispatcher::execute() тощо, вам потрібно оновити код, щоб не викликати ці методи безпосередньо. Dispatcher було перетворено на більш об'єктно-орієнтований, щоб полегшити використання контейнерів ін'єкції залежностей. Якщо вам потрібно викликати метод подібно до того, як це робив Dispatcher, ви можете вручну використовувати щось на кшталт $result = $class->$method(...$params); або call_user_func_array().
Зміни в halt() stop() redirect() та error()
v3.10.0
Поведінка за замовчуванням до 3.10.0 полягала в очищенні як заголовків, так і тіла відповіді. Це було змінено на очищення лише тіла відповіді. Якщо вам потрібно також очистити заголовки, ви можете використовувати Flight::response()->clear().
Learn/configuration
Конфігурація
Огляд
Flight надає простий спосіб налаштування різних аспектів фреймворку відповідно до потреб вашого застосунку. Деякі з них встановлені за замовчуванням, але ви можете перевизначити їх за потреби. Ви також можете встановити власні змінні для використання у всьому застосунку.
Чітка, багаторівнева конфігурація (файлові значення за замовчуванням + секрети середовища) також допомагає інструментам ШІ: агенти вивчають одне місце для літералів і одне місце для секретів, замість того щоб вигадувати читання $_ENV у контролерах.
Розуміння
Ви можете налаштовувати певні поведінки Flight, встановлюючи значення конфігурації через метод set.
Flight::set('flight.log_errors', true);
У структурованому застосунку (включно з skeleton) ви зазвичай завантажуєте налаштування проєкту з app/config/config.php, а потім застосовуєте відповідні ключі до Engine (наприклад, flight.base_url, flight.views.path). Ви також можете впроваджувати невеликий об'єкт конфігурації у контролери замість читання глобальних змінних усюди — це зручніше для тестів та для агентів, що дотримуються AGENTS.md.
Базове використання
Параметри конфігурації Flight
Нижче наведено перелік усіх доступних параметрів конфігурації:
- flight.base_url
?string- Перевизначити базову URL-адресу запиту, якщо Flight працює у підкаталозі. (за замовчуванням: null) - flight.case_sensitive
bool- Чутливе до регістру зіставлення URL-адрес. (за замовчуванням: false) - flight.handle_errors
bool- Дозволити Flight обробляти всі помилки внутрішньо. (за замовчуванням: true) - flight.log_errors
bool- Журналювати помилки у файл журналу помилок веб-сервера. (за замовчуванням: false)- Якщо у вас встановлено Tracy, Tracy буде журналювати помилки на основі конфігурацій Tracy, а не цієї конфігурації.
- flight.debug
bool- Виводити детальну інформацію про помилку (повідомлення винятку, код і стек викликів) у браузері, коли виникає помилка. (за замовчуванням: false)- Ніколи не вмикайте це у продакшені — це розкриває внутрішні деталі застосунку. Використовуйте лише для локальної розробки або стейджингу.
- Коли значення
false, замість цього показується загальна помилка500 Internal Server Error. Поєднуйте зflight.log_errors, щоб перехоплювати помилки на стороні сервера.
- flight.allow_method_override
bool- Дозволити перевизначення HTTP-методу через заголовок запитуX-HTTP-Method-Overrideабо поле_methodу тілі POST-запиту. (за замовчуванням: true)- Рекомендується встановити
falseдля застосунків, які не потребують підміни методу на основі HTML-форм, оскільки це запобігає підробці клієнтамиDELETEабоPUTзапитів через стандартну POST-форму. - Дивіться Security для отримання додаткових деталей.
- Рекомендується встановити
- flight.views.path
string- Каталог, що містить файли шаблонів подань. (за замовчуванням: ./views) - flight.views.extension
string- Розширення файлу шаблону подання. (за замовчуванням:.php; офіційний skeleton встановлює.twigпід час використання Twig) - flight.content_length
bool- Встановлювати заголовокContent-Length. (за замовчуванням: true)- Якщо ви використовуєте Tracy, це потрібно встановити false, щоб Tracy міг правильно відображатися.
- flight.v2.output_buffering
bool- Використовувати застаріле виведення буферизації. Дивіться перехід на v3. (за замовчуванням: false)
Конфігурація завантажувача
Існує також додатковий параметр конфігурації для завантажувача. Він дозволяє автоматично завантажувати класи з _ у назві класу.
// Увімкнути завантаження класів із підкресленнями
// За замовчуванням true
Loader::$v2ClassLoading = false;
Пам'ятайте, що автозавантаження також залежить від регістру папок, що відповідає вашим просторам імен — особливо зі структурою skeleton'а App\ + app/Controller/.
Конфігурація проєкту та .env (шаблон skeleton)
Ядро Flight не вимагає файлів .env. Багато застосунків використовують лише PHP-масив конфігурації. Офіційний skeleton розшаровує конфігурацію, щоб секрети залишалися поза git, а Runway міг безпечно перезаписувати літеральну конфігурацію:
.env/ реальне середовище — секрети та перевизначення для розгортання (ігнорується git).app/config/config.php— літеральні PHP-масиви за замовчуванням (копіюється зconfig_sample.php). Бажано не використовувати вирази$_ENV[...]у цьому файлі: такі інструменти, якrunway config:set, можуть перезаписати його статичними значеннями та вбудувати секрети у файл.- Об'єднання під час завантаження — env має пріоритет для зіставлених ключів; код застосунку читає об'єкт конфігурації або
$app->get(), а не$_ENVу контролерах.
Приклад структури config_sample.php / config.php (спрощено):
<?php
// Лише літерали — секрети належать до .env для робочого процесу skeleton
return [
'app' => [
'env' => 'development',
'debug' => true,
'base_url' => '/',
'timezone' => 'UTC',
],
'database' => [
'driver' => 'sqlite', // або mysql, або '' щоб вимкнути
'host' => 'localhost',
'dbname' => '',
'user' => '',
'password' => '',
'file_path' => __DIR__ . '/../../database.sqlite',
],
// ...
];
# .env.example → .env (skeleton)
APP_ENV=development
APP_DEBUG=true
FLIGHT_BASE_URL=/
DB_DRIVER=sqlite
# DB_PASSWORD=...
Цей поділ є навмисним для AI-дружніх проєктів: інструкції можуть говорити “значення за замовчуванням у config.php, секрети в .env, впроваджуйте Config / Engine — ніколи не вигадуйте доступ до env у контролері.” Існуючі застосунки можуть повністю ігнорувати .env і зберігати єдиний файл конфігурації.
Змінні
Flight дозволяє зберігати змінні, щоб їх можна було використовувати будь-де у вашому застосунку.
// Збережіть вашу змінну
Flight::set('id', 123);
// Деінде у вашому застосунку
$id = Flight::get('id');
Щоб перевірити, чи змінну встановлено, ви можете:
if (Flight::has('id')) {
// Зробіть щось
}
Ви можете очистити змінну:
// Очищує змінну id
Flight::clear('id');
// Очищує всі змінні
Flight::clear();
Примітка: Те, що ви можете встановити змінну, не означає, що ви повинні це робити. Використовуйте цю функцію помірно. Причина в тому, що все, що зберігається тут, стає глобальною змінною. Глобальні змінні є поганими, оскільки їх можна змінити з будь-якого місця у вашому застосунку, що ускладнює відстеження помилок. Крім того, це може ускладнити такі речі, як модульне тестування. Віддавайте перевагу впровадженню через конструктор (як у skeleton + налаштуванні Dice) для сервісів і конфігурації, які потрібні контролерам.
Помилки та винятки
Всі помилки та винятки перехоплюються Flight і передаються методу error, якщо flight.handle_errors встановлено true.
Стандартна поведінка — надсилати загальну відповідь HTTP 500 Internal Server Error із деякою інформацією про помилку.
Ви можете перевизначити цю поведінку відповідно до своїх потреб:
Flight::map('error', function (Throwable $error) {
// Обробка помилки
echo $error->getTraceAsString();
});
За замовчуванням помилки не журналюються на веб-сервері. Ви можете увімкнути це, змінивши конфігурацію:
Flight::set('flight.log_errors', true);
404 Не знайдено
Коли URL-адресу не знайдено, Flight викликає метод notFound. Стандартна поведінка — надсилати відповідь HTTP 404 Not Found із простим повідомленням.
Ви можете перевизначити цю поведінку відповідно до своїх потреб:
Flight::map('notFound', function () {
// Обробка випадку, коли не знайдено
});
Дивіться також
- Встановлення - Конфігурація skeleton,
.envі структура завантаження. - Автозавантаження - Простори імен і регістр папок.
- Розширення Flight - Як розширювати та налаштовувати основну функціональність Flight.
- Модульне тестування - Як писати модульні тести для вашого застосунку Flight.
- AI та досвід розробника -
AGENTS.mdі послідовні інструкції проєкту. - Tracy - Плагін для розширеної обробки помилок і налагодження.
- Розширення Tracy - Розширення для інтеграції Tracy з Flight.
- APM - Плагін для моніторингу продуктивності застосунку та відстеження помилок.
- Безпека - Прапорці посилення захисту та обробка секретів.
Усунення неполадок
- Якщо у вас виникають проблеми з визначенням усіх значень вашої конфігурації, ви можете виконати
var_dump(Flight::get()); - Якщо Runway або інструменти розгортання перезаписали
config.php, переконайтеся, що секрети не було закомічено — тримайте їх у.envабо реальному середовищі, якщо використовуєте шаблон skeleton.
Журнал змін
- Документація – Описано конфігурацію у стилі skeleton / розшарування
.envі розширення подань Twig за замовчуванням для нових проєктів. - v3.18.1 - Додано параметри конфігурації
flight.debugіflight.allow_method_override. - v3.5.0 - Додано конфігурацію для
flight.v2.output_bufferingдля підтримки застарілої поведінки буферизації виведення. - v2.0 - Додано основні конфігурації.
Learn/ai
AI та досвід розробника з Flight
Огляд
Flight створено для роботи з інструментами AI-кодування, а не проти них. Невеликий, передбачуваний API, чітка структура застосунку в офіційному скелеті та інструкційні файли, специфічні для проєкту, означають, що асистенти, як-от GitHub Copilot, Cursor, Windsurf, Claude Code і Gemini, можуть дотримуватися тих самих шаблонів, які ви написали б вручну.
Завдяки вбудованим командам Runway для підключення до LLM-провайдерів і генерації інструкцій проєкту, Flight допомагає вам і вашій команді отримувати послідовну та релевантну допомогу без повторного вставлення одного й того ж контексту в кожний чат.
Розуміння
AI-асистенти для написання коду є найкориснішими, коли вони розуміють контекст вашого проєкту, його угоди та цілі. AI-помічники Flight дозволяють вам:
- Підключити ваш проєкт до популярних LLM-провайдерів (OpenAI, Grok, Claude тощо).
- Генерувати та оновлювати інструкції, специфічні для проєкту, щоб усі отримували однакові настанови.
- Підтримувати рукописний і AI-згенерований код в одній структурі (особливо зі скелетом).
Ці функції постачаються з ядром CLI Flight (через Runway) і попередньо налаштовані в офіційному стартовому проєкті flightphp/skeleton.
Що скелет постачає для AI
Офіційний стартовий проєкт розглядає AGENTS.md як джерело істини для AI-інструментів:
| Файл | Роль |
|---|---|
AGENTS.md (корінь проєкту) |
Глобальні правила, послідовність завантаження, простори імен, DI, «чого не робити» |
Локальний AGENTS.md у app/, migrations/, tests/ тощо |
Легкі поради, специфічні для папки, коли ви працюєте в цьому дереві |
SECURITY.md |
Секрети, заголовки, XSS/SQL, звітування — безпека залишається свідомою та окремою |
У скелеті немає окремого файлу внутрішнього стилю для Copilot / Cursor / Gemini / Windsurf. Спрямуйте свого асистента на кореневий AGENTS.md (і дозвольте йому переходити за посиланнями на файли з меншими областями дії). Люди можуть повністю ігнорувати ці файли та користуватися README; структура однакова в обох випадках.
Документація навчає API; скелет навчає структурі. Короткі приклади
Flight::у цій документації чудово підходять для навчання. У застосунку на основі скелета віддавайте перевагу класамApp\…, ін'єкції через конструктор і$this->appнад статичним фасадом у контролерах. Дивіться Встановлення та Автозавантаження.
Базове використання
Налаштування облікових даних LLM
Команда ai:init проведе вас через процес підключення вашого проєкту до LLM-провайдера.
php runway ai:init
Вам буде запропоновано:
- Вибрати вашого провайдера (OpenAI, Grok, Claude тощо).
- Ввести ваш API-ключ.
- Встановити базову URL-адресу та назву моделі.
Це створює облікові дані, які використовуються для подальших LLM-запитів (наприклад, для генерації інструкцій).
Приклад:
Welcome to AI Init!
Which LLM API do you want to use? [1] openai, [2] grok, [3] claude: 1
Enter the base URL for the LLM API [https://api.openai.com]:
Enter your API key for openai: sk-...
Enter the model name you want to use (e.g. gpt-4, claude-3-opus, etc) [gpt-4o]:
Credentials saved to .runway-creds.json
Генерація AI-інструкцій, специфічних для проєкту
Команда ai:generate-instructions створює або оновлює інструкції для AI-асистентів кодування, адаптовані під ваш проєкт.
php runway ai:generate-instructions
Ви відповісте на кілька запитань (опис, база даних, шаблонізація, безпека, розмір команди тощо). Flight використовує вашого LLM-провайдера для генерації інструкцій і записує їх переважно до:
AGENTS.mdу корені проєкту (незалежний від інструментів; саме цього очікують офіційний скелет і більшість сучасних агентів)
Залежно від версії CLI та параметрів, команда може також записувати копії, специфічні для інструментів, для старіших робочих процесів (наприклад, файли правил Copilot, Cursor, Windsurf або Gemini). Для нових проєктів на основі скелета сприймайте AGENTS.md (а також будь-які локальні файли AGENTS.md, які ви зберігаєте в app/) як єдине джерело істини — не підтримуйте вручну п'ять розбіжних інструкційних файлів.
Приклад:
Please describe what your project is for? My awesome API
What database are you planning on using? MySQL
What HTML templating engine will you plan on using (if any)? twig
Is security an important element of this project? (y/n) y
...
AI instructions updated successfully.
Тепер AI-інструменти можуть пропонувати код, який відповідає вашому реальному стеку та структурі, а не загальному підручнику з PHP.
Поглиблене використання
- Налаштовуйте облікові дані або шляхи виводу за допомогою параметрів команд (дивіться
--helpдля кожної команди). - Помічники працюють з будь-яким LLM-провайдером, який підтримує API, сумісний з OpenAI.
- Повторно запускайте
ai:generate-instructionsу міру розвитку проєкту, щоб агенти залишалися синхронізованими. - У скелеті тримайте політику безпеки у
SECURITY.md, а структуру коду — вAGENTS.md, щоб жоден документ не перетворився на збірку всього підряд. - Віддавайте перевагу docs.flightphp.com і серверу Flight MCP, коли агентам потрібні деталі API; перевіряйте вигадані методи на відповідність
vendor/flightphp/core.
Дивіться також
- Flight Skeleton – офіційний стартовий проєкт із
AGENTS.md, Twig, SimplePdo та Dice, налаштований для AI-дружньої структури - Встановлення – рекомендована структура
create-project - Автозавантаження – регістр папок відповідає просторам імен (
App\Controller↔app/Controller/) - CLI Runway – CLI, що забезпечує роботу команд
ai:*і генерації каркасу - Безпека – безпечні налаштування за замовчуванням, які агенти (і люди) не повинні послаблювати
Усунення неполадок
- Якщо ви бачите «Missing .runway-creds.json», спершу запустіть
php runway ai:init. - Переконайтеся, що ваш API-ключ дійсний і має доступ до вибраної моделі.
- Якщо інструкції не оновлюються, перевірте права доступу до файлів у каталозі вашого проєкту.
- Якщо агент вигадує API Flight або неправильну структуру папок, спрямуйте його на кореневий
AGENTS.mdі цей сайт документації; структура скелета є визначальною для коду вapp/.
Журнал змін
- v3.18.4 –
ai:generate-instructionsзаписує інструкції проєкту доAGENTS.mdу корені проєкту. - v3.16.0 – Додано CLI-команди
ai:initтаai:generate-instructionsдля інтеграції з AI.
Learn/unit_testing_and_solid_principles
Ця стаття спочатку була опублікована на Airpair у 2015 році. Усі заслуги належать Airpair і Браяну Фентону, який спочатку написав цю статтю, хоча веб-сайт більше не доступний, і стаття існує лише в Wayback Machine. Цю статтю додано на сайт для навчальних і освітніх цілей для спільноти PHP загалом.
1 Налаштування та конфігурація
1.1 Тримайте актуальним
Давайте скажемо це з самого початку - вражаюче мала кількість інсталяцій PHP у реальному світі є актуальними або залишаються актуальними. Чи то через обмеження спільного хостингу, налаштувань, які ніхто не думає змінити, чи відсутність часу/бюджету на тестування оновлень, скромні бінарні файли PHP часто залишаються позаду. Отже, одним чітким найкращим правилом, якому потрібно надати більше уваги, є завжди використовувати актуальну версію PHP (5.6.x на момент написання цієї статті). Крім того, важливо планувати регулярні оновлення як самого PHP, так і будь-яких розширень чи бібліотек постачальників, які ви можете використовувати. Оновлення дають нові функції мови, покращену швидкість, нижче використання пам'яті та оновлення безпеки. Чим частіше ви оновлюєте, тим менш болючим стає процес.
1.2 Встановіть розумні налаштування за замовчуванням
PHP робить пристойну роботу з встановленням хороших налаштувань за замовчуванням у файлах php.ini.development і php.ini.production, але ми можемо зробити краще. По-перше, вони не встановлюють дату/часовий пояс для нас. Це має сенс з точки зору дистрибуції, але без нього PHP видаватиме помилку E_WARNING щоразу, коли ми викликаємо функцію, пов'язану з датою/часом. Ось деякі рекомендовані налаштування:
- date.timezone - виберіть з списку підтриманих часових поясів
- session.savepath - якщо ми використовуємо файли для сесій, а не інший обробник збереження, встановіть це щось за межами /tmp. Залишення цього як /tmp може бути ризикованим у середовищі спільного хостингу, оскільки /tmp_ зазвичай має широкі дозволи. Навіть з встановленим бітом sticky, будь-кому з доступом до переліку вмісту цього каталогу можна дізнатися всі ваші активні ID сесій.
- session.cookie_secure - очевидно, увімкніть це, якщо ви обслуговуєте код PHP через HTTPS.
- session.cookie_httponly - встановіть це, щоб запобігти доступу до файлів cookie сесії PHP через JavaScript
- Більше... використовуйте інструмент, як iniscan, щоб перевірити вашу конфігурацію на поширені вразливості
1.3 Розширення
Також гарною ідеєю є вимкнення (або принаймні не ввімкнення) розширень, які ви не використовуватимете, як драйвери баз даних. Щоб побачити, що ввімкнено, запустіть команду phpinfo() або перейдіть до командного рядка та запустіть це.
$ php -i
Інформація така сама, але phpinfo() додає HTML-форматування. Версія CLI легше перенаправляти до grep для пошуку конкретної інформації. Приклад.
$ php -i | grep error_log
Однак є застереження цього методу: можливо, мати різні налаштування PHP, які застосовуються до веб-версії та версії CLI.
2 Використовуйте Composer
Це може бути несподіванкою, але одним з найкращих правил для написання сучасного PHP є написання меншої його кількості. Хоча правда, що один з найкращих способів стати хорошим у програмуванні - це робити це, є велика кількість проблем, які вже розв'язано в просторі PHP, як маршрутизація, базові бібліотеки перевірки введення, перетворення одиниць, шари абстракції баз даних тощо... Просто перейдіть до Packagist та перегляньте. Ви, ймовірно, виявите, що значна частина проблеми, яку ви намагаєтеся розв'язати, вже написана та протестована.
Хоча спокусливо написати весь код самостійно (і немає нічого поганого в написанні власної фреймворку чи бібліотеки як досвіду навчання) ви повинні боротися з цими почуттями "Не винайдено тут" та заощадити собі багато часу та головного болю. Слідуйте доктрині PIE - Proudly Invented Elsewhere. Також, якщо ви вирішите написати власне що-небудь, не випускайте це, якщо це робить щось значно інше чи краще, ніж існуючі пропозиції.
Composer є менеджером пакетів для PHP, подібним до pip у Python, gem у Ruby та npm у Node. Він дозволяє визначити файл JSON, який перелічує залежності вашого коду, і він спробує розв'язати ці вимоги, завантаживши та встановивши необхідні пакунки коду.
2.1 Встановлення Composer
Ми припускаємо, що це локальний проєкт, тож давайте встановимо екземпляр Composer лише для поточного проєкту. Перейдіть до каталогу проєкту та запустіть це:
$ curl -sS https://getcomposer.org/installer | php
Майте на увазі, що перенаправлення будь-якого завантаження безпосередньо до інтерпретатора скриптів (sh, ruby, php тощо) є ризиком безпеки, тож спочатку прочитайте код встановлення та переконайтеся, що ви комфортні з ним, перш ніж запускати будь-яку таку команду.
З міркувань зручності (якщо ви віддаєте перевагу введенню composer install замість php composer.phar install), ви можете використовувати цю команду, щоб встановити єдиний екземпляр composer глобально:
$ mv composer.phar /usr/local/bin/composer
$ chmod +x composer
Вам може знадобитися запустити ці з sudo, залежно від ваших дозволів файлів.
2.2 Використання Composer
Composer має дві основні категорії залежностей, які він може керувати: "require" і "require-dev". Залежності, перелічені як "require", встановлюються скрізь, але залежності "require-dev" встановлюються лише коли спеціально запитано. Зазвичай це інструменти для активної розробки коду, такі як PHP_CodeSniffer. Рядок нижче показує приклад, як встановити Guzzle, популярну бібліотеку HTTP.
$ php composer.phar require guzzle/guzzle
Щоб встановити інструмент лише для цілей розробки, додайте прапорець --dev:
$ php composer.phar require --dev 'sebastian/phpcpd'
Це встановлює PHP Copy-Paste Detector, інший інструмент якості коду як залежність лише для розробки.
2.3 Install vs update
Коли ми вперше запускаємо composer install, він встановить будь-які бібліотеки та їхні залежності, які нам потрібні, на основі файлу composer.json. Коли це зроблено, composer створює файл блокування, передбачувано званий composer.lock. Цей файл містить перелік залежностей, які composer знайшов для нас, та їхні точні версії з хешами. Потім щоразу, коли ми запускаємо composer install, він перевірить файл блокування та встановить ці точні версії.
composer update трохи інша звірюка. Він ігноруватиме файл composer.lock (якщо він є) і спробує знайти найновіші версії кожної з залежностей, які все ще задовольняють обмеження в composer.json. Потім він запише новий файл composer.lock, коли закінчить.
2.4 Автозавантаження
Як composer install, так і composer update згенерують автозавантажувач для нас, який повідомляє PHP, де знайти всі необхідні файли для використання бібліотек, які ми щойно встановили. Щоб використовувати його, просто додайте цей рядок (зазвичай до файлу завантаження, який виконується на кожному запиті):
require 'vendor/autoload.php';
3 Слідуйте хорошим принципам дизайну
3.1 SOLID
SOLID є мнемонікою, щоб нагадати нам про п'ять ключових принципів у хорошому дизайні об'єктно-орієнтованого програмного забезпечення.
3.1.1 S - Принцип єдиної відповідальності
Це стверджує, що класи повинні мати лише одну відповідальність, або, інакше кажучи, лише одну причину для зміни. Це добре поєднується з філософією Unix про багато маленьких інструментів, які роблять одну річ добре. Класи, які роблять лише одну річ, набагато легше тестувати та відлагоджувати, і вони менш імовірно здивують вас. Ви не хочете, щоб виклик методу до класу Validator оновлював записи в базі даних. Ось приклад порушення SRP, яке ви часто бачите в застосунку, заснованому на шаблоні ActiveRecord.
class Person extends Model
{
public $name;
public $birthDate;
protected $preferences;
public function getPreferences() {}
public function save() {}
}
Отже, це досить базова модель сутності. Однак одна з цих речей не належить сюди. Єдина відповідальність моделі сутності повинна бути поведінкою, пов'язаною з сутністю, яку вона представляє, вона не повинна бути відповідальною за збереження себе.
class Person extends Model
{
public $name;
public $birthDate;
protected $preferences;
public function getPreferences() {}
}
class DataStore
{
public function save(Model $model) {}
}
Це краще. Модель Person повернулася до виконання лише однієї речі, а поведінка збереження була перенесена до об'єкта збереження. Зверніть увагу, що я лише вказав тип на Model, а не на Person. Ми повернемося до цього, коли дійдемо до частин L і D SOLID.
3.1.2 O - Принцип відкритості/замкнутості
Є чудовий тест для цього, який досить добре підсумовує, про що цей принцип: подумайте про функцію для реалізації, ймовірно, найостаннішу, над якою ви працювали або працюєте. Чи можете ви реалізувати цю функцію у вашому існуючому коді виключно шляхом додавання нових класів і не змінюючи жодних існуючих класів у вашій системі? Ваша конфігурація та код з'єднання трохи виняток, але в більшості систем це дивно складно. Вам потрібно сильно покладатися на поліморфну диспетчеризацію, і більшість кодових баз просто не налаштовані для цього. Якщо вас це цікавить, є гарна лекція Google на YouTube про поліморфізм та написання коду без If, яка копає глибше. Як бонус, лекція проводиться Miško Hevery, якого багато хто знає як творця AngularJs.
3.1.3 L - Принцип заміни Лискова
Цей принцип названо на честь Barbara Liskov і наведено нижче:
"Об'єкти в програмі повинні бути замінними на екземпляри їхніх підтипів без зміни правильності тієї програми."
Це звучить добре, але це більш чітко ілюструється на прикладі.
abstract class Shape
{
public function getHeight();
public function setHeight($height);
public function getLength();
public function setLength($length);
}
Це буде представляти наш базовий чотирибічний об'єкт. Нічого особливого тут.
class Square extends Shape
{
protected $size;
public function getHeight() {
return $this->size;
}
public function setHeight($height) {
$this->size = $height;
}
public function getLength() {
return $this->size;
}
public function setLength($length) {
$this->size = $length;
}
}
Ось наш перший об'єкт, квадрат. Досить простий об'єкт, правда? Ви можете припустити, що є конструктор, де ми встановлюємо розміри, але ви бачите тут з цієї реалізації, що довжина та висота завжди будуть однаковими. Квадрати такі.
class Rectangle extends Shape
{
protected $height;
protected $length;
public function getHeight() {
return $this->height;
}
public function setHeight($height) {
$this->height = $height;
}
public function getLength() {
return $this->length;
}
public function setLength($length) {
$this->length = $length;
}
}
Отже, тут у нас інший об'єкт. Він має ті самі сигнатури методів, це все ще чотирибічний об'єкт, але що, якщо ми почнемо намагатися використовувати їх замість один одного? Тепер раптом, якщо ми змінимо висоту нашого Shape, ми більше не можемо припустити, що довжина нашого об'єкту співпадатиме. Ми порушили договір, який мали з користувачем, коли надали їм наш квадратний об'єкт.
Це приклад порушення LSP, і нам потрібен такий тип принципу, щоб найкраще використовувати систему типів. Навіть duck typing не скаже нам, чи відрізняється базова поведінка, і оскільки ми не можемо знати це без того, щоб побачити, як це ламається, краще переконатися, що це не відрізняється спочатку.
3.1.3 I - Принцип сегрегації інтерфейсів
Цей принцип говорить на користь багатьох малих, дрібнозернистих інтерфейсів проти одного великого. Інтерфейси повинні базуватися на поведінці, а не "це один з цих класів". Подумайте про інтерфейси, які постачаються з PHP. Traversable, Countable, Serializable, такі речі. Вони рекламують можливості, які об'єкт має, а не те, що він успадковує. Тож тримайте свої інтерфейси малими. Ви не хочете, щоб інтерфейс мав 30 методів, 3 - набагато краща мета.
3.1.4 D - Принцип інверсії залежностей
Ви, ймовірно, чули про це в інших місцях, де йшлося про Dependency Injection, але інверсія залежностей і ін'єкція залежностей не зовсім одне і те ж. Інверсія залежностей - це насправді спосіб сказати, що ви повинні залежати від абстракцій у вашій системі, а не від її деталей. Що це означає для вас у повсякденному житті?
Не використовуйте безпосередньо mysqli_query() по всьому вашому коду, використовуйте щось на зразок DataStore->query() замість.
Ядро цього принципу - це насправді про абстракції. Йдеться про те, щоб сказати "використовуйте адаптер бази даних" замість залежності від прямих викликів, як mysqli_query. Якщо ви безпосередньо використовуєте mysqli_query у половині ваших класів, ви прив'язуєте все безпосередньо до вашої бази даних. Нічого проти MySQL, але якщо ви використовуєте mysqli_query, такий тип низькорівневого деталю повинен бути приховано лише в одному місці, а потім ця функціональність повинна бути викрита через загальну обгортку.
Тепер я знаю, що це дещо банальний приклад, якщо ви подумаєте про це, бо кількість разів, коли ви фактично повністю зміните двигун бази даних після випуску продукту в виробництво, дуже низка. Я вибрав це, бо вважав, що люди будуть знайомі з ідеєю зі свого власного коду. Крім того, навіть якщо у вас є база даних, з якою ви плануєте залишитися, цей абстрактний об'єкт обгортки дозволяє виправляти помилки, змінювати поведінку або впроваджувати функції, які ви бажаєте, щоб ваша вибрана база даних мала. Він також робить можливим одиничне тестування, де низькорівневі виклики не роблять.
4 Об'єктні вправи
Це не повний занурення в ці принципи, але перші два легкі запам'ятати, надають хорошу цінність і можуть бути негайно застосовані до практично будь-якої кодової бази.
4.1 Не більше одного рівня відступу на метод
Це корисний спосіб думати про розкладання методів на менші шматки, залишаючи код, який є чіткішим і більш само-документованим. Чим більше рівнів відступу ви маєте, тим більше метод робить і тим більше стану ви повинні відстежувати у своїй голові, працюючи з ним.
Негайно я знаю, що люди заперечуватимуть проти цього, але це лише рекомендація/евристика, а не тверде правило. Я не очікую, що хтось буде застосовувати правила PHP_CodeSniffer для цього (хоча люди робили).
Давайте швидко пройдемо приклад того, як це може виглядати:
public function transformToCsv($data)
{
$csvLines = array();
$csvLines[] = implode(',', array_keys($data[0]));
foreach ($data as $row) {
if (!$row) {
continue;
}
$csvLines[] = implode(',', $row);
}
return $csvLines;
}
Хоча це не жахливий код (він технічно правильний, тестується тощо), ми можемо зробити багато більше, щоб зробити це чітким. Як би ми зменшили рівні вкладення тут?
Ми знаємо, що нам потрібно значно спростити вміст циклу foreach (або видалити його повністю), тож давайте розпочнемо з цього.
if (!$row) {
continue;
}
Ця перша частина проста. Все, що це робить, - це ігнорує порожні рядки. Ми можемо скоротити цей весь процес, використовуючи вбудовану функцію PHP до того, як ми навіть дійдемо до циклу.
$data = array_filter($data);
foreach ($data as $row) {
$csvLines[] = implode(',', $row);
}
Тепер у нас є наш єдиний рівень вкладення. Але, дивлячись на це, все, що ми робимо, - це застосовуємо функцію до кожного елемента масиву. Нам навіть не потрібен цикл foreach для цього.
$data = array_filter($data);
$csvLines = array_map(function($row) {
return implode(',', $row);
}, $data);
Тепер у нас немає вкладення взагалі, і код, ймовірно, буде швидшим, оскільки ми робимо весь цикл з нативними C-функціями замість PHP. Нам потрібно трохи хитрощів, щоб передати кому до implode, тож ви можете стверджувати, що зупинка на попередньому кроці є набагато зрозумілішою.
4.2 Спробуйте не використовувати else
Це справді стосується двох основних ідей. Перша - це кілька інструкцій return з методу. Якщо у вас достатньо інформації, щоб прийняти рішення про результат методу, вперед і прийміть це рішення та поверніть. Друга - ідея, відома як Guard Clauses. Це, по суті, перевірки перевірки, поєднані з ранніми return, зазвичай поблизу верху методу. Дозвольте мені показати, що я маю на увазі.
public function addThreeInts($first, $second, $third) {
if (is_int($first)) {
if (is_int($second)) {
if (is_int($third)) {
$sum = $first + $second + $third;
} else {
return null;
}
} else {
return null;
}
} else {
return null;
}
return $sum;
}
Отже, це досить простий приклад, він додає 3 цілих числа та повертає результат, або null, якщо будь-який з параметрів не є цілим. Ігноруючи той факт, що ми могли б поєднати всі ці перевірки в один рядок з операторами AND, я думаю, ви бачите, як вкладена структура if/else робить код важчим для слідкування. Тепер подивіться на цей приклад замість.
public function addThreeInts($first, $second, $third) {
if (!is_int($first)) {
return null;
}
if (!is_int($second)) {
return null;
}
if (!is_int($third)) {
return null;
}
return $first + $second + $third;
}
Для мене цей приклад набагато легший для слідкування. Тут ми використовуємо захисні клаузи, щоб перевірити наші початкові твердження про параметри, які ми передаємо, і негайно виходимо з методу, якщо вони не проходять. Ми також більше не маємо проміжної змінної для відстеження суми протягом усього методу. У цьому випадку ми перевірили, що ми вже на щасливому шляху, і можемо просто робити те, що прийшли сюди робити. Знову ж таки, ми могли б просто зробити всі ці перевірки в одному if, але принцип повинен бути зрозумілим.
5 Одиничне тестування
Одиничне тестування - це практика написання малих тестів, які перевіряють поведінку у вашому коді. Вони майже завжди пишуть на тій самій мові, що й код (у цьому випадку PHP) і призначені бути достатньо швидкими, щоб запускатися в будь-який час. Вони надзвичайно цінні як інструмент для покращення вашого коду. Крім очевидних переваг забезпечення того, що ваш код робить те, що ви думаєте, одиничне тестування може надати дуже корисний зворотний зв'язок дизайну. Якщо шматок коду важко тестувати, це часто демонструє проблеми дизайну. Вони також дають вам сітку безпеки проти регресій, і це дозволяє вам рефакторити набагато частіше та еволюціонувати свій код до чистішого дизайну.
5.1 Інструменти
Існує кілька інструментів одиничного тестування в PHP, але далеко найпоширеніший - PHPUnit. Ви можете встановити його, завантаживши PHAR файл безпосередньо, або встановити за допомогою composer. Оскільки ми використовуємо composer для всього іншого, ми покажемо цей метод. Крім того, оскільки PHPUnit, ймовірно, не буде розгорнуто в виробництві, ми можемо встановити його як залежність розробки з такою командою:
composer require --dev phpunit/phpunit
5.2 Тести є специфікацією
Найважливіша роль одиничних тестів у вашому коді - це надання виконуваної специфікації того, що код повинен робити. Навіть якщо код тесту неправильний, або код має помилки, знання того, що система повинна робити, безцінне.
5.3 Пишіть ваші тести першими
Якщо ви мали шанс побачити набір тестів, написаних перед кодом і один, написаний після того, як код був завершений, вони вражаюче різні. "Після" тести набагато більше стурбовані деталями реалізації класу та забезпеченням хорошого покриття рядків, тоді як "до" тести більше про перевірку бажаного зовнішнього поведінки. Це дійсно те, що нас цікавить з одиничними тестами, є забезпечення того, щоб клас демонстрував правильну поведінку. Тести, орієнтовані на реалізацію, фактично ускладнюють рефакторинг, тому що вони ламаються, якщо внутрішні частини класів змінюються, і ви щойно втратили переваги приховування інформації від ООП.
5.4 Що робить хороший одиничний тест
Хороші одиничні тести мають багато таких характеристик:
- Швидкі - повинні запускатися в мілісекундах.
- Без доступу до мережі - повинні мати можливість вимкнути бездротовий/від'єднати і всі тести все ще проходити.
- Обмежений доступ до файлової системи - це додає до швидкості та гнучкості при розгортанні коду в інші середовища.
- Без доступу до бази даних - уникає costly налаштування та дій розбирання.
- Тестуйте лише одну річ за раз - одиничний тест повинен мати лише одну причину для невдачі.
- Добре названі - див. 5.2 вище.
- Здебільшого фейкові об'єкти - єдині "реальні" об'єкти в одиничних тестах повинні бути об'єктом, який ми тестуємо, та простими об'єктами значень. Решта повинна бути якоюсь формою test double
Є причини йти проти деяких з цих, але як загальні рекомендації вони послужать вам добре.
5.5 Коли тестування боляче
Одиничне тестування змушує вас відчути біль поганого дизайну спереду - Michael Feathers
Коли ви пишете одиничні тести, ви змушуєте себе фактично використовувати клас для досягнення речей. Якщо ви пишете тести в кінці, або, що гірше, просто кидаєте код через стіну для QA чи когось, щоб написати тести, ви не отримуєте жодного зворотного зв'язку про те, як клас фактично поводиться. Якщо ми пишемо тести, і клас є справжнім болем для використання, ми дізнаємося про це, коли пишемо його, що майже найдешевший час, щоб виправити це.
Якщо клас важко тестувати, це дефект дизайну. Різні дефекти проявляються по-різному, хоча. Якщо вам потрібно робити багато моків, ваш клас, ймовірно, має забагато залежностей, або ваші методи роблять забагато. Чим більше налаштування ви повинні робити для кожного тесту, тим більше ймовірно, що ваші методи роблять забагато. Якщо вам потрібно писати дуже заплутані сценарії тестів, щоб перевірити поведінку, методи класу, ймовірно, роблять забагато. Якщо вам потрібно копати всередині купи приватних методів та стану, щоб тестувати речі, можливо, є інший клас, який намагається вийти. Одиничне тестування дуже добре розкриває "айсбергові класи", де 80% того, що робить клас, приховано в захищеному або приватному коді. Я колись був великим шанувальником робити якомога більше захищеним, але тепер зрозумів, що я просто робив свої індивідуальні класи відповідальними за забагато, і справжнє рішення було розділити клас на менші шматки.
Написано Браяном Фентоном - Браян Фентон є розробником PHP протягом 8 років у Середньому Заході та районі затоки, зараз у Thismoment. Він зосереджується на майстерності коду та принципах дизайну. Блог на www.brianfenton.us, Twitter на @brianfenton. Коли він не зайнятий тим, щоб бути татом, він насолоджується їжею, пивом, іграми та навчанням.
Learn/security
Безпека
Огляд
Безпека є дуже важливою для веб-додатків. Ви маєте переконатися, що ваш додаток захищений, а дані ваших користувачів у безпеці. Flight надає низку функцій, які допоможуть вам захистити ваші веб-додатки.
Офіційний скелет також містить спеціальний SECURITY.md і проміжне програмне забезпечення для заголовків безпеки, щоб AI-інструменти для кодування (і люди) мали одне продумане місце для секретів, заголовків і правил XSS/SQL — окремо від загального стилю кодування в AGENTS.md.
Розуміння
Існує кілька поширених загроз безпеці, про які слід пам’ятати під час створення веб-додатків. Деякі з найпоширеніших загроз включають:
- Міжсайтова підробка запитів (CSRF)
- Міжсайтовий скриптинг (XSS)
- SQL-ін'єкція
- Обмін ресурсами між джерелами (CORS)
Шаблони допомагають із XSS, екрануючи вивід за замовчуванням (Twig і Latte роблять це; використовуйте цю перевагу). Сесії можуть допомогти з CSRF, зберігаючи CSRF-токен у сесії користувача, як описано нижче. Використання підготовлених запитів із PDO або помічників на основі SimplePdo допомагає запобігти SQL-ін'єкціям. CORS можна обробити за допомогою простого хука перед викликом Flight::start().
Усі ці методи працюють разом, щоб захистити ваші веб-додатки. Завжди слід пам’ятати про вивчення та розуміння найкращих практик безпеки. Не просіть AI-асистента «вимкнути CSP» або послабити заголовки лише для того, щоб сторінка завантажилася, не розуміючи компромісів.
Базове використання
Заголовки
HTTP-заголовки — це один із найпростіших способів захистити ваші веб-додатки. Ви можете використовувати заголовки для запобігання клікджекінгу, XSS та інших атак. Існує кілька способів додати ці заголовки до вашого додатка.
Два чудові веб-сайти для перевірки безпеки ваших заголовків: securityheaders.com та observatory.mozilla.org. Після налаштування наведеного нижче коду ви легко зможете перевірити роботу заголовків за допомогою цих двох сайтів.
Скелет включає App\Middleware\SecurityHeadersMiddleware (CSP із nonce для кожного запиту, параметри фреймів, HSTS та інше). Віддавайте перевагу свідомому розширенню цього класу, а не вимкненню заголовків.
Додати вручну
Ви можете додати ці заголовки вручну за допомогою методу header на об'єкті Flight\Response.
// Встановлюємо заголовок X-Frame-Options для запобігання клікджекінгу
Flight::response()->header('X-Frame-Options', 'SAMEORIGIN');
// Встановлюємо заголовок Content-Security-Policy для запобігання XSS
// Зверніть увагу: цей заголовок може бути дуже складним, тому вам варто
// переглянути приклади в інтернеті для вашого додатка
Flight::response()->header("Content-Security-Policy", "default-src 'self'");
// Встановлюємо заголовок X-XSS-Protection для запобігання XSS
Flight::response()->header('X-XSS-Protection', '1; mode=block');
// Встановлюємо заголовок X-Content-Type-Options для запобігання MIME-сніфінгу
Flight::response()->header('X-Content-Type-Options', 'nosniff');
// Встановлюємо заголовок Referrer-Policy для контролю обсягу інформації про реферера
Flight::response()->header('Referrer-Policy', 'no-referrer-when-downgrade');
// Встановлюємо заголовок Strict-Transport-Security для примусового HTTPS
Flight::response()->header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
// Встановлюємо заголовок Permissions-Policy для контролю функцій і API
Flight::response()->header('Permissions-Policy', 'geolocation=()');
Їх можна додати на початку файлів routes.php або index.php.
Додати як фільтр
Ви також можете додати їх у фільтр/хук, як показано нижче:
// Додаємо заголовки у фільтр
Flight::before('start', function() {
Flight::response()->header('X-Frame-Options', 'SAMEORIGIN');
Flight::response()->header("Content-Security-Policy", "default-src 'self'");
Flight::response()->header('X-XSS-Protection', '1; mode=block');
Flight::response()->header('X-Content-Type-Options', 'nosniff');
Flight::response()->header('Referrer-Policy', 'no-referrer-when-downgrade');
Flight::response()->header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
Flight::response()->header('Permissions-Policy', 'geolocation=()');
});
Додати як проміжне програмне забезпечення
Ви також можете додати їх як клас проміжного програмного забезпечення, який забезпечує найбільшу гнучкість щодо того, до яких маршрутів це застосовувати. Загалом ці заголовки слід застосовувати до всіх HTML та API відповідей.
Шлях і простір імен у стилі скелета (регістр папки відповідає App\Middleware):
// app/Middleware/SecurityHeadersMiddleware.php
namespace App\Middleware;
use flight\Engine;
class SecurityHeadersMiddleware
{
protected Engine $app;
public function __construct(Engine $app)
{
$this->app = $app;
}
public function before(array $params): void
{
$response = $this->app->response();
// Віддавайте перевагу nonce CSP з bootstrap, якщо у вас є інлайн-скрипти (скелет встановлює csp_nonce)
$nonce = $this->app->get('csp_nonce');
$csp = $nonce
? "default-src 'self'; script-src 'self' 'nonce-{$nonce}'; style-src 'self' 'nonce-{$nonce}'"
: "default-src 'self'";
$response->header('X-Frame-Options', 'SAMEORIGIN');
$response->header('Content-Security-Policy', $csp);
$response->header('X-XSS-Protection', '1; mode=block');
$response->header('X-Content-Type-Options', 'nosniff');
$response->header('Referrer-Policy', 'no-referrer-when-downgrade');
$response->header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
$response->header('Permissions-Policy', 'geolocation=()');
}
}
// app/config/routes.php — порожній рядок групи = глобальне проміжне ПЗ для всіх маршрутів
use App\Middleware\SecurityHeadersMiddleware;
use flight\net\Router;
$router->group('', function (Router $router) {
$router->get('/users', [ \App\Controller\UserController::class, 'getUsers' ]);
// інші маршрути
}, [SecurityHeadersMiddleware::class]);
Старі проєкти можуть досі використовувати app/middlewares і app\middlewares; це працює, якщо папки збігаються. Нові додатки на скелеті використовують app/Middleware/ та App\Middleware. Дивіться Автозавантаження.
Міжсайтова підробка запитів (CSRF)
Міжсайтова підробка запитів (CSRF) — це тип атаки, коли зловмисний веб-сайт може змусити браузер користувача надіслати запит на ваш веб-сайт. Це можна використати для виконання дій на вашому веб-сайті без відома користувача. Flight не має вбудованого механізму захисту від CSRF, але ви можете легко реалізувати власний за допомогою проміжного програмного забезпечення.
Налаштування
Спочатку потрібно згенерувати CSRF-токен і зберегти його в сесії користувача. Потім ви можете використовувати цей токен у ваших формах і перевіряти його під час надсилання форми. Ми використаємо плагін flightphp/session для керування сесіями.
// Генеруємо CSRF-токен і зберігаємо його в сесії користувача
// (припускаючи, що ви створили об'єкт сесії та приєднали його до Flight)
// дивіться документацію сесій для додаткової інформації
Flight::register('session', flight\Session::class);
// Ви можете згенерувати лише один токен на сесію (тож він працює
// у кількох вкладках і запитах для одного користувача)
if(Flight::session()->get('csrf_token') === null) {
Flight::session()->set('csrf_token', bin2hex(random_bytes(32)) );
}
Використання стандартного шаблону PHP Flight
<!-- Використовуємо CSRF-токен у вашій формі -->
<form method="post">
<input type="hidden" name="csrf_token" value="<?= Flight::session()->get('csrf_token') ?>">
<!-- інші поля форми -->
</form>
Використання Twig (типово для скелета)
Зареєструйте функцію Twig або передавайте токен у кожне подання форми. Мінімальний приклад із глобальною змінною та полем форми:
// Під час налаштування Twig (наприклад, services.php)
$twig->addGlobal('csrf_token', $app->session()->get('csrf_token'));
{# app/views/form.twig #}
<form method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
{# інші поля #}
</form>
Використання Latte
Ви також можете встановити власну функцію для виведення CSRF-токена у ваших шаблонах Latte.
Flight::map('render', function(string $template, array $data, ?string $block): void {
$latte = new Latte\Engine;
// інші конфігурації...
// Встановлюємо власну функцію для виведення CSRF-токена
$latte->addFunction('csrf', function() {
$csrfToken = Flight::session()->get('csrf_token');
return new \Latte\Runtime\Html('<input type="hidden" name="csrf_token" value="' . $csrfToken . '">');
});
$latte->render($finalPath, $data, $block);
});
І тепер у ваших шаблонах Latte ви можете використовувати функцію csrf() для виведення CSRF-токена.
<form method="post">
{csrf()}
<!-- інші поля форми -->
</form>
Перевірка CSRF-токена
Ви можете перевірити CSRF-токен кількома методами.
Проміжне програмне забезпечення
// app/Middleware/CsrfMiddleware.php
namespace App\Middleware;
use flight\Engine;
class CsrfMiddleware
{
protected Engine $app;
public function __construct(Engine $app)
{
$this->app = $app;
}
public function before(array $params): void
{
if($this->app->request()->method == 'POST') {
$token = $this->app->request()->data->csrf_token;
if($token !== $this->app->session()->get('csrf_token')) {
$this->app->halt(403, 'Invalid CSRF token');
}
}
}
}
// routes.php
use App\Middleware\CsrfMiddleware;
$router->group('', function ($router) {
$router->get('/users', [ \App\Controller\UserController::class, 'getUsers' ]);
// інші маршрути
}, [CsrfMiddleware::class]);
Фільтри подій
// Це проміжне ПЗ перевіряє, чи є запит POST, і якщо так, перевіряє дійсність CSRF-токена
Flight::before('start', function() {
if(Flight::request()->method == 'POST') {
// отримуємо csrf-токен із значень форми
$token = Flight::request()->data->csrf_token;
if($token !== Flight::session()->get('csrf_token')) {
Flight::halt(403, 'Invalid CSRF token');
// або для JSON-відповіді
Flight::jsonHalt(['error' => 'Invalid CSRF token'], 403);
}
}
});
Міжсайтовий скриптинг (XSS)
Міжсайтовий скриптинг (XSS) — це тип атаки, коли зловмисне введення у формі може впровадити код у ваш веб-сайт. Більшість таких можливостей походять від значень форм, які заповнюють ваші кінцеві користувачі. Вам ніколи не слід довіряти виводу від користувачів! Завжди вважайте, що всі вони — найкращі хакери у світі. Вони можуть впровадити зловмисний JavaScript або HTML на вашу сторінку. Цей код може бути використаний для викрадення інформації від ваших користувачів або виконання дій на вашому веб-сайті. Використовуючи клас представлення Flight або шаблонізатор, як-от Twig чи Latte, ви можете легко екранувати вивід для запобігання XSS-атакам.
// Припустимо, користувач розумний і намагається використати це як ім'я
$name = '<script>alert("XSS")</script>';
// Це екранує вивід
Flight::view()->set('name', $name);
// Це виведе: <script>alert("XSS")</script>
// Twig (типово для скелета) і Latte автоматично екранують за замовчуванням — віддавайте їм перевагу над сирим echo у PHP
Flight::render('template', ['name' => $name]);
// Twig: {{ name }} → екрановано
// Уникайте |raw / неекранованого виводу, якщо контент повністю надійний
SQL-ін'єкція
SQL-ін'єкція — це тип атаки, коли зловмисний користувач може впровадити SQL-код у вашу базу даних. Це можна використати для викрадення інформації з вашої бази даних або виконання дій із нею. Знову ж таки, вам ніколи не слід довіряти введенню від користувачів! Завжди вважайте, що вони прагнуть крові. Використовуйте підготовлені запити — помічники SimplePdo роблять цей шлях типовим.
// Припускаємо, що Flight::db() зареєстровано як SimplePdo (або SimplePdo впроваджено в контролер)
$statement = Flight::db()->prepare('SELECT * FROM users WHERE username = :username');
$statement->execute([':username' => $username]);
$users = $statement->fetchAll();
// SimplePdo (рекомендовано) — однострокові запити з зв'язаними параметрами
$users = Flight::db()->fetchAll('SELECT * FROM users WHERE username = :username', [ 'username' => $username ]);
// Та сама ідея з плейсхолдерами ?
$users = Flight::db()->fetchAll('SELECT * FROM users WHERE username = ?', [ $username ]);
У контролерах у стилі скелета віддавайте перевагу впровадженню SimplePdo через конструктор, а не Flight::db(), щоб тести та AI-згенерований код залишалися узгодженими (DIC).
Небезпечний приклад
Нижче наведено, чому ми використовуємо підготовлені SQL-запити для захисту від таких простих прикладів:
// кінцевий користувач заповнює веб-форму.
// для значення форми хакер вводить щось на кшталт цього:
$username = "' OR 1=1; -- ";
$sql = "SELECT * FROM users WHERE username = '$username' LIMIT 5";
$users = Flight::db()->fetchAll($sql);
// Після побудови запиту він виглядає так
// SELECT * FROM users WHERE username = '' OR 1=1; -- LIMIT 5
// Це виглядає дивно, але це дійсний запит, який спрацює. Насправді
// це дуже поширена SQL-ін'єкція, яка поверне всіх користувачів.
var_dump($users); // це виведе всіх користувачів у базі даних, а не лише одне ім'я користувача
Секрети та конфігурація
- Зберігайте секрети в
.env(або в реальному середовищі), а не в закомічених зразкахconfig.php. - Правило скелета: буквальні значення за замовчуванням у
config.php; об'єднуйте з.envпід час bootstrap; не читайте$_ENVу контролерах — натомість впроваджуйте конфігурацію. Дивіться Конфігурація. - Ніколи не комітьте API-ключі, паролі бази даних або ключі шифрування сесій. Вказуйте AI-інструментам на
SECURITY.md, щоб вони не вигадували небезпечні скорочення.
Перевірка зворотного виклику JSONP
Якщо ви використовуєте метод Flight::jsonp(), майте на увазі, що Flight перевіряє назву параметра зворотного виклику JSONP за суворим дозволеним регулярним виразом (/^[A-Za-z_$][\w$.]{0,127}$/). Будь-яка назва зворотного виклику, яка не відповідає цьому шаблону, призведе до винятку Flight, що запобігає впровадженню довільного JavaScript через зловмисне значення зворотного виклику.
Ця перевірка вбудована і не потребує додаткової конфігурації, але про неї варто знати під час налагодження неочікуваних помилок від JSONP-кінцевих точок.
CORS (Обмін ресурсами між джерелами)
CORS — це механізм, який дозволяє багатьом ресурсам (наприклад, шрифтам, JavaScript тощо) на веб-сторінці запитуватися з іншого домену, ніж домен, з якого походить ресурс. Flight не має вбудованої функціональності, але це легко обробляється за допомогою хука, який виконується перед викликом методу Flight::start().
// app/Utils/CorsUtil.php (скелет: тека Utils у PascalCase → App\Utils)
namespace App\Utils;
use flight\Engine;
class CorsUtil
{
protected Engine $app;
public function __construct(Engine $app)
{
$this->app = $app;
}
public function set(array $params = []): void
{
$request = $this->app->request();
$response = $this->app->response();
if ($request->getVar('HTTP_ORIGIN') !== '') {
$this->allowOrigins();
$response->header('Access-Control-Allow-Credentials', 'true');
$response->header('Access-Control-Max-Age', '86400');
}
if ($request->method === 'OPTIONS') {
if ($request->getVar('HTTP_ACCESS_CONTROL_REQUEST_METHOD') !== '') {
$response->header(
'Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD'
);
}
if ($request->getVar('HTTP_ACCESS_CONTROL_REQUEST_HEADERS') !== '') {
$response->header(
"Access-Control-Allow-Headers",
$request->getVar('HTTP_ACCESS_CONTROL_REQUEST_HEADERS')
);
}
$response->status(200);
$response->send();
exit;
}
}
private function allowOrigins(): void
{
// налаштуйте дозволені хости тут.
$allowed = [
'capacitor://localhost',
'ionic://localhost',
'http://localhost',
'http://localhost:4200',
'http://localhost:8080',
'http://localhost:8100',
];
$request = $this->app->request();
if (in_array($request->getVar('HTTP_ORIGIN'), $allowed, true) === true) {
$response = $this->app->response();
$response->header("Access-Control-Allow-Origin", $request->getVar('HTTP_ORIGIN'));
}
}
}
// bootstrap / routes — виконується перед start
$app = Flight::app();
$cors = new \App\Utils\CorsUtil($app);
$app->before('start', [ $cors, 'set' ]);
Посилення конфігурації Flight
Flight надає кілька налаштувань движка, які мають прямий вплив на безпеку. Правильне налаштування цих параметрів — один із найпростіших способів посилити захист вашого додатка.
flight.allow_method_override
За замовчуванням Flight дозволяє клієнтам перевизначати HTTP-метод запиту за допомогою заголовка X-HTTP-Method-Override або поля _method у POST-тілі. Хоча це зручно для HTML-форм, які можуть надсилати лише GET/POST, це може бути небезпечно, якщо ви цього не очікуєте — зловмисник може підробити DELETE або PUT запити через звичайну форму.
Якщо ваш додаток не покладається на цю поведінку (наприклад, ви створюєте API для сучасних клієнтів або JavaScript-фронтендів, які можуть надсилати будь-які HTTP-дієслова), вам слід вимкнути це:
// У вашому index.php або bootstrap-файлі, перед Flight::start()
Flight::set('flight.allow_method_override', false);
Значення за замовчуванням — true для зворотної сумісності, але встановлення false настійно рекомендується для будь-якого додатка, який явно не потребує функції перевизначення.
flight.debug
Flight має налаштування flight.debug, яке контролює, чи відображається детальна інформація про помилку (повідомлення винятку, код і повний стек викликів) у браузері, коли виникає необроблений виняток. За замовчуванням значення false, що означає показ лише загального повідомлення 500 Internal Server Error — жодні внутрішні деталі не витікають клієнту.
Ніколи не вмикайте це на продакшн-сервері. Використовуйте це лише локально або в стейджинг-середовищі:
// Безпечно лише для локальної розробки — НІКОЛИ не для продакшну
Flight::set('flight.debug', true);
Коли flight.debug має значення false (за замовчуванням), ви все одно можете фіксувати помилки, увімкнувши flight.log_errors:
// Логуємо помилки на сервері, не показуючи їх клієнту
Flight::set('flight.debug', false);
Flight::set('flight.log_errors', true);
Рекомендована продакшн-конфігурація
// index.php або застосовується з конфігурації додатка / bootstrap
Flight::set('flight.allow_method_override', false);
Flight::set('flight.debug', false);
Flight::set('flight.log_errors', true);
Обробка помилок
Приховуйте деталі чутливих помилок у продакшні, щоб не розкривати інформацію зловмисникам. У продакшні журналюйте помилки замість їх відображення, встановивши display_errors у 0.
// У вашому bootstrap.php або index.php
// додайте це до вашого app/config/config.php
$environment = ENVIRONMENT;
if ($environment === 'production') {
ini_set('display_errors', 0); // Вимкнути відображення помилок
ini_set('log_errors', 1); // Журналювати помилки замість цього
ini_set('error_log', '/path/to/error.log');
}
// У ваших маршрутах або контролерах
// Використовуйте Flight::halt() для контрольованих відповідей з помилками
Flight::halt(403, 'Access denied');
Очищення вхідних даних
Ніколи не довіряйте введеним користувачем даним. Очищуйте їх за допомогою filter_var перед обробкою, щоб запобігти проникненню шкідливих даних. Віддавайте перевагу читанню введення через $app->request() (або Flight::request()), а не через сирі $_GET / $_POST у коді додатка.
// Припустімо, є $_POST запит із $_POST['input'] та $_POST['email']
// Очищуємо введений рядок
$clean_input = filter_var(Flight::request()->data->input, FILTER_SANITIZE_STRING);
// Очищуємо email
$clean_email = filter_var(Flight::request()->data->email, FILTER_SANITIZE_EMAIL);
Хешування паролів
Зберігайте паролі безпечно та перевіряйте їх безпечно за допомогою вбудованих функцій PHP, як-от password_hash і password_verify. Паролі ніколи не повинні зберігатися у відкритому вигляді, а також не повинні шифруватися оборотними методами. Хешування гарантує, що навіть якщо ваша база даних буде скомпрометована, фактичні паролі залишаться захищеними.
$password = Flight::request()->data->password;
// Хешуємо пароль під час збереження (наприклад, під час реєстрації)
$hashed_password = password_hash($password, PASSWORD_DEFAULT);
// Перевіряємо пароль (наприклад, під час входу)
if (password_verify($password, $stored_hash)) {
// Пароль збігається
}
Обмеження швидкості (Rate Limiting)
Захищайтеся від атак перебором або атак типу «відмова в обслуговуванні», обмежуючи швидкість запитів за допомогою кешу.
// Припускаємо, що у вас встановлено та зареєстровано flightphp/cache
// Використання flightphp/cache у фільтрі
Flight::before('start', function() {
$cache = Flight::cache();
$ip = Flight::request()->ip;
$key = "rate_limit_{$ip}";
$attempts = (int) $cache->retrieve($key);
if ($attempts >= 10) {
Flight::halt(429, 'Too many requests');
}
$cache->set($key, $attempts + 1, 60); // Скидання через 60 секунд
});
Дивіться також
- Сесії — Як безпечно керувати сесіями користувачів.
- Шаблони — Автоматичне екранування Twig/Latte та XSS.
- SimplePdo — Помічники для бази даних із підготовленими запитами.
- PdoWrapper — Застарілий; для нового коду використовуйте SimplePdo.
- Проміжне програмне забезпечення — Як використовувати проміжне ПЗ для спрощення додавання заголовків безпеки.
- Конфігурація —
.envпроти буквальної конфігурації, прапорці продакшну. - AI та досвід розробника — Тримайте політику безпеки в
SECURITY.mdдля агентів. - Відповіді — Як налаштовувати HTTP-відповіді з безпечними заголовками.
- Запити — Як обробляти та очищувати введення користувача.
- filter_var — PHP-функція для очищення введення.
- password_hash — PHP-функція для безпечного хешування паролів.
- password_verify — PHP-функція для перевірки хешованих паролів.
Усунення неполадок
- Зверніться до розділу «Дивіться також» вище для інформації про усунення неполадок, пов’язаних із компонентами Flight Framework.
- Якщо CSP блокує ваші скрипти, додайте nonce (шаблон скелета) або внесіть конкретні джерела до білого списку — не встановлюйте
script-src *без плану.
Журнал змін
- Документація — Скелет
App\Middleware, нотатки Twig CSRF/XSS, SimplePdo, секрети/.envтаSECURITY.mdдля AI-дружніх проєктів. - v3.18.1 — Додано розділ «Посилення конфігурації Flight», що охоплює
flight.allow_method_override,flight.debugта перевірку зворотного виклику JSONP. - v3.1.0 — Додано розділи про CORS, обробку помилок, очищення введення, хешування паролів та обмеження швидкості.
- v2.0 — Додано екранування для стандартних представлень для запобігання XSS.
Learn/routing
Маршрутизація
Огляд
Маршрутизація у Flight PHP зіставляє URL-шаблони з функціями зворотного виклику або методами класів, що забезпечує швидку та просту обробку запитів. Вона розроблена з мінімальними накладними витратами, зручна для початківців і розширювана без зовнішніх залежностей.
Розуміння
Маршрутизація — це основний механізм, який пов’язує HTTP-запити з логікою вашого застосунку у Flight. Визначаючи маршрути, ви задаєте, як різні URL-адреси запускають певний код — через функції, методи класів або дії контролерів. Система маршрутизації Flight є гнучкою: підтримує базові шаблони, іменовані параметри, регулярні вирази та розширені можливості, як-от впровадження залежностей і ресурсна маршрутизація. Такий підхід тримає ваш код організованим і легким у підтримці, залишаючись швидким і простим для початківців та розширюваним для досвідчених користувачів.
Примітка: Хочете краще зрозуміти маршрутизацію? Перегляньте сторінку «чому фреймворк?» для детальнішого пояснення.
Основне використання
Визначення простого маршруту
Базова маршрутизація у Flight виконується шляхом зіставлення URL-шаблону з функцією зворотного виклику або масивом із класом і методом.
Flight::route('/', function(){
echo 'hello world!';
});
Маршрути зіставляються в порядку їх визначення. Перший маршрут, який відповідає запиту, буде викликано.
Використання функцій як зворотних викликів
Зворотний виклик може бути будь-яким викличним об’єктом. Тож можна використовувати звичайну функцію:
function hello() {
echo 'hello world!';
}
Flight::route('/', 'hello');
Використання класів і методів як контролера
Можна також використовувати метод класу (статичний або звичайний):
class GreetingController {
public function hello() {
echo 'hello world!';
}
}
Flight::route('/', [ 'GreetingController','hello' ]);
// або
Flight::route('/', [ GreetingController::class, 'hello' ]); // рекомендований спосіб
// або
Flight::route('/', [ 'GreetingController::hello' ]);
// або
Flight::route('/', [ 'GreetingController->hello' ]);
Або створивши об’єкт спочатку, а потім викликавши метод:
use flight\Engine;
// GreetingController.php
class GreetingController
{
protected Engine $app
public function __construct(Engine $app) {
$this->app = $app;
$this->name = 'John Doe';
}
public function hello() {
echo "Hello, {$this->name}!";
}
}
// index.php
$app = Flight::app();
$greeting = new GreetingController($app);
Flight::route('/', [ $greeting, 'hello' ]);
Примітка: За замовчуванням, коли контролер викликається у межах фреймворку, клас
flight\Engineзавжди впроваджується, якщо ви не вкажете інше через контейнер впровадження залежностей
Маршрутизація за методами
За замовчуванням шаблони маршрутів зіставляються з усіма методами запитів. Ви можете реагувати на певні методи, розмістивши ідентифікатор перед URL.
Flight::route('GET /', function () {
echo 'I received a GET request.';
});
Flight::route('POST /', function () {
echo 'I received a POST request.';
});
// Не можна використовувати Flight::get() для маршрутів, оскільки це метод
// для отримання змінних, а не створення маршруту.
Flight::post('/', function() { /* код */ });
Flight::patch('/', function() { /* код */ });
Flight::put('/', function() { /* код */ });
Flight::delete('/', function() { /* код */ });
Також можна зіставити кілька методів з одним зворотним викликом, використовуючи розділювач |:
Flight::route('GET|POST /', function () {
echo 'I received either a GET or a POST request.';
});
Особлива обробка HEAD і OPTIONS запитів
Flight має вбудовану обробку для HTTP-запитів HEAD та OPTIONS:
HEAD-запити
- HEAD-запити обробляються так само, як і
GET, але Flight автоматично видаляє тіло відповіді перед відправкою клієнту. - Це означає, що ви можете визначити маршрут для
GET, і HEAD-запити на ту саму URL-адресу повертатимуть лише заголовки (без вмісту), як того вимагають стандарти HTTP.
Flight::route('GET /info', function() {
echo 'This is some info!';
});
// HEAD-запит до /info поверне ті самі заголовки, але без тіла.
OPTIONS-запити
OPTIONS-запити автоматично обробляються Flight для будь-якого визначеного маршруту.
- Коли надходить OPTIONS-запит, Flight відповідає статусом
204 No Contentта заголовкомAllow, у якому перелічено всі підтримувані HTTP-методи для цього маршруту. - Вам не потрібно визначати окремий маршрут для OPTIONS.
// Для маршруту, визначеного як:
Flight::route('GET|POST /users', function() { /* ... */ });
// OPTIONS-запит до /users відповість:
//
// Status: 204 No Content
// Allow: GET, POST, HEAD, OPTIONS
Використання об’єкта Router
Крім того, ви можете отримати об’єкт Router, який має кілька допоміжних методів:
$router = Flight::router();
// зіставляє всі методи, як і Flight::route()
$router->map('/', function() {
echo 'hello world!';
});
// GET-запит
$router->get('/users', function() {
echo 'users';
});
$router->post('/users', function() { /* код */});
$router->put('/users/update/@id', function() { /* код */});
$router->delete('/users/@id', function() { /* код */});
$router->patch('/users/@id', function() { /* код */});
Регулярні вирази (Regex)
Ви можете використовувати регулярні вирази у своїх маршрутах:
Flight::route('/user/[0-9]+', function () {
// Цей маршрут відповідатиме /user/1234
});
Хоча цей метод доступний, рекомендується використовувати іменовані параметри або іменовані параметри з регулярними виразами, оскільки вони більш читабельні та легші у підтримці.
Іменовані параметри
Ви можете вказувати іменовані параметри у маршрутах, які будуть передані у вашу функцію зворотного виклику. Це більше для читабельності маршруту, ніж щось інше. Будь ласка, перегляньте важливе застереження нижче.
Flight::route('/@name/@id', function (string $name, string $id) {
echo "hello, $name ($id)!";
});
Ви також можете додавати регулярні вирази до іменованих параметрів, використовуючи
розділювач ::
Flight::route('/@name/@id:[0-9]{3}', function (string $name, string $id) {
// Цей маршрут відповідатиме /bob/123
// Але не відповідатиме /bob/12345
});
Примітка: Зіставлення груп регулярних виразів
()із позиційними параметрами не підтримується. Напр.::'\(
Важливе застереження
Хоча у наведеному вище прикладі здається, що @name безпосередньо пов’язаний зі змінною $name, це не так. Порядок параметрів у функції зворотного виклику визначає, що буде передано. Якщо поміняти порядок параметрів у функції зворотного виклику, змінні також поміняються. Ось приклад:
Flight::route('/@name/@id', function (string $id, string $name) {
echo "hello, $name ($id)!";
});
І якщо ви перейдете за наступним URL: /bob/123, вивід буде hello, 123 (bob)!.
Будьте уважні, коли налаштовуєте маршрути та функції зворотного виклику!
Необов’язкові параметри
Ви можете визначити іменовані параметри як необов’язкові для зіставлення, обгорнувши сегменти у круглі дужки.
Flight::route(
'/blog(/@year(/@month(/@day)))',
function(?string $year, ?string $month, ?string $day) {
// Цей маршрут відповідатиме таким URL:
// /blog/2012/12/10
// /blog/2012/12
// /blog/2012
// /blog
}
);
Будь-які необов’язкові параметри, які не збіглися, будуть передані як NULL.
Глобальні шаблони (Wildcard Routing)
Зіставлення виконується лише для окремих сегментів URL. Якщо потрібно зіставити кілька
сегментів, можна використовувати символ *.
Flight::route('/blog/*', function () {
// Цей маршрут відповідатиме /blog/2000/02/01
});
Щоб направити всі запити на один зворотний виклик, можна зробити так:
Flight::route('*', function () {
// Зробити щось
});
Обробник 404 Not Found
За замовчуванням, якщо URL не знайдено, Flight надішле дуже просту відповідь HTTP 404 Not Found.
Якщо ви хочете налаштувати власну відповідь 404, ви можете змапити власний метод notFound:
Flight::map('notFound', function() {
$url = Flight::request()->url;
// Ви також можете використати Flight::render() з власним шаблоном.
$output = <<<HTML
<h1>My Custom 404 Not Found</h1>
<h3>The page you have requested {$url} could not be found.</h3>
HTML;
$this->response()
->clearBody()
->status(404)
->write($output)
->send();
});
Обробник Method Not Found
За замовчуванням, якщо URL знайдено, але метод не дозволений, Flight надішле дуже просту відповідь HTTP 405 Method Not Allowed (напр.: Method Not Allowed. Allowed Methods are: GET, POST). Вона також міститиме заголовок Allow із дозволеними методами для цього URL.
Якщо ви хочете налаштувати власну відповідь 405, ви можете змапити власний метод methodNotFound:
use flight\net\Route;
Flight::map('methodNotFound', function(Route $route) {
$url = Flight::request()->url;
$methods = implode(', ', $route->methods);
// Ви також можете використати Flight::render() з власним шаблоном.
$output = <<<HTML
<h1>My Custom 405 Method Not Allowed</h1>
<h3>The method you have requested for {$url} is not allowed.</h3>
<p>Allowed Methods are: {$methods}</p>
HTML;
$this->response()
->clearBody()
->status(405)
->setHeader('Allow', $methods)
->write($output)
->send();
});
Розширене використання
Впровадження залежностей у маршрутах
Якщо ви хочете використовувати впровадження залежностей через контейнер (PSR-11, PHP-DI, Dice тощо), то це доступно лише для таких типів маршрутів: або ви самостійно створюєте об’єкт і використовуєте контейнер для його створення, або використовуєте рядки для визначення класу та методу, який потрібно викликати. Більше інформації ви можете знайти на сторінці Впровадження залежностей.
Ось швидкий приклад:
use flight\database\SimplePdo;
// Greeting.php
class Greeting
{
protected SimplePdo $db;
public function __construct(SimplePdo $db) {
$this->db = $db;
}
public function hello(int $id) {
// зробити щось із $this->db
$name = $this->db->fetchField("SELECT name FROM users WHERE id = ?", [ $id ]);
echo "Hello, world! My name is {$name}!";
}
}
// index.php
// Налаштуйте контейнер із необхідними параметрами
// Дивіться сторінку Впровадження залежностей для отримання додаткової інформації про PSR-11
$dice = new \Dice\Dice();
// Не забудьте переназначити змінну з '$dice = '!!!!!
$dice = $dice->addRule(SimplePdo::class, [
'shared' => true,
'constructParams' => [
'mysql:host=localhost;dbname=test',
'root',
'password'
]
]);
// Зареєструйте обробник контейнера
Flight::registerContainerHandler(function($class, $params) use ($dice) {
return $dice->create($class, $params);
});
// Маршрути як зазвичай
Flight::route('/hello/@id', [ 'Greeting', 'hello' ]);
// або
Flight::route('/hello/@id', 'Greeting->hello');
// або
Flight::route('/hello/@id', 'Greeting::hello');
Flight::start();
Передача виконання наступному маршруту
Застаріло
Ви можете передати виконання наступному відповідному маршруту, повернувши true зі своєї
функції зворотного виклику.
Flight::route('/user/@name', function (string $name) {
// Перевірити певну умову
if ($name !== "Bob") {
// Перейти до наступного маршруту
return true;
}
});
Flight::route('/user/*', function () {
// Цей маршрут буде викликано
});
Тепер для складних випадків, подібних до цього, рекомендується використовувати мідлвару.
Псевдоніми маршрутів
Призначивши маршруту псевдонім, ви можете пізніше динамічно викликати цей псевдонім у своєму застосунку для генерації URL (наприклад, посилання у HTML-шаблоні або для створення URL-адреси перенаправлення).
Flight::route('/users/@id', function($id) { echo 'user:'.$id; }, false, 'user_view');
// або
Flight::route('/users/@id', function($id) { echo 'user:'.$id; })->setAlias('user_view');
// пізніше десь у коді
class UserController {
public function update() {
// код для збереження користувача...
$id = $user['id']; // наприклад, 5
$redirectUrl = Flight::getUrl('user_view', [ 'id' => $id ]); // поверне '/users/5'
Flight::redirect($redirectUrl);
}
}
Це особливо корисно, якщо ваш URL змінюється. У наведеному вище прикладі, скажімо, користувачів перенесли на /admin/users/@id.
Завдяки псевдоніму маршруту вам більше не потрібно шукати всі старі URL у коді та змінювати їх, оскільки псевдонім тепер повертатиме /admin/users/5, як у прикладі вище.
Псевдоніми маршрутів також працюють у групах:
Flight::group('/users', function() {
Flight::route('/@id', function($id) { echo 'user:'.$id; }, false, 'user_view');
// або
Flight::route('/@id', function($id) { echo 'user:'.$id; })->setAlias('user_view');
});
Перегляд інформації про маршрут
Якщо ви хочете переглянути інформацію про відповідний маршрут, є два способи:
- Використати властивість
executedRouteна об’єктіFlight::router(). - Попросити передати об’єкт маршруту у ваш зворотний виклик, передавши
trueтретім параметром у методі маршруту. Об’єкт маршруту завжди буде останнім параметром, переданим у вашу функцію зворотного виклику.
executedRoute
Flight::route('/', function() {
$route = Flight::router()->executedRoute;
// Зробити щось із $route
// Масив HTTP-методів, з якими зіставлено маршрут
$route->methods;
// Масив іменованих параметрів
$route->params;
// Відповідний регулярний вираз
$route->regex;
// Містить вміст будь-якого '*', використаного у шаблоні URL
$route->splat;
// Показує шлях URL... якщо вам справді це потрібно
$route->pattern;
// Показує, яка мідлвара призначена цьому маршруту
$route->middleware;
// Показує псевдонім, призначений цьому маршруту
$route->alias;
});
Примітка: Властивість
executedRouteбуде встановлена лише після виконання маршруту. Якщо спробувати отримати до неї доступ до виконання маршруту, вона будеNULL. Ви також можете використовувати executedRoute у мідлварі!
Передача true у визначення маршруту
Flight::route('/', function(\flight\net\Route $route) {
// Масив HTTP-методів, з якими зіставлено маршрут
$route->methods;
// Масив іменованих параметрів
$route->params;
// Відповідний регулярний вираз
$route->regex;
// Містить вміст будь-якого '*', використаного у шаблоні URL
$route->splat;
// Показує шлях URL... якщо вам справді це потрібно
$route->pattern;
// Показує, яка мідлвара призначена цьому маршруту
$route->middleware;
// Показує псевдонім, призначений цьому маршруту
$route->alias;
}, true);// <-- Цей параметр true робить це можливим
Групування маршрутів і мідлвара
Іноді потрібно згрупувати пов’язані маршрути (наприклад, /api/v1).
Це можна зробити за допомогою методу group:
Flight::group('/api/v1', function () {
Flight::route('/users', function () {
// Відповідає /api/v1/users
});
Flight::route('/posts', function () {
// Відповідає /api/v1/posts
});
});
Можна навіть вкладати групи в групи:
Flight::group('/api', function () {
Flight::group('/v1', function () {
// Flight::get() отримує змінні, він не встановлює маршрут! Дивіться контекст об’єкта нижче
Flight::route('GET /users', function () {
// Відповідає GET /api/v1/users
});
Flight::post('/posts', function () {
// Відповідає POST /api/v1/posts
});
Flight::put('/posts/1', function () {
// Відповідає PUT /api/v1/posts
});
});
Flight::group('/v2', function () {
// Flight::get() отримує змінні, він не встановлює маршрут! Дивіться контекст об’єкта нижче
Flight::route('GET /users', function () {
// Відповідає GET /api/v2/users
});
});
});
Групування з контекстом об’єкта
Ви також можете використовувати групування маршрутів з об’єктом Engine таким чином:
$app = Flight::app();
$app->group('/api/v1', function (Router $router) {
// використовуйте змінну $router
$router->get('/users', function () {
// Відповідає GET /api/v1/users
});
$router->post('/posts', function () {
// Відповідає POST /api/v1/posts
});
});
Примітка: Це рекомендований спосіб визначення маршрутів і груп з об’єктом
$router.
Групування з мідлварою
Ви також можете призначити мідлвару групі маршрутів:
Flight::group('/api/v1', function () {
Flight::route('/users', function () {
// Відповідає /api/v1/users
});
}, [ MyAuthMiddleware::class ]); // або [ new MyAuthMiddleware() ], якщо ви хочете використати екземпляр
Більше деталей на сторінці групова мідлвара.
Ресурсна маршрутизація
Ви можете створити набір маршрутів для ресурсу за допомогою методу resource. Це створить
набір маршрутів для ресурсу, що відповідає RESTful-конвенціям.
Щоб створити ресурс, зробіть наступне:
Flight::resource('/users', UsersController::class);
У фоновому режимі буде створено такі маршрути:
[
'index' => 'GET /users',
'create' => 'GET /users/create',
'store' => 'POST /users',
'show' => 'GET /users/@id',
'edit' => 'GET /users/@id/edit',
'update' => 'PUT /users/@id',
'destroy' => 'DELETE /users/@id'
]
А ваш контролер використовуватиме наступні методи:
class UsersController
{
public function index(): void
{
}
public function show(string $id): void
{
}
public function create(): void
{
}
public function store(): void
{
}
public function edit(string $id): void
{
}
public function update(string $id): void
{
}
public function destroy(string $id): void
{
}
}
Примітка: Ви можете переглянути новостворені маршрути за допомогою
runway, виконавшиphp runway routes.
Налаштування ресурсних маршрутів
Є кілька опцій для конфігурування ресурсних маршрутів.
База псевдоніма
Ви можете налаштувати aliasBase. За замовчуванням псевдонім — це остання частина вказаного URL.
Наприклад, /users/ дасть aliasBase зі значенням users. Коли ці маршрути створюються,
псевдоніми будуть users.index, users.create тощо. Якщо ви хочете змінити псевдонім, встановіть aliasBase
на потрібне значення.
Flight::resource('/users', UsersController::class, [ 'aliasBase' => 'user' ]);
only та except
Ви також можете вказати, які маршрути потрібно створювати, за допомогою опцій only та except.
// Білий список лише цих методів, решта — у чорному списку
Flight::resource('/users', UsersController::class, [ 'only' => [ 'index', 'show' ] ]);
// Чорний список лише цих методів, решта — у білому списку
Flight::resource('/users', UsersController::class, [ 'except' => [ 'create', 'store', 'edit', 'update', 'destroy' ] ]);
По суті, це опції білого та чорного списків, тож ви можете вказати, які маршрути створювати.
Мідлвара
Ви також можете вказати мідлвару, яка буде виконуватися для кожного маршруту, створеного методом resource.
Flight::resource('/users', UsersController::class, [ 'middleware' => [ MyAuthMiddleware::class ] ]);
Потокові відповіді
Тепер ви можете передавати відповіді клієнту потоково за допомогою stream() або streamWithHeaders().
Це корисно для надсилання великих файлів, довготривалих процесів або генерації великих відповідей.
Потокова передача маршруту обробляється дещо інакше, ніж звичайного маршруту.
Примітка: Потокові відповіді доступні лише тоді, коли
flight.v2.output_bufferingвстановлено уfalse.
Потік із ручними заголовками
Ви можете передавати відповідь клієнту потоково за допомогою методу stream() на маршруті. Якщо ви
це робите, ви повинні вручну встановити всі заголовки перед тим, як виводити щось клієнту.
Це робиться за допомогою PHP-функції header() або методу Flight::response()->setRealHeader().
Flight::route('/@filename', function($filename) {
$response = Flight::response();
// очевидно, ви маєте очистити шлях тощо.
$fileNameSafe = basename($filename);
// Якщо вам потрібно встановити додаткові заголовки після виконання маршруту,
// ви повинні визначити їх до того, як щось буде виведено.
// Вони мають бути необробленим викликом функції header() або
// викликом Flight::response()->setRealHeader()
header('Content-Disposition: attachment; filename="'.$fileNameSafe.'"');
// або
$response->setRealHeader('Content-Disposition: attachment; filename="'.$fileNameSafe.'"');
$filePath = '/some/path/to/files/'.$fileNameSafe;
if (!is_readable($filePath)) {
Flight::halt(404, 'File not found');
}
// вручну встановіть довжину вмісту, якщо бажаєте
header('Content-Length: '.filesize($filePath));
// або
$response->setRealHeader('Content-Length: '.filesize($filePath));
// Потоково передайте файл клієнту, читаючи його
readfile($filePath);
// Це магічний рядок
})->stream();
Потік із заголовками
Ви також можете використовувати метод streamWithHeaders(), щоб встановити заголовки перед початком потокової передачі.
Flight::route('/stream-users', function() {
// тут ви можете додати будь-які додаткові заголовки
// ви просто повинні використовувати header() або Flight::response()->setRealHeader()
// однак ви отримуєте свої дані, наприклад...
$users_stmt = Flight::db()->query("SELECT id, first_name, last_name FROM users");
echo '{';
$user_count = count($users);
while($user = $users_stmt->fetch(PDO::FETCH_ASSOC)) {
echo json_encode($user);
if(--$user_count > 0) {
echo ',';
}
// Це необхідно для надсилання даних клієнту
ob_flush();
}
echo '}';
// Ось так ви встановите заголовки перед початком потокової передачі.
})->streamWithHeaders([
'Content-Type' => 'application/json',
'Content-Disposition' => 'attachment; filename="users.json"',
// необов’язковий код статусу, за замовчуванням 200
'status' => 200
]);
Дивіться також
- Мідлвара — використання мідлвари з маршрутами для автентифікації, журналювання тощо.
- Впровадження залежностей — спрощення створення об’єктів та керування ними у маршрутах.
- Чому фреймворк? — розуміння переваг використання фреймворку, такого як Flight.
- Розширення — як розширити Flight власними функціями, зокрема методом
notFound. - php.net: preg_match — PHP-функція для зіставлення регулярних виразів.
Усунення неполадок
- Параметри маршруту зіставляються за порядком, а не за ім’ям. Переконайтеся, що порядок параметрів зворотного виклику відповідає визначенню маршруту.
- Використання
Flight::get()не визначає маршрут; для маршрутизації використовуйтеFlight::route('GET /...')або контекст об’єкта Router у групах (напр.,$router->get(...)). - Властивість executedRoute встановлюється лише після виконання маршруту; до цього вона дорівнює NULL.
- Потокова передача потребує вимкнення застарілої функціональності вихідного буферизації Flight (
flight.v2.output_buffering = false). - Для впровадження залежностей лише певні визначення маршрутів підтримують створення через контейнер.
404 Not Found або неочікувана поведінка маршруту
Якщо ви бачите помилку 404 Not Found (але ви присягаєтеся, що маршрут справді існує, і це не помилка), насправді це може бути проблемою з тим, що ви повертаєте значення у кінцевій точці маршруту, а не просто виводите його. Причина цього навмисна, але може стати несподіванкою для деяких розробників.
Flight::route('/hello', function(){
// Це може спричинити помилку 404 Not Found
return 'Hello World';
});
// Ймовірно, ви хочете так
Flight::route('/hello', function(){
echo 'Hello World';
});
Причина в тому, що в маршрутизатор вбудовано спеціальний механізм, який трактує повернений результат як сигнал «перейти до наступного маршруту». Цю поведінку задокументовано в розділі Маршрутизація.
Журнал змін
- v3: Додано ресурсну маршрутизацію, псевдоніми маршрутів, підтримку потокової передачі, групи маршрутів та підтримку мідлвари.
- v1: Доступна переважна більшість базових функцій.
Learn/learn
Дізнайтеся про Flight
Flight — це швидкий, простий, розширюваний фреймворк для PHP. Він доволі універсальний і може використовуватися для створення будь-яких вебзастосунків. Він створений із простотою в основі та написаний так, щоб його було легко зрозуміти й використовувати — як людям, так і AI-помічникам із кодування.
Примітка: Ви побачите приклади, які використовують
Flight::як статичну змінну, а також приклади з об'єктом Engine$app->. Обидва підходи взаємозамінні.$appі$this->appу контролері/мідлварі — це рекомендований підхід команди Flight (і саме його стандартизують офіційний скелет іAGENTS.mdдля нових проєктів).
Основні компоненти
Маршрутизація
Дізнайтеся, як керувати маршрутами для вашого вебзастосунку. Це також охоплює групування маршрутів, параметри маршрутів і мідлвар.
Мідлвар
Дізнайтеся, як використовувати мідлвар для фільтрації запитів і відповідей у вашому застосунку.
Автозавантаження
Дізнайтеся, як автозавантажувати власні класи. Регістр літер у назвах тек має відповідати вашим неймспейсам — скелет використовує App\ і папки в PascalCase, наприклад app/Controller/.
Запити
Дізнайтеся, як обробляти запити та відповіді у вашому застосунку.
Відповіді
Дізнайтеся, як надсилати відповіді вашим користувачам.
HTML-шаблони
Дізнайтеся, як рендерити HTML за допомогою Twig (типовий для скелета), Latte або інших рушіїв — не лише вбудованих PHP-представлень.
Безпека
Дізнайтеся, як захистити ваш застосунок від поширених загроз безпеки.
Конфігурація
Дізнайтеся, як налаштувати фреймворк для вашого застосунку.
Менеджер подій
Дізнайтеся, як використовувати систему подій для додавання власних подій у ваш застосунок.
Розширення Flight
Дізнайтеся, як розширювати фреймворк, додаючи власні методи та класи.
Хуки методів і фільтрація
Дізнайтеся, як додавати хуки подій до ваших методів і внутрішніх методів фреймворку.
Контейнер впровадження залежностей (DIC)
Дізнайтеся, як використовувати контейнери впровадження залежностей (DIC) для керування залежностями вашого застосунку.
Допоміжні класи
Колекції
Колекції використовуються для зберігання даних і доступу до них як до масиву або об'єкта для зручності.
JSON-обгортка
Це кілька простих функцій, які роблять кодування та декодування JSON узгодженим.
SimplePdo
PDO іноді може створювати більше проблем, ніж потрібно. SimplePdo — це сучасний допоміжний клас для PDO зі зручними методами, такими як insert(), update(), delete() і transaction(), що значно спрощує роботу з базою даних.
PdoWrapper (застарілий)
Оригінальна PDO-обгортка є застарілою, починаючи з v3.18.0. Будь ласка, замість неї використовуйте SimplePdo.
Обробник завантажених файлів
Простий клас, який допомагає керувати завантаженими файлами та переміщувати їх у постійне місце зберігання.
Важливі концепції
Чому фреймворк?
Ось коротка стаття про те, чому варто використовувати фреймворк. Перш ніж почати користуватися фреймворком, варто зрозуміти його переваги.
Крім того, чудовий підручник створив @lubiana. Хоча він не заглиблюється в деталі саме Flight, цей посібник допоможе вам зрозуміти основні концепції, пов'язані з фреймворками, і чому їх варто використовувати. Ви можете знайти підручник тут.
Flight порівняно з іншими фреймворками
Якщо ви переходите з іншого фреймворка, такого як Laravel, Slim, Fat-Free або Symfony, на Flight, ця сторінка допоможе вам зрозуміти відмінності між ними.
Інші теми
Юніт-тестування
Дотримуйтеся цього посібника, щоб навчитися тестувати ваш Flight-код, щоб він був надійним.
AI і досвід розробника
Flight створений для роботи з LLM-помічниками з кодування: AGENTS.md, команди Runway ai:* і чітке розташування скелета, щоб агенти залишалися в межах шаблону.
Міграція v2 -> v3
Зворотну сумісність здебільшого збережено, але є деякі зміни, про які слід знати під час переходу з v2 на v3.
Learn/unit_testing
Модульне тестування
Огляд
Модульне тестування у Flight допомагає вам переконатися, що ваш застосунок поводиться очікувано, виявляти помилки на ранніх етапах і полегшувати підтримку вашої кодової бази. Flight розроблено для бездоганної роботи з PHPUnit — найпопулярнішим фреймворком для тестування PHP.
Розуміння
Модульні тести перевіряють поведінку невеликих частин вашого застосунку (як-от контролери чи сервіси) ізольовано. У Flight це означає перевірку того, як ваші маршрути, контролери та логіка реагують на різні вхідні дані — без залежності від глобального стану чи реальних зовнішніх сервісів.
Ключові принципи:
- Тестуйте поведінку, а не реалізацію: Зосереджуйтеся на тому, що робить ваш код, а не на тому, як він це робить.
- Уникайте глобального стану: Використовуйте впровадження залежностей замість
Flight::set()абоFlight::get(). - Імітуйте зовнішні сервіси: Замінюйте такі речі, як бази даних або поштові сервіси, тестовими дублерами.
- Тримайте тести швидкими та зосередженими: Модульні тести не повинні звертатися до реальних баз даних чи API.
Базове використання
Налаштування PHPUnit
- Встановіть PHPUnit за допомогою Composer:
composer require --dev phpunit/phpunit - Створіть каталог
testsу корені вашого проєкту. - Додайте тестовий скрипт до вашого
composer.json:"scripts": { "test": "phpunit --configuration phpunit.xml" } - Створіть файл
phpunit.xml:<?xml version="1.0" encoding="UTF-8"?> <phpunit bootstrap="vendor/autoload.php"> <testsuites> <testsuite name="Flight Tests"> <directory>tests</directory> </testsuite> </testsuites> </phpunit>
Тепер ви можете запускати свої тести за допомогою composer test.
Тестування простого обробника маршруту
Припустимо, у вас є маршрут, який перевіряє електронну пошту:
// index.php
$app->route('POST /register', [ UserController::class, 'register' ]);
// UserController.php
class UserController {
protected $app;
public function __construct(flight\Engine $app) {
$this->app = $app;
}
public function register() {
$email = $this->app->request()->data->email;
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
return $this->app->json(['status' => 'error', 'message' => 'Invalid email']);
}
return $this->app->json(['status' => 'success', 'message' => 'Valid email']);
}
}
Простий тест для цього контролера:
use PHPUnit\Framework\TestCase;
use flight\Engine;
class UserControllerTest extends TestCase {
public function testValidEmailReturnsSuccess() {
$app = new Engine();
$app->request()->data->email = 'test@example.com';
$controller = new UserController($app);
$controller->register();
$response = $app->response()->getBody();
$output = json_decode($response, true);
$this->assertEquals('success', $output['status']);
$this->assertEquals('Valid email', $output['message']);
}
public function testInvalidEmailReturnsError() {
$app = new Engine();
$app->request()->data->email = 'invalid-email';
$controller = new UserController($app);
$controller->register();
$response = $app->response()->getBody();
$output = json_decode($response, true);
$this->assertEquals('error', $output['status']);
$this->assertEquals('Invalid email', $output['message']);
}
}
Поради:
- Імітуйте POST-дані за допомогою
$app->request()->data. - Уникайте використання статичних
Flight::у тестах — використовуйте екземпляр$app.
Використання впровадження залежностей для тестованих контролерів
Впроваджуйте залежності (наприклад, базу даних або поштовий сервіс) у ваші контролери, щоб їх було легко імітувати в тестах:
use flight\database\SimplePdo;
class UserController {
protected $app;
protected $db;
protected $mailer;
public function __construct($app, $db, $mailer) {
$this->app = $app;
$this->db = $db;
$this->mailer = $mailer;
}
public function register() {
$email = $this->app->request()->data->email;
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
return $this->app->json(['status' => 'error', 'message' => 'Invalid email']);
}
$this->db->runQuery('INSERT INTO users (email) VALUES (?)', [$email]);
$this->mailer->sendWelcome($email);
return $this->app->json(['status' => 'success', 'message' => 'User registered']);
}
}
І тест із тестовими дублерами:
use PHPUnit\Framework\TestCase;
class UserControllerDICTest extends TestCase {
public function testValidEmailSavesAndSendsEmail() {
$mockDb = $this->createMock(flight\database\SimplePdo::class);
$mockDb->method('runQuery')->willReturn(true);
$mockMailer = new class {
public $sentEmail = null;
public function sendWelcome($email) { $this->sentEmail = $email; return true; }
};
$app = new flight\Engine();
$app->request()->data->email = 'test@example.com';
$controller = new UserController($app, $mockDb, $mockMailer);
$controller->register();
$response = $app->response()->getBody();
$result = json_decode($response, true);
$this->assertEquals('success', $result['status']);
$this->assertEquals('User registered', $result['message']);
$this->assertEquals('test@example.com', $mockMailer->sentEmail);
}
}
Просунуте використання
- Створення тестових дублерів: Використовуйте вбудовані тестові дублери PHPUnit або анонімні класи для заміни залежностей.
- Тестування контролерів безпосередньо: Створюйте контролери з новим
Engineта імітуйте залежності. - Не перестарайтеся з імітацією: Дайте реальній логіці виконуватися там, де це можливо; імітуйте лише зовнішні сервіси.
Дивіться також
- Посібник з модульного тестування — вичерпний посібник із найкращих практик модульного тестування.
- Контейнер впровадження залежностей — як використовувати DIC для керування залежностями та покращення тестованості.
- Розширення — як додавати власні помічники або перевизначати основні класи.
- SimplePdo — спрощує взаємодію з базою даних і легше імітується в тестах.
- Запити — обробка HTTP-запитів у Flight.
- Відповіді — надсилання відповідей користувачам.
- Модульне тестування та принципи SOLID — дізнайтеся, як принципи SOLID можуть покращити ваші модульні тести.
Усунення неполадок
- Уникайте використання глобального стану (
Flight::set(),$_SESSIONтощо) у вашому коді та тестах. - Якщо ваші тести повільні, можливо, ви пишете інтеграційні тести — імітуйте зовнішні сервіси, щоб модульні тести залишалися швидкими.
- Якщо налаштування тестів складне, розгляньте можливість рефакторингу коду для використання впровадження залежностей.
Журнал змін
- v3.15.0 — додано приклади для впровадження залежностей та створення тестових дублерів.
Learn/flight_vs_symfony
Flight vs Symfony
Що таке Symfony?
Symfony — це набір повторно використовуваних компонентів PHP і PHP фреймворк для веб-проектів.
Стандартна основа, на якій побудовані найкращі PHP додатки. Виберіть будь-який з 50 автономних компонентів, доступних для ваших власних додатків.
Прискорте створення та підтримку ваших PHP веб-додатків. Припиніть повторювані завдання кодування і насолоджуйтесь можливістю контролювати свій код.
Плюси в порівнянні з Flight
- Symfony має великий екосистему розробників і модулів, які можна використовувати для вирішення поширених проблем.
- Symfony має повнофункціональний ORM (Doctrine), який можна використовувати для взаємодії з вашою базою даних.
- Symfony має велику кількість документації та навчальних посібників, які можна використовувати для вивчення фреймворку.
- Symfony має подкасти, конференції, зустрічі, відео та інші ресурси, які можна використовувати для вивчення фреймворку.
- Symfony орієнтований на досвідченого розробника, який прагне створити повнофункціональний, корпоративний веб-додаток.
Мінуси в порівнянні з Flight
- Symfony має набагато більше в роботі під капотом, ніж Flight. Це має драматичну ціну з точки зору продуктивності. Дивіться TechEmpower benchmarks для отримання додаткової інформації.
- Flight орієнтований на розробника, який прагне створити легкий, швидкий і простий у використанні веб-додаток.
- Flight спрямований на простоту і зручність використання.
- Одна з основних функцій Flight полягає в тому, що вона намагається підтримувати зворотну сумісність.
- Flight не має залежностей, тоді як Symfony має безліч залежностей.
- Flight призначений для розробників, які вперше намагаються впровадити фреймворки.
- Flight також може виконувати корпоративні додатки, але у нього немає стільки ж прикладів і навчальних посібників, як у Symfony. Це також вимагатиме більше дисципліни з боку розробника для підтримки організованості та структурованості.
- Flight надає розробнику більше контролю над додатком, тоді як Symfony може непомітно включати деяку магію за лаштунками.
Learn/flight_vs_another_framework
Порівняння Flight з іншим фреймворком
Якщо ви мігруєте з іншого фреймворка, такого як Laravel, Slim, Fat-Free або Symfony до Flight, ця сторінка допоможе вам зрозуміти відмінності між цими двома.
Laravel
Laravel — це повнофункціональний фреймворк, який має всі принади та дивовижну екосистему, орієнтовану на розробників, але вартість цього — продуктивність і складність.
Дивіться порівняння між Laravel і Flight.
Slim
Slim — це мікрофреймворк, схожий на Flight. Він розроблений для того, щоб бути легким і простим у використанні, але може бути трохи складнішим, ніж Flight.
Дивіться порівняння між Slim і Flight.
Fat-Free
Fat-Free — це повноцінний фреймворк у набагато меншому пакеті. Хоча в ньому є всі інструменти в ящику, він має архітектуру даних, яка може ускладнити деякі проекти більше, ніж потрібно.
Дивіться порівняння між Fat-Free і Flight.
Symfony
Symfony — це модульний фреймворк корпоративного рівня, який розроблений для того, щоб бути гнучким і масштабованим. Для менших проектів або нових розробників Symfony може бути трохи приголомшуючим.
Дивіться порівняння між Symfony і Flight.
Learn/pdo_wrapper
PdoWrapper Клас-помічник PDO
ПОПЕРЕДЖЕННЯ
Застарілий:
PdoWrapperє застарілим з версії Flight v3.18.0. Він не буде видалений у майбутніх версіях, але буде підтримуватися для зворотної сумісності. Будь ласка, використовуйте SimplePdo замість нього, який пропонує ту ж функціональність плюс додаткові допоміжні методи для поширених операцій з базою даних.
Огляд
Клас PdoWrapper у Flight є дружнім помічником для роботи з базами даних за допомогою PDO. Він спрощує поширені завдання з базами даних, додає деякі зручні методи для отримання результатів і повертає результати як Collections для легкого доступу. Він також підтримує логування запитів і моніторинг продуктивності додатків (APM) для просунутих випадків використання.
Розуміння
Робота з базами даних у PHP може бути дещо багатослівною, особливо при прямому використанні PDO. PdoWrapper розширює PDO і додає методи, які роблять запитування, отримання та обробку результатів набагато простішими. Замість жонглювання підготовленими виразами та режимами отримання, ви отримуєте прості методи для поширених завдань, і кожен рядок повертається як Collection, тому ви можете використовувати нотацію масиву або об'єкта.
Ви можете зареєструвати PdoWrapper як спільну послугу в Flight, а потім використовувати його будь-де у вашому додатку за допомогою Flight::db().
Основне використання
Реєстрація помічника PDO
Спочатку зареєструйте клас PdoWrapper у Flight:
Flight::register('db', \flight\database\PdoWrapper::class, [
'mysql:host=localhost;dbname=cool_db_name', 'user', 'pass', [
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'utf8mb4\'',
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_STRINGIFY_FETCHES => false,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
]
]);
Тепер ви можете використовувати Flight::db() будь-де, щоб отримати з'єднання з базою даних.
Виконання запитів
runQuery()
function runQuery(string $sql, array $params = []): PDOStatement
Використовуйте це для INSERT, UPDATE або коли ви хочете отримати результати вручну:
$db = Flight::db();
$statement = $db->runQuery("SELECT * FROM users WHERE status = ?", ['active']);
while ($row = $statement->fetch()) {
// $row є масивом
}
Ви також можете використовувати його для записів:
$db->runQuery("INSERT INTO users (name) VALUES (?)", ['Alice']);
$db->runQuery("UPDATE users SET name = ? WHERE id = ?", ['Bob', 1]);
fetchField()
function fetchField(string $sql, array $params = []): mixed
Отримайте одне значення з бази даних:
$count = Flight::db()->fetchField("SELECT COUNT(*) FROM users WHERE status = ?", ['active']);
fetchRow()
function fetchRow(string $sql, array $params = []): Collection
Отримайте один рядок як Collection (доступ як до масиву/об'єкта):
$user = Flight::db()->fetchRow("SELECT * FROM users WHERE id = ?", [123]);
echo $user['name'];
// або
echo $user->name;
fetchAll()
function fetchAll(string $sql, array $params = []): array<Collection>
Отримайте всі рядки як масив Collections:
$users = Flight::db()->fetchAll("SELECT * FROM users WHERE status = ?", ['active']);
foreach ($users as $user) {
echo $user['name'];
// або
echo $user->name;
}
Використання заповнювачів IN()
Ви можете використовувати єдиний ? у клаузі IN() і передати масив або рядок, розділений комами:
$ids = [1, 2, 3];
$users = Flight::db()->fetchAll("SELECT * FROM users WHERE id IN (?)", [$ids]);
// або
$users = Flight::db()->fetchAll("SELECT * FROM users WHERE id IN (?)", ['1,2,3']);
Просунуте використання
Логування запитів та APM
Якщо ви хочете відстежувати продуктивність запитів, увімкніть відстеження APM під час реєстрації:
Flight::register('db', \flight\database\PdoWrapper::class, [
'mysql:host=localhost;dbname=cool_db_name', 'user', 'pass', [/* options */], true // останній параметр увімкнює APM
]);
Після виконання запитів ви можете логувати їх вручну, але APM логуватиме їх автоматично, якщо увімкнено:
Flight::db()->logQueries();
Це викличе подію (flight.db.queries) з метриками з'єднання та запитів, яку ви можете прослуховувати за допомогою системи подій Flight.
Повний приклад
Flight::route('/users', function () {
// Отримайте всіх користувачів
$users = Flight::db()->fetchAll('SELECT * FROM users');
// Потоково отримайте всіх користувачів
$statement = Flight::db()->runQuery('SELECT * FROM users');
while ($user = $statement->fetch()) {
echo $user['name'];
}
// Отримайте одного користувача
$user = Flight::db()->fetchRow('SELECT * FROM users WHERE id = ?', [123]);
// Отримайте одне значення
$count = Flight::db()->fetchField('SELECT COUNT(*) FROM users');
// Спеціальний синтаксис IN()
$users = Flight::db()->fetchAll('SELECT * FROM users WHERE id IN (?)', [[1,2,3,4,5]]);
$users = Flight::db()->fetchAll('SELECT * FROM users WHERE id IN (?)', ['1,2,3,4,5']);
// Вставте нового користувача
Flight::db()->runQuery("INSERT INTO users (name, email) VALUES (?, ?)", ['Bob', 'bob@example.com']);
$insert_id = Flight::db()->lastInsertId();
// Оновіть користувача
Flight::db()->runQuery("UPDATE users SET name = ? WHERE id = ?", ['Bob', 123]);
// Видаліть користувача
Flight::db()->runQuery("DELETE FROM users WHERE id = ?", [123]);
// Отримайте кількість уражених рядків
$statement = Flight::db()->runQuery("UPDATE users SET name = ? WHERE name = ?", ['Bob', 'Sally']);
$affected_rows = $statement->rowCount();
});
Дивіться також
- Collections - Дізнайтеся, як використовувати клас Collection для легкого доступу до даних.
Вирішення проблем
- Якщо ви отримуєте помилку про з'єднання з базою даних, перевірте ваш DSN, ім'я користувача, пароль та опції.
- Усі рядки повертаються як Collections — якщо вам потрібен звичайний масив, використовуйте
$collection->getData(). - Для запитів
IN (?)переконайтеся, що ви передаєте масив або рядок, розділений комами.
Журнал змін
- v3.2.0 - Початковий реліз PdoWrapper з базовими методами запитів та отримання.
Learn/dependency_injection_container
Контейнер впровадження залежностей
Огляд
Контейнер впровадження залежностей (DIC) — це потужне розширення, яке дозволяє керувати залежностями вашого застосунку. Це також одна з найбільших причин, чому Flight добре працює з AI-інструментами кодування та модульними тестами: контролери отримують те, що їм потрібно, через конструктор, замість того щоб звертатися до глобальних змінних.
Розуміння
Впровадження залежностей (DI) — це ключова концепція сучасних PHP-фреймворків, яка використовується для керування створенням та конфігурацією об'єктів. Деякі приклади бібліотек DIC: flightphp/container, Dice, Pimple, PHP-DI та league/container.
DIC — це вигадливий спосіб створення та керування вашими класами в централізованому місці. Це корисно, коли вам потрібно передати один і той самий об'єкт кільком класам (контролерам, проміжному ПЗ, командам тощо).
Офіційний flightphp/skeleton під'єднує Dice у app/config/services.php, підставляє спільний екземпляр flight\Engine та розв'язує цілі маршрутів як-от [App\Controller\HomeController::class, 'index']. Віддавайте перевагу цьому патерну для нових проєктів, щоб люди та агенти редагували одні й ті самі місця.
Базове використання
Старий спосіб може виглядати так:
require 'vendor/autoload.php';
// клас для керування користувачами з бази даних
class UserController {
protected PDO $pdo;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
public function view(int $id) {
$stmt = $this->pdo->prepare('SELECT * FROM users WHERE id = :id');
$stmt->execute(['id' => $id]);
print_r($stmt->fetch());
}
}
// у вашому файлі routes.php
$db = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');
$UserController = new UserController($db);
Flight::route('/user/@id', [ $UserController, 'view' ]);
// інші маршрути UserController...
Flight::start();
З наведеного вище коду видно, що ми створюємо новий об'єкт PDO і передаємо його
нашому класу UserController. Це нормально для невеликого застосунку, але в міру
зростання вашого застосунку ви виявите, що створюєте або передаєте той самий об'єкт PDO
в кількох місцях. Ось тут і стає в пригоді DIC.
Ось той самий приклад із використанням DIC (з Dice):
require 'vendor/autoload.php';
// той самий клас, що й вище. Нічого не змінилося
class UserController {
protected PDO $pdo;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
public function view(int $id) {
$stmt = $this->pdo->prepare('SELECT * FROM users WHERE id = :id');
$stmt->execute(['id' => $id]);
print_r($stmt->fetch());
}
}
// створюємо новий контейнер
$container = new \Dice\Dice;
// додаємо правило, щоб розповісти контейнеру, як створити об'єкт PDO
// не забудьте переназначити його самому собі, як показано нижче!
$container = $container->addRule('PDO', [
// shared означає, що щоразу повертатиметься той самий об'єкт
'shared' => true,
'constructParams' => ['mysql:host=localhost;dbname=test', 'user', 'pass' ]
]);
// Це реєструє обробник контейнера, щоб Flight знав, як його використовувати.
Flight::registerContainerHandler(function($class, $params) use ($container) {
return $container->create($class, $params);
});
// тепер ми можемо використовувати контейнер для створення нашого UserController
Flight::route('/user/@id', [ UserController::class, 'view' ]);
Flight::start();
Гадаю, ви можете подумати, що в приклад було додано багато зайвого коду.
Магія проявляється, коли у вас з'являється інший контролер, якому потрібен об'єкт PDO.
// Якщо всі ваші контролери мають конструктор, якому потрібен об'єкт PDO,
// кожен із маршрутів нижче автоматично отримає його через впровадження!!!
Flight::route('/company/@id', [ CompanyController::class, 'view' ]);
Flight::route('/organization/@id', [ OrganizationController::class, 'view' ]);
Flight::route('/category/@id', [ CategoryController::class, 'view' ]);
Flight::route('/settings', [ SettingsController::class, 'view' ]);
Додаткова перевага використання DIC полягає в тому, що модульне тестування стає набагато простішим. Ви можете створити імітаційний об'єкт і передати його у ваш клас. Це величезна перевага, коли ви пишете тести для свого застосунку — а коли AI-асистент генерує контролер, впровадження через конструктор дає йому чіткий і послідовний патерн для наслідування (посібник із модульного тестування).
Створення централізованого обробника DIC
Ви можете створити централізований обробник DIC у вашому файлі сервісів, розширивши ваш застосунок. Ось приклад:
// services.php
// створюємо новий контейнер
$container = new \Dice\Dice;
// не забудьте переназначити його самому собі, як показано нижче!
$container = $container->addRule('PDO', [
// shared означає, що щоразу повертатиметься той самий об'єкт
'shared' => true,
'constructParams' => ['mysql:host=localhost;dbname=test', 'user', 'pass' ]
]);
// тепер ми можемо створити метод для зіставлення, щоб створювати будь-який об'єкт.
Flight::map('make', function($class, $params = []) use ($container) {
return $container->create($class, $params);
});
// Це реєструє обробник контейнера, щоб Flight знав, як використовувати його для контролерів/проміжного ПЗ
Flight::registerContainerHandler(function($class, $params) {
return Flight::make($class, $params);
});
// припустімо, у нас є такий приклад класу, який приймає об'єкт PDO у конструкторі
class EmailCron {
protected PDO $pdo;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
public function send() {
// код, який надсилає електронний лист
}
}
// І нарешті ви можете створювати об'єкти за допомогою впровадження залежностей
$emailCron = Flight::make(EmailCron::class);
$emailCron->send();
flightphp/container
Flight має плагін, який надає простий PSR-11 сумісний контейнер, який ви можете використовувати для керування впровадженням залежностей. Ось швидкий приклад його використання:
// index.php, наприклад
require 'vendor/autoload.php';
use flight\Container;
$container = new Container;
$container->set(PDO::class, fn(): PDO => new PDO('sqlite::memory:'));
Flight::registerContainerHandler([$container, 'get']);
class TestController {
private PDO $pdo;
function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
function index() {
var_dump($this->pdo);
// виведе це правильно!
}
}
Flight::route('GET /', [TestController::class, 'index']);
Flight::start();
Розширене використання flightphp/container
Ви також можете розв'язувати залежності рекурсивно. Ось приклад:
<?php
require 'vendor/autoload.php';
use flight\Container;
class User {}
interface UserRepository {
function find(int $id): ?User;
}
class PdoUserRepository implements UserRepository {
private PDO $pdo;
function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
function find(int $id): ?User {
// Реалізація ...
return null;
}
}
$container = new Container;
$container->set(PDO::class, static fn(): PDO => new PDO('sqlite::memory:'));
$container->set(UserRepository::class, PdoUserRepository::class);
$userRepository = $container->get(UserRepository::class);
var_dump($userRepository);
/*
object(PdoUserRepository)#4 (1) {
["pdo":"PdoUserRepository":private]=>
object(PDO)#3 (0) {
}
}
*/
DICE
Ви також можете створити власний обробник DIC. Це корисно, якщо у вас є власний контейнер, який ви хочете використовувати і який не є PSR-11 (Dice). Перегляньте розділ базове використання, щоб дізнатися, як це зробити.
Крім того, є кілька корисних налаштувань за замовчуванням, які полегшать вам життя під час роботи з Flight.
Екземпляр Engine (обов'язково для впровадження $app)
Якщо ви вказуєте тип flight\Engine у контролерах або проміжному ПЗ, Dice не повинен створювати новий Engine. Підставте той самий екземпляр із bootstrap-коду. Саме це робить офіційний skeleton, і саме цей патерн очікує AGENTS.md для AI-згенерованих контролерів:
// Десь у вашому bootstrap / services.php
use flight\Engine;
use flight\database\SimplePdo;
$app = Flight::app(); // або $engine = Flight::app();
$container = new \Dice\Dice;
$container = $container->addRule('*', [
'substitutions' => [
// Критично важливо: перевикористовуйте ініціалізований Engine — не дозволяйте Dice робити `new Engine()`
Engine::class => $app,
// Віддавайте перевагу SimplePdo для нового коду
// SimplePdo::class => $db,
// Config::class => $config,
// \Twig\Environment::class => $twig,
]
]);
$app->registerContainerHandler(function ($class, $params) use ($container) {
return $container->create($class, $params);
});
// Додатковий помічник для коду поза маршрутами
$app->map('make', function ($class, $params = []) use ($container) {
return $container->create($class, $params);
});
// app/Controller/MyController.php (структура skeleton — регістр папок відповідає неймспейсу)
namespace App\Controller;
use flight\Engine;
class MyController
{
protected Engine $app;
public function __construct(Engine $app)
{
$this->app = $app;
}
public function index(): void
{
// Жодного фасаду Flight:: у шарі застосунку — легше тестувати та зрозуміліше для AI-інструментів
$this->app->render('welcome', ['message' => 'Hello']);
}
}
// app/config/routes.php
use App\Controller\MyController;
$router->get('/', [MyController::class, 'index']);
Якщо ви пропустите підстановку Engine, Dice може створити другий Engine, і ваш контролер не матиме спільних маршрутів, конфігурації або зіставленого Twig render із bootstrap-коду.
Додавання інших спільних сервісів (SimplePdo, Config, Twig)
use flight\database\SimplePdo;
use flight\Engine;
// Після створення $db, $config, $twig у services.php:
$substitutions = [
Engine::class => $app,
SimplePdo::class => $db,
// App\Utils\Config::class => $config,
// \Twig\Environment::class => $twig,
];
$container = $container->addRule('*', [
'substitutions' => $substitutions,
]);
Тоді контролери можуть приймати SimplePdo $db (або ваш тип конфігурації) у конструкторі й ніколи не викликати Flight::db(). Це відповідає рекомендаціям із модульного тестування та фірмовому стилю skeleton.
Додавання інших класів
Якщо у вас є інші класи, які ви хочете додати до контейнера, з Dice це просто, оскільки контейнер автоматично їх розв'яже. Ось приклад:
$container = new \Dice\Dice;
// Якщо вам не потрібно впроваджувати жодних залежностей у ваші класи,
// вам не потрібно нічого визначати!
Flight::registerContainerHandler(function($class, $params) use ($container) {
return $container->create($class, $params);
});
class MyCustomClass {
public function parseThing() {
return 'thing';
}
}
class UserController {
protected MyCustomClass $MyCustomClass;
public function __construct(MyCustomClass $MyCustomClass) {
$this->MyCustomClass = $MyCustomClass;
}
public function index() {
echo $this->MyCustomClass->parseThing();
}
}
Flight::route('/user', 'UserController->index');
PSR-11
Flight також може використовувати будь-який PSR-11 сумісний контейнер. Це означає, що ви можете використовувати будь-який контейнер, який реалізує інтерфейс PSR-11. Ось приклад використання контейнера League PSR-11:
require 'vendor/autoload.php';
use flight\database\SimplePdo;
// та сама ідея UserController, що й вище, але з типом SimplePdo замість сирого PDO
$container = new \League\Container\Container();
$container->add(UserController::class)->addArgument(SimplePdo::class);
$container->add(SimplePdo::class)
->addArgument('mysql:host=localhost;dbname=test')
->addArgument('user')
->addArgument('pass');
Flight::registerContainerHandler($container);
Flight::route('/user', [ 'UserController', 'view' ]);
Flight::start();
Це може бути трохи багатослівніше, ніж попередній приклад із Dice, але це все одно виконує роботу з тими самими перевагами!
Дивіться також
- Встановлення — Структура skeleton і де знаходиться
services.php. - Автозавантаження — Неймспейси
App\та регістр папок. - Розширення Flight — Дізнайтеся, як додати впровадження залежностей до ваших власних класів, розширивши фреймворк.
- Конфігурація — Дізнайтеся, як налаштувати Flight для вашого застосунку.
- Маршрутизація — Дізнайтеся, як визначати маршрути для вашого застосунку та як впровадження залежностей працює з контролерами.
- Проміжне ПЗ — Дізнайтеся, як створювати проміжне ПЗ для вашого застосунку та як впровадження залежностей працює з проміжним ПЗ.
- Модульне тестування — Чому впровадження через конструктор перевершує глобальні змінні
Flight::. - AI та досвід розробника — Єдиний патерн DI для людей та агентів.
- SimplePdo — Бажаний помічник для роботи з базою даних для впровадження.
Усунення неполадок
- Якщо у вас виникають проблеми з вашим контейнером, переконайтеся, що ви передаєте правильні назви класів до контейнера.
- Контролери, які вказують тип
Engine, але отримують «порожній» застосунок: додайте підстановку Engine (див. вище). Dice не повинен створювати другий Engine черезnew. - Клас не знайдено для
App\Controller\…: перевірте регістр папок уapp/Controller/— див. Автозавантаження. - Обробник повинен повертати створений об'єкт із
registerContainerHandler(не викликайтеFlight::make()безreturn).
Журнал змін
- Документація – Документовано skeleton Dice + підстановки Engine, SimplePdo та структуру
App\Controllerдля AI-дружніх проєктів. - v3.7.0 - Додано можливість реєструвати обробник DIC у Flight.
Learn/middleware
Middleware
Огляд
Flight підтримує middleware для маршрутів та груп маршрутів. Middleware — це частина вашого додатка, де код виконується перед (або після) виклику маршруту. Це чудовий спосіб додати перевірки автентифікації API у ваш код або перевірити, чи має користувач дозвіл на доступ до маршруту.
Розуміння
Middleware може значно спростити ваш додаток. Замість складної спадкування абстрактних класів або перевизначення методів, middleware дозволяє контролювати ваші маршрути, призначаючи власну логіку додатка до них. Ви можете уявити middleware як сендвіч. У вас є хліб ззовні, а потім шари інгредієнтів, як-от салат, помідори, м'ясо та сир. Потім уявіть, що кожен запит — це укус сендвіча, де ви спочатку їсте зовнішні шари та просуваєтеся до серцевини.
Ось візуалізація того, як працює middleware. Потім ми покажемо вам практичний приклад того, як це функціонує.
User request at URL /api ---->
Middleware->before() executed ----->
Callable/method attached to /api executed and response generated ------>
Middleware->after() executed ----->
User receives response from server
І ось практичний приклад:
User navigates to URL /dashboard
LoggedInMiddleware->before() executes
before() checks for valid logged in session
if yes do nothing and continue execution
if no redirect the user to /login
Callable/method attached to /api executed and response generated
LoggedInMiddleware->after() has nothing defined so it lets execution continue
User receives dashboard HTML from server
Порядок виконання
Функції middleware виконуються в порядку, в якому вони додаються до маршруту. Виконання подібне до того, як Slim Framework обробляє це.
Методи before() виконуються в порядку додавання, а методи after() виконуються у зворотному порядку.
Наприклад: Middleware1->before(), Middleware2->before(), Middleware2->after(), Middleware1->after().
Базове використання
Ви можете використовувати middleware як будь-який викличний метод, включаючи анонімну функцію або клас (рекомендовано)
Анонімна функція
Ось простий приклад:
Flight::route('/path', function() { echo ' Here I am!'; })->addMiddleware(function() {
echo 'Middleware first!';
});
Flight::start();
// This will output "Middleware first! Here I am!"
Примітка: При використанні анонімної функції інтерпретується лише метод
before(). Ви не можете визначити поведінкуafter()з анонімним класом.
Використання класів
Middleware можна (і слід) реєструвати як клас. Якщо вам потрібна функціональність "after", ви повинні використовувати клас.
class MyMiddleware {
public function before($params) {
echo 'Middleware first!';
}
public function after($params) {
echo 'Middleware last!';
}
}
$MyMiddleware = new MyMiddleware();
Flight::route('/path', function() { echo ' Here I am! '; })->addMiddleware($MyMiddleware);
// also ->addMiddleware([ $MyMiddleware, $MyMiddleware2 ]);
Flight::start();
// This will display "Middleware first! Here I am! Middleware last!"
Ви також можете просто визначити ім'я класу middleware, і він буде створений екземпляр.
Flight::route('/path', function() { echo ' Here I am! '; })->addMiddleware(MyMiddleware::class);
Примітка: Якщо ви передаєте лише ім'я middleware, воно автоматично буде виконане за допомогою контейнера залежностей, і middleware буде виконано з параметрами, які йому потрібні. Якщо у вас не зареєстровано контейнер залежностей, за замовчуванням буде передано екземпляр
flight\Engineу__construct(Engine $app).
Використання маршрутів з параметрами
Якщо вам потрібні параметри з вашого маршруту, вони будуть передані як єдиний масив до вашої функції middleware. (function($params) { ... } або public function before($params) { ... }). Причина в тому, що ви можете структурувати параметри в групи, і в деяких з цих груп ваші параметри можуть з'являтися в іншому порядку, що зламає функцію middleware через звернення до неправильного параметра. Таким чином, ви можете звертатися до них за ім'ям, а не за позицією.
use flight\Engine;
class RouteSecurityMiddleware {
protected Engine $app;
public function __construct(Engine $app) {
$this->app = $app;
}
public function before(array $params) {
$clientId = $params['clientId'];
// jobId may or may not be passed in
$jobId = $params['jobId'] ?? 0;
// maybe if there's no job ID, you don't need to lookup anything.
if($jobId === 0) {
return;
}
// perform a lookup of some kind in your database
$isValid = !!$this->app->db()->fetchField("SELECT 1 FROM client_jobs WHERE client_id = ? AND job_id = ?", [ $clientId, $jobId ]);
if($isValid !== true) {
$this->app->halt(400, 'You are blocked, muahahaha!');
}
}
}
// routes.php
$router->group('/client/@clientId/job/@jobId', function(Router $router) {
// This group below still gets the parent middleware
// But the parameters are passed in one single array
// in the middleware.
$router->group('/job/@jobId', function(Router $router) {
$router->get('', [ JobController::class, 'view' ]);
$router->put('', [ JobController::class, 'update' ]);
$router->delete('', [ JobController::class, 'delete' ]);
// more routes...
});
}, [ RouteSecurityMiddleware::class ]);
Групування маршрутів з middleware
Ви можете додати групу маршрутів, і кожен маршрут у цій групі матиме той самий middleware. Це корисно, якщо вам потрібно згрупувати багато маршрутів, наприклад, за допомогою middleware Auth для перевірки ключа API в заголовку.
// added at the end of the group method
Flight::group('/api', function() {
// This "empty" looking route will actually match /api
Flight::route('', function() { echo 'api'; }, false, 'api');
// This will match /api/users
Flight::route('/users', function() { echo 'users'; }, false, 'users');
// This will match /api/users/1234
Flight::route('/users/@id', function($id) { echo 'user:'.$id; }, false, 'user_view');
}, [ new ApiAuthMiddleware() ]);
Якщо ви хочете застосувати глобальний middleware до всіх ваших маршрутів, ви можете додати "порожню" групу:
// added at the end of the group method
Flight::group('', function() {
// This is still /users
Flight::route('/users', function() { echo 'users'; }, false, 'users');
// And this is still /users/1234
Flight::route('/users/@id', function($id) { echo 'user:'.$id; }, false, 'user_view');
}, [ ApiAuthMiddleware::class ]); // or [ new ApiAuthMiddleware() ], same thing
Поширені випадки використання
Валідація ключа API
Якщо ви хочете захистити ваші маршрути /api, перевіряючи, чи правильний ключ API, ви можете легко обробити це за допомогою middleware.
use flight\Engine;
class ApiMiddleware {
protected Engine $app;
public function __construct(Engine $app) {
$this->app = $app;
}
public function before(array $params) {
$authorizationHeader = $this->app->request()->getHeader('Authorization');
$apiKey = str_replace('Bearer ', '', $authorizationHeader);
// do a lookup in your database for the api key
$apiKeyHash = hash('sha256', $apiKey);
$hasValidApiKey = !!$this->db()->fetchField("SELECT 1 FROM api_keys WHERE hash = ? AND valid_date >= NOW()", [ $apiKeyHash ]);
if($hasValidApiKey !== true) {
$this->app->jsonHalt(['error' => 'Invalid API Key']);
}
}
}
// routes.php
$router->group('/api', function(Router $router) {
$router->get('/users', [ ApiController::class, 'getUsers' ]);
$router->get('/companies', [ ApiController::class, 'getCompanies' ]);
// more routes...
}, [ ApiMiddleware::class ]);
Тепер усі ваші API-маршрути захищені цим middleware валідації ключа API, яке ви налаштували! Якщо ви додасте більше маршрутів до групи роутера, вони миттєво отримають той самий захист!
Валідація входу в систему
Чи хочете ви захистити деякі маршрути, щоб вони були доступні лише користувачам, які увійшли в систему? Це легко досягти за допомогою middleware!
use flight\Engine;
class LoggedInMiddleware {
protected Engine $app;
public function __construct(Engine $app) {
$this->app = $app;
}
public function before(array $params) {
$session = $this->app->session();
if($session->get('logged_in') !== true) {
$this->app->redirect('/login');
exit;
}
}
}
// routes.php
$router->group('/admin', function(Router $router) {
$router->get('/dashboard', [ DashboardController::class, 'index' ]);
$router->get('/clients', [ ClientController::class, 'index' ]);
// more routes...
}, [ LoggedInMiddleware::class ]);
Валідація параметрів маршруту
Чи хочете ви захистити ваших користувачів від зміни значень в URL для доступу до даних, до яких вони не повинні мати доступ? Це можна вирішити за допомогою middleware!
use flight\Engine;
class RouteSecurityMiddleware {
protected Engine $app;
public function __construct(Engine $app) {
$this->app = $app;
}
public function before(array $params) {
$clientId = $params['clientId'];
$jobId = $params['jobId'];
// perform a lookup of some kind in your database
$isValid = !!$this->app->db()->fetchField("SELECT 1 FROM client_jobs WHERE client_id = ? AND job_id = ?", [ $clientId, $jobId ]);
if($isValid !== true) {
$this->app->halt(400, 'You are blocked, muahahaha!');
}
}
}
// routes.php
$router->group('/client/@clientId/job/@jobId', function(Router $router) {
$router->get('', [ JobController::class, 'view' ]);
$router->put('', [ JobController::class, 'update' ]);
$router->delete('', [ JobController::class, 'delete' ]);
// more routes...
}, [ RouteSecurityMiddleware::class ]);
Обробка виконання middleware
Припустимо, у вас є middleware автентифікації, і ви хочете перенаправити користувача на сторінку входу, якщо він не автентифікований. У вас є кілька варіантів:
- Ви можете повернути false з функції middleware, і Flight автоматично поверне помилку 403 Forbidden, але без кастомізації.
- Ви можете перенаправити користувача на сторінку входу за допомогою
Flight::redirect(). - Ви можете створити власну помилку в middleware і зупинити виконання маршруту.
Простий і прямий
Ось простий приклад return false;:
class MyMiddleware {
public function before($params) {
$hasUserKey = Flight::session()->exists('user');
if ($hasUserKey === false) {
return false;
}
// since it's true, everything just keeps on going
}
}
Приклад перенаправлення
Ось приклад перенаправлення користувача на сторінку входу:
class MyMiddleware {
public function before($params) {
$hasUserKey = Flight::session()->exists('user');
if ($hasUserKey === false) {
Flight::redirect('/login');
exit;
}
}
}
Приклад власної помилки
Припустимо, вам потрібно кинути JSON-помилку, оскільки ви створюєте API. Ви можете зробити це так:
class MyMiddleware {
public function before($params) {
$authorization = Flight::request()->getHeader('Authorization');
if(empty($authorization)) {
Flight::jsonHalt(['error' => 'You must be logged in to access this page.'], 403);
// or
Flight::json(['error' => 'You must be logged in to access this page.'], 403);
exit;
// or
Flight::halt(403, json_encode(['error' => 'You must be logged in to access this page.']);
}
}
}
Дивіться також
- Routing - How to map routes to controllers and render views.
- Requests - Understanding how to handle incoming requests.
- Responses - How to customize HTTP responses.
- Dependency Injection - Simplifying object creation and management in routes.
- Why a Framework? - Understanding the benefits of using a framework like Flight.
- Middleware Execution Strategy Example
Вирішення проблем
- If you have a redirect in your middleware, but your app doesn't seem to be redirecting, make sure you add an
exit;statement in your middleware.
Changelog
- v3.1: Added support for middleware.
Learn/filtering
Фільтрування
Огляд
Flight дозволяє вам фільтрувати відображені методи до та після їх виклику.
Розуміння
Немає заздалегідь визначених хуків, які вам потрібно запам'ятовувати. Ви можете фільтрувати будь-які стандартні методи фреймворку, а також будь-які власні методи, які ви відобразили.
Функція фільтра виглядає так:
/**
* @param array $params Параметри, передані методу, що фільтрується.
* @param string $output (лише для буферизації виводу v2) Вивід методу, що фільтрується.
* @return bool Поверніть true/void або не повертайте нічого, щоб продовжити ланцюжок, false, щоб перервати ланцюжок.
*/
function (array &$params, string &$output): bool {
// Код фільтра
}
Використовуючи передані змінні, ви можете маніпулювати вхідними параметрами та/або виводом.
Ви можете запустити фільтр перед методом, виконавши:
Flight::before('start', function (array &$params, string &$output): bool {
// Зробіть щось
});
Ви можете запустити фільтр після методу, виконавши:
Flight::after('start', function (array &$params, string &$output): bool {
// Зробіть щось
});
Ви можете додати стільки фільтрів, скільки захочете, до будь-якого методу. Вони будуть викликані в порядку, в якому вони оголошені.
Ось приклад процесу фільтрування:
// Відобразіть власний метод
Flight::map('hello', function (string $name) {
return "Hello, $name!";
});
// Додайте фільтр перед
Flight::before('hello', function (array &$params, string &$output): bool {
// Маніпулюйте параметром
$params[0] = 'Fred';
return true;
});
// Додайте фільтр після
Flight::after('hello', function (array &$params, string &$output): bool {
// Маніпулюйте виводом
$output .= " Have a nice day!";
return true;
});
// Викличте власний метод
echo Flight::hello('Bob');
Це повинно відобразити:
Hello Fred! Have a nice day!
Якщо ви визначили кілька фільтрів, ви можете перервати ланцюжок, повернувши false
в будь-якій з ваших функцій фільтра:
Flight::before('start', function (array &$params, string &$output): bool {
echo 'one';
return true;
});
Flight::before('start', function (array &$params, string &$output): bool {
echo 'two';
// Це завершить ланцюжок
return false;
});
// Це не буде викликано
Flight::before('start', function (array &$params, string &$output): bool {
echo 'three';
return true;
});
Примітка: Основні методи, такі як
mapіregister, не можна фільтрувати, оскільки вони викликаються безпосередньо і не викликаються динамічно. Дивіться Розширення Flight для отримання додаткової інформації.
Див. також
Вирішення проблем
- Переконайтеся, що ви повертаєте
falseзі своїх функцій фільтра, якщо хочете, щоб ланцюжок зупинився. Якщо ви нічого не повертаєте, ланцюжок продовжиться.
Журнал змін
- v2.0 - Початковий реліз.
Learn/requests
Запити
Огляд
Flight інкапсулює HTTP-запит в один об'єкт, до якого можна отримати доступ так:
$request = Flight::request();
Розуміння
HTTP-запити є одним з основних аспектів, які потрібно розуміти щодо життєвого циклу HTTP. Користувач виконує дію в веб-браузері або HTTP-клієнті, і вони надсилають серію заголовків, тіла, URL тощо до вашого проекту. Ви можете захоплювати ці заголовки (мова браузера, тип стиснення, який вони можуть обробляти, user agent тощо) і захоплювати тіло та URL, що надсилаються до вашої програми Flight. Ці запити є суттєвими для вашої програми, щоб зрозуміти, що робити далі.
Базове використання
PHP має кілька суперглобальних змінних, включаючи $_GET, $_POST, $_REQUEST, $_SERVER, $_FILES та $_COOKIE. Flight абстрагує їх у зручні Collections. Ви можете отримати доступ до властивостей query, data, cookies та files як до масивів або об'єктів.
Примітка: НАЙКАТЕГОРІЧНІШЕ не рекомендується використовувати ці суперглобальні змінні у вашому проекті, і їх слід посилатися через об'єкт
request().
Примітка: Немає доступної абстракції для
$_ENV.
$_GET
Ви можете отримати доступ до масиву $_GET через властивість query:
// GET /search?keyword=something
Flight::route('/search', function(){
$keyword = Flight::request()->query['keyword'];
// or
$keyword = Flight::request()->query->keyword;
echo "You are searching for: $keyword";
// query a database or something else with the $keyword
});
$_POST
Ви можете отримати доступ до масиву $_POST через властивість data:
Flight::route('POST /submit', function(){
$name = Flight::request()->data['name'];
$email = Flight::request()->data['email'];
// or
$name = Flight::request()->data->name;
$email = Flight::request()->data->email;
echo "You submitted: $name, $email";
// save to a database or something else with the $name and $email
});
$_COOKIE
Ви можете отримати доступ до масиву $_COOKIE через властивість cookies:
Flight::route('GET /login', function(){
$savedLogin = Flight::request()->cookies['myLoginCookie'];
// or
$savedLogin = Flight::request()->cookies->myLoginCookie;
// check if it's really saved or not and if it is auto log them in
if($savedLogin) {
Flight::redirect('/dashboard');
return;
}
});
Для допомоги щодо встановлення нових значень cookie дивіться overclokk/cookie
$_SERVER
Доступний ярлик для доступу до масиву $_SERVER через метод getVar():
$host = Flight::request()->getVar('HTTP_HOST');
$_FILES
Ви можете отримати доступ до завантажених файлів через властивість files:
// raw access to $_FILES property. See below for recommended approach
$uploadedFile = Flight::request()->files['myFile'];
// or
$uploadedFile = Flight::request()->files->myFile;
Дивіться Uploaded File Handler для отримання додаткової інформації.
Обробка завантаження файлів
v3.12.0
Ви можете обробляти завантаження файлів за допомогою фреймворку з деякими допоміжними методами. По суті, це зводиться до витягування даних файлу з запиту та переміщення його в нове місце розташування.
Flight::route('POST /upload', function(){
// If you had an input field like <input type="file" name="myFile">
$uploadedFileData = Flight::request()->getUploadedFiles();
$uploadedFile = $uploadedFileData['myFile'];
$uploadedFile->moveTo('/path/to/uploads/' . $uploadedFile->getClientFilename());
});
Якщо у вас завантажено кілька файлів, ви можете перебирати їх:
Flight::route('POST /upload', function(){
// If you had an input field like <input type="file" name="myFiles[]">
$uploadedFiles = Flight::request()->getUploadedFiles()['myFiles'];
foreach ($uploadedFiles as $uploadedFile) {
$uploadedFile->moveTo('/path/to/uploads/' . $uploadedFile->getClientFilename());
}
});
Примітка щодо безпеки: Завжди валідуйте та очищайте вхідні дані користувача, особливо при роботі з завантаженням файлів. Завжди валідуйте типи розширень, які ви дозволите завантажувати, але також валідуйте "магічні байти" файлу, щоб переконатися, що це дійсно тип файлу, який стверджує користувач. Є статті та бібліотеки, доступні для допомоги з цим.
Тіло запиту
Щоб отримати сире тіло HTTP-запиту, наприклад, при роботі з POST/PUT-запитами, ви можете зробити:
Flight::route('POST /users/xml', function(){
$xmlBody = Flight::request()->getBody();
// do something with the XML that was sent.
});
JSON тіло
Якщо ви отримуєте запит з типом вмісту application/json і прикладом даних {"id": 123}
воно буде доступне з властивості data:
$id = Flight::request()->data->id;
Заголовки запиту
Ви можете отримати доступ до заголовків запиту за допомогою методу getHeader() або getHeaders():
// Maybe you need Authorization header
$host = Flight::request()->getHeader('Authorization');
// or
$host = Flight::request()->header('Authorization');
// If you need to grab all headers
$headers = Flight::request()->getHeaders();
// or
$headers = Flight::request()->headers();
Метод запиту
Ви можете отримати доступ до методу запиту за допомогою властивості method або методу getMethod():
$method = Flight::request()->method; // actually populated by getMethod()
$method = Flight::request()->getMethod();
Примітка: Метод getMethod() спочатку витягує метод з $_SERVER['REQUEST_METHOD'], потім його можна перезаписати
за допомогою $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'], якщо він існує, або $_REQUEST['_method'], якщо він існує.
Властивості об'єкта запиту
Об'єкт запиту надає такі властивості:
- body - Сире тіло HTTP-запиту
- url - URL, що запитується
- base - Батьківська підкаталог URL
- method - Метод запиту (GET, POST, PUT, DELETE)
- referrer - URL реферера
- ip - IP-адреса клієнта
- ajax - Чи є запит AJAX-запитом
- scheme - Протокол сервера (http, https)
- user_agent - Інформація про браузер
- type - Тип вмісту
- length - Довжина вмісту
- query - Параметри рядка запиту
- data - Дані POST або JSON-даних
- cookies - Дані cookie
- files - Завантажені файли
- secure - Чи є з'єднання захищеним
- accept - HTTP параметри accept
- proxy_ip - IP-адреса проксі клієнта. Сканує масив
$_SERVERнаHTTP_CLIENT_IP,HTTP_X_FORWARDED_FOR,HTTP_X_FORWARDED,HTTP_X_CLUSTER_CLIENT_IP,HTTP_FORWARDED_FOR,HTTP_FORWARDEDв такому порядку. - host - Ім'я хоста запиту
- servername - SERVER_NAME з
$_SERVER
Допоміжні методи
Є кілька допоміжних методів для складання частин URL або роботи з певними заголовками.
Повний URL
Ви можете отримати доступ до повного URL запиту за допомогою методу getFullUrl():
$url = Flight::request()->getFullUrl();
// https://example.com/some/path?foo=bar
Базовий URL
Ви можете отримати доступ до базового URL за допомогою методу getBaseUrl():
// http://example.com/path/to/something/cool?query=yes+thanks
$url = Flight::request()->getBaseUrl();
// https://example.com
// Notice, no trailing slash.
Парсинг запиту
Ви можете передати URL методу parseQuery(), щоб розпарсити рядок запиту в асоціативний масив:
$query = Flight::request()->parseQuery('https://example.com/some/path?foo=bar');
// ['foo' => 'bar']
Переговори щодо типів вмісту Accept
v3.17.2
Ви можете використовувати метод negotiateContentType(), щоб визначити найкращий тип вмісту для відповіді на основі заголовка Accept, надісланого клієнтом.
// Example Accept header: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
// The below defines what you support.
$availableTypes = ['application/json', 'application/xml'];
$typeToServe = Flight::request()->negotiateContentType($availableTypes);
if ($typeToServe === 'application/json') {
// Serve JSON response
} elseif ($typeToServe === 'application/xml') {
// Serve XML response
} else {
// Default to something else or throw an error
}
Примітка: Якщо жоден з доступних типів не знайдено в заголовку
Accept, метод повернеnull. Якщо заголовокAcceptне визначено, метод поверне перший тип у масиві$availableTypes.
Див. також
- Routing - Дивіться, як відображати маршрути на контролери та рендерити види.
- Responses - Як налаштовувати HTTP-відповіді.
- Why a Framework? - Як запити вписуються в загальну картину.
- Collections - Робота з колекціями даних.
- Uploaded File Handler - Обробка завантаження файлів.
Вирішення проблем
request()->ipтаrequest()->proxy_ipможуть відрізнятися, якщо ваш веб-сервер знаходиться за проксі, балансувальником навантаження тощо.
Журнал змін
- v3.17.2 - Додано negotiateContentType()
- v3.12.0 - Додано можливість обробляти завантаження файлів через об'єкт запиту.
- v1.0 - Початкове випущення.
Learn/why_frameworks
Чому фреймворк?
Деякі програмісти рішуче проти використання фреймворків. Вони стверджують, що фреймворки є надмірними, повільними і важкими для навчання. Вони кажуть, що фреймворки є непотрібними і що ви можете написати кращий код без них. Звичайно, є деякі слушні зауваження щодо недоліків використання фреймворків. Однак також є багато переваг використання фреймворків.
Причини використання фреймворка
Ось кілька причин, чому ви можете хотіти розглянути можливість використання фреймворка:
- Швидка розробка: Фреймворки забезпечують багато функціональності прямо з коробки. Це означає, що ви можете швидше створювати веб-додатки. Вам не потрібно писати так багато коду, оскільки фреймворк забезпечує багато функціональності, яку ви потребуєте.
- Послідовність: Фреймворки забезпечують послідовний спосіб виконання завдань. Це полегшує розуміння того, як працює код, і спрощує іншим розробникам розуміння вашого коду. Якщо ви маєте все по порядку, ви можете втратити послідовність між скриптами, особливо якщо працюєте в команді з розробниками.
- Безпека: Фреймворки надають функції безпеки, які допомагають захистити ваші веб-додатки від загальних загроз безпеки. Це означає, що вам не потрібно так сильно хвилюватися про безпеку, оскільки фреймворк піклується про багато з цього.
- Спільнота: У фреймворків є великі спільноти розробників, які роблять внески у фреймворк. Це означає, що ви можете отримувати допомогу від інших розробників, коли у вас є запитання або проблеми. Це також означає, що існує багато ресурсів для навчання роботи з фреймворком.
- Кращі практики: Фреймворки створені за допомогою кращих практик. Це означає, що ви можете навчитися від фреймворка і використовувати ті ж самі кращі практики у своєму коді. Це може допомогти вам стати кращим програмістом. Іноді ви не знаєте, що ви не знаєте, і це може зашкодити вам у кінці.
- Розширюваність: Фреймворки розроблені для розширення. Це означає, що ви можете додавати свою функціональність у фреймворк. Це дозволяє вам створювати веб-додатки, які відповідають вашим специфічним потребам.
Flight — це мікро-фреймворк. Це означає, що він невеликий і легкий. Він не надає такої ж функціональності, як більші фреймворки, такі як Laravel або Symfony. Однак він забезпечує багато функціональності, яку ви потребуєте для створення веб-додатків. Його також легко навчитися і використовувати. Це робить його хорошим вибором для швидкого і легкого створення веб-додатків. Якщо ви новачок у фреймворках, Flight є чудовим початковим фреймворком для старту. Він допоможе вам дізнатися про переваги використання фреймворків, не перевантажуючи вас занадто великою складністю. Після того, як ви отримаєте деякий досвід з Flight, вам буде легше перейти на більш складні фреймворки, такі як Laravel або Symfony, однак Flight все ще може скласти успішну надійну програму.
Що таке маршрутизація?
Маршрутизація — це основа фреймворка Flight, але що це таке насправді? Маршрутизація — це процес взяття URL-адреси і зіставлення її з конкретною функцією у вашому коді.
Це спосіб, яким ви можете змусити ваш веб-сайт виконувати різні дії в залежності від запитуваної URL-адреси. Наприклад, ви можете захотіти показати профіль користувача, коли вони
відвідують /user/1234, але показати список всіх користувачів, коли вони відвідують /users. Це все робиться через маршрутизацію.
Це може працювати приблизно так:
- Користувач переходить у ваш браузер і вводить
http://example.com/user/1234. - Сервер отримує запит і дивиться на URL-адресу та передає його вашому коду додатку Flight.
- Скажімо, у вашому коді Flight ви маєте щось на зразок
Flight::route('/user/@id', [ 'UserController', 'viewUserProfile' ]);. Ваш код додатку Flight дивиться на URL-адресу і бачить, що вона відповідає маршруту, який ви визначили, а потім виконує код, який ви визначили для цього маршруту. - Маршрутизатор Flight запуститься і викличе метод
viewUserProfile($id)у класіUserController, передаючи1234як аргумент$idу методі. - Код у вашому методі
viewUserProfile()потім виконається і зробить те, що ви йому сказали. Ви можете закодувати виведення HTML для сторінки профілю користувача, або, якщо це RESTful API, ви можете закодувати виведення JSON-відповіді з інформацією про користувача. - Flight упаковує це красиво, генерує заголовки відповіді і відправляє їх назад у браузер користувача.
- Користувач заповнюється радістю та обіймає себе!
І чому це важливо?
Наявність належного централізованого маршрутизатора може значно спростити ваше життя! Це може бути важко зрозуміти на перший погляд. Ось кілька причин, чому:
- Централізована маршрутизація: Ви можете зберігати всі свої маршрути в одному місці. Це полегшує перегляд маршрутів, які у вас є, та того, що вони роблять. Це також полегшує внесення змін, якщо це буде потрібно.
- Параметри маршруту: Ви можете використовувати параметри маршруту, щоб передавати дані своїм методам маршруту. Це чудовий спосіб зберегти чистоту та організованість вашого коду.
- Групи маршрутів: Ви можете групувати маршрути разом. Це чудово для організації вашого коду та застосування проміжного програмного забезпечення до групи маршрутів.
- Псевдоніми маршрутів: Ви можете призначити псевдонім маршруту, щоб URL-адреса могла динамічно генеруватись пізніше у вашому коді (наприклад, як шаблон). Наприклад: замість того, щоб хардкодити
/user/1234у вашому коді, ви можете натомість посилатись на псевдонімuser_viewі передаватиidяк параметр. Це робить його чудовим, якщо ви вирішите змінити його на/admin/user/1234пізніше. Вам не потрібно буде змінювати всі ваші зашкарублені URL-адреси, а тільки URL, що прикріплений до маршруту. - Проміжне програмне забезпечення маршруту: Ви можете додати проміжне програмне забезпечення до ваших маршрутів. Проміжне програмне забезпечення надзвичайно потужне, додаючи специфічні поведінки до вашого додатку, такі як підтвердження, що певний користувач має доступ до маршруту або групи маршрутів.
Я впевнений, що ви знайомі з методом "скрипт за скриптом" для створення веб-сайту. У вас може бути файл під назвою index.php, який містить безліч if
операцій для перевірки URL-адреси, а потім виконання конкретної функції на основі URL-адреси. Це форма маршрутизації, але вона не дуже організована і може
вийти з-під контролю швидко. Система маршрутизації Flight є набагато більш організованим і потужним способом обробки маршрутизації.
Це?
// /user/view_profile.php?id=1234
if ($_GET['id']) {
$id = $_GET['id'];
viewUserProfile($id);
}
// /user/edit_profile.php?id=1234
if ($_GET['id']) {
$id = $_GET['id'];
editUserProfile($id);
}
// і т.д...
Або це?
// index.php
Flight::route('/user/@id', [ 'UserController', 'viewUserProfile' ]);
Flight::route('/user/@id/edit', [ 'UserController', 'editUserProfile' ]);
// У вашому app/controllers/UserController.php
class UserController {
public function viewUserProfile($id) {
// зробити щось
}
public function editUserProfile($id) {
// зробити щось
}
}
Сподіваюсь, ви вже почали бачити переваги використання централізованої системи маршрутизації. Це значно легше керувати та розуміти в довгостроковій перспективі!
Запити та відповіді
Flight забезпечує простий і легкий спосіб обробки запитів і відповідей. Це основа того, що робить веб-фреймворк. Він приймає запит від браузера користувача, обробляє його, а потім надсилає відповідь. Це спосіб, яким ви можете створювати веб-додатки, які виконують такі дії, як показ профілю користувача, дозволяють користувачу входити в систему або дозволяють користувачу публікувати новий блог.
Запити
Запит — це те, що браузер користувача надсилає на ваш сервер, коли вони відвідують ваш веб-сайт. Цей запит містить інформацію про те, що користувач хоче зробити. Наприклад, він може містити інформацію про те, яку URL-адресу хоче відвідати користувач, які дані хоче надіслати користувач на ваш сервер, або який тип даних хоче отримати користувач від вашого сервера. Важливо знати, що запит є тільки для читання. Ви не можете змінити запит, але можете читати з нього.
Flight забезпечує простий спосіб доступу до інформації про запит. Ви можете отримати доступ до інформації про запит, використовуючи метод Flight::request().
Цей метод повертає об'єкт Request, який містить інформацію про запит. Ви можете використовувати цей об'єкт, щоб отримати інформацію про
запит, наприклад, URL, метод або дані, які користувач надіслав на ваш сервер.
Відповіді
Відповідь — це те, що ваш сервер надсилає назад браузеру користувача, коли вони відвідують ваш веб-сайт. Ця відповідь містить інформацію про те, що ваш сервер хоче зробити. Наприклад, вона може містити інформацію про те, які дані ваш сервер хоче надіслати користувачу, які дані ваш сервер хоче отримати від користувача, або які дані ваш сервер хоче зберегти на комп'ютері користувача.
Flight надає простий спосіб надіслати відповідь браузеру користувача. Ви можете надіслати відповідь, використовуючи метод Flight::response(). Цей метод
приймає об'єкт Response як аргумент і надсилає відповідь браузеру користувача. Ви можете використовувати цей об'єкт, щоб надіслати відповідь
до браузера користувача, таку як HTML, JSON або файл. Flight допомагає вам автоматично генерувати деякі частини відповіді, щоб упростити процес, але в кінцевому підсумку ви маєте
контроль над тим, що ви надсилаєте назад користувачу.
Learn/responses
Відповіді
Огляд
Flight допомагає генерувати частину заголовків відповіді для вас, але ви маєте більший контроль над тим, що ви надсилаєте назад користувачеві. Більшість часу ви звертатиметесь безпосередньо до об'єкта response(), але Flight має деякі допоміжні методи для встановлення деяких заголовків відповіді для вас.
Розуміння
Після того, як користувач надсилає свій запит до вашого додатка, вам потрібно генерувати правильну відповідь для них. Вони надіслали вам інформацію, таку як мова, яку вони віддають перевагу, чи можуть вони обробляти певні типи стиснення, їхній агент користувача тощо, і після обробки всього настав час надіслати їм назад правильну відповідь. Це може бути встановлення заголовків, вивід тіла HTML або JSON для них або перенаправлення їх на сторінку.
Основне використання
Надсилання тіла відповіді
Flight використовує ob_start() для буферизації виводу. Це означає, що ви можете використовувати echo або print для надсилання відповіді користувачеві, і Flight захопить її та надішле назад користувачеві з відповідними заголовками.
// Це надішле "Hello, World!" до браузера користувача
Flight::route('/', function() {
echo "Hello, World!";
});
// HTTP/1.1 200 OK
// Content-Type: text/html
//
// Hello, World!
Як альтернативу, ви можете викликати метод write() для додавання до тіла також.
// Це надішле "Hello, World!" до браузера користувача
Flight::route('/', function() {
// детально, але іноді потрібно для виконання роботи
Flight::response()->write("Hello, World!");
// якщо ви хочете отримати тіло, яке ви встановили на цьому етапі
// ви можете зробити це так
$body = Flight::response()->getBody();
});
JSON
Flight надає підтримку для надсилання JSON та JSONP відповідей. Щоб надіслати JSON відповідь, ви передаєте деякі дані для кодування JSON:
Flight::route('/@companyId/users', function(int $companyId) {
// якось витягніть своїх користувачів з бази даних, наприклад
$users = Flight::db()->fetchAll("SELECT id, first_name, last_name FROM users WHERE company_id = ?", [ $companyId ]);
Flight::json($users);
});
// [{"id":1,"first_name":"Bob","last_name":"Jones"}, /* more users */ ]
Примітка: За замовчуванням Flight надішле заголовок
Content-Type: application/jsonз відповіддю. Він також використовуватиме прапорціJSON_THROW_ON_ERRORтаJSON_UNESCAPED_SLASHESпід час кодування JSON.
JSON з кодом статусу
Ви також можете передати код статусу як другий аргумент:
Flight::json(['id' => 123], 201);
JSON з красивим виводом
Ви також можете передати аргумент в останню позицію для увімкнення красивого виведення:
Flight::json(['id' => 123], 200, true, 'utf-8', JSON_PRETTY_PRINT);
Зміна порядку аргументів JSON
Flight::json() є дуже застарілим методом, але мета Flight — підтримувати зворотну сумісність для проєктів. Насправді це дуже просто, якщо ви хочете переробити порядок аргументів для використання простішого синтаксису, ви можете просто переналаштувати метод JSON як будь-який інший метод Flight:
Flight::map('json', function($data, $code = 200, $options = 0) {
// тепер вам не потрібно `true, 'utf-8'` при використанні методу json()!
Flight::_json($data, $code, true, 'utf-8', $options);
}
// А тепер його можна використовувати так
Flight::json(['id' => 123], 200, JSON_PRETTY_PRINT);
JSON та зупинка виконання
v3.10.0
Якщо ви хочете надіслати JSON відповідь і зупинити виконання, ви можете використовувати метод jsonHalt(). Це корисно для випадків, коли ви перевіряєте, можливо, якийсь тип авторизації, і якщо користувач не авторизований, ви можете негайно надіслати JSON відповідь, очистити існуючий вміст тіла та зупинити виконання.
Flight::route('/users', function() {
$authorized = someAuthorizationCheck();
// Перевірте, чи авторизований користувач
if($authorized === false) {
Flight::jsonHalt(['error' => 'Unauthorized'], 401);
// немає exit; потрібно тут.
}
// Продовжуйте з рештою маршруту
});
До v3.10.0 ви б мусили зробити щось на кшталт цього:
Flight::route('/users', function() {
$authorized = someAuthorizationCheck();
// Перевірте, чи авторизований користувач
if($authorized === false) {
Flight::halt(401, json_encode(['error' => 'Unauthorized']));
}
// Продовжуйте з рештою маршруту
});
Очищення тіла відповіді
Якщо ви хочете очистити тіло відповіді, ви можете використовувати метод clearBody:
Flight::route('/', function() {
if($someCondition) {
Flight::response()->write("Hello, World!");
} else {
Flight::response()->clearBody();
}
});
Випадок використання вище, ймовірно, не є поширеним, однак він міг би бути більш поширеним, якщо це використовувалося в middleware.
Виконання зворотного виклику на тілі відповіді
Ви можете виконати зворотний виклик на тілі відповіді, використовуючи метод addResponseBodyCallback:
Flight::route('/users', function() {
$db = Flight::db();
$users = $db->fetchAll("SELECT * FROM users");
Flight::render('users_table', ['users' => $users]);
});
// Це gzip-увиме всі відповіді для будь-якого маршруту
Flight::response()->addResponseBodyCallback(function($body) {
return gzencode($body, 9);
});
Ви можете додати кілька зворотних викликів, і вони виконуватимуться в порядку, в якому вони були додані. Оскільки це може приймати будь-який викликаний, він може приймати масив класу [ $class, 'method' ], замикання $strReplace = function($body) { str_replace('hi', 'there', $body); };, або ім'я функції 'minify', якщо у вас є функція для мініфікації вашого HTML коду, наприклад.
Примітка: Зворотні виклики маршрутів не працюватимуть, якщо ви використовуєте опцію конфігурації flight.v2.output_buffering.
Специфічний зворотний виклик маршруту
Якщо ви хочете, щоб це застосовувалося тільки до конкретного маршруту, ви можете додати зворотний виклик безпосередньо в маршрут:
Flight::route('/users', function() {
$db = Flight::db();
$users = $db->fetchAll("SELECT * FROM users");
Flight::render('users_table', ['users' => $users]);
// Це gzip-увиме тільки відповідь для цього маршруту
Flight::response()->addResponseBodyCallback(function($body) {
return gzencode($body, 9);
});
});
Опція Middleware
Ви також можете використовувати middleware для застосування зворотного виклику до всіх маршрутів через middleware:
// MinifyMiddleware.php
class MinifyMiddleware {
public function before() {
// Застосуйте зворотний виклик тут на об'єкті response().
Flight::response()->addResponseBodyCallback(function($body) {
return $this->minify($body);
});
}
protected function minify(string $body): string {
// мініфікуйте тіло якось
return $body;
}
}
// index.php
Flight::group('/users', function() {
Flight::route('', function() { /* ... */ });
Flight::route('/@id', function($id) { /* ... */ });
}, [ new MinifyMiddleware() ]);
Коди статусу
Ви можете встановити код статусу відповіді, використовуючи метод status:
Flight::route('/@id', function($id) {
if($id == 123) {
Flight::response()->status(200);
echo "Hello, World!";
} else {
Flight::response()->status(403);
echo "Forbidden";
}
});
Якщо ви хочете отримати поточний код статусу, ви можете використовувати метод status без будь-яких аргументів:
Flight::response()->status(); // 200
Встановлення заголовка відповіді
Ви можете встановити заголовок, такий як тип вмісту відповіді, використовуючи метод header:
// Це надішле "Hello, World!" до браузера користувача як звичайний текст
Flight::route('/', function() {
Flight::response()->header('Content-Type', 'text/plain');
// або
Flight::response()->setHeader('Content-Type', 'text/plain');
echo "Hello, World!";
});
Перенаправлення
Ви можете перенаправити поточний запит, використовуючи метод redirect() і передаючи новий URL:
Flight::route('/login', function() {
$username = Flight::request()->data->username;
$password = Flight::request()->data->password;
$passwordConfirm = Flight::request()->data->password_confirm;
if($password !== $passwordConfirm) {
Flight::redirect('/new/location');
return; // це необхідно, щоб функціональність нижче не виконувалася
}
// додайте нового користувача...
Flight::db()->runQuery("INSERT INTO users ....");
Flight::redirect('/admin/dashboard');
});
Примітка: За замовчуванням Flight надсилає HTTP 303 ("See Other") код статусу. Ви можете опціонально встановити власний код:
Flight::redirect('/new/location', 301); // постійний
Зупинка виконання маршруту
Ви можете зупинити фреймворк і негайно вийти в будь-якій точці, викликаючи метод halt:
Flight::halt();
Ви також можете вказати опціональний HTTP код статусу та повідомлення:
Flight::halt(200, 'Be right back...');
Виклик halt відкине будь-який вміст відповіді до цієї точки та зупинить все виконання. Якщо ви хочете зупинити фреймворк і вивести поточну відповідь, використовуйте метод stop:
Flight::stop($httpStatusCode = null);
Примітка:
Flight::stop()має деяку дивну поведінку, таку як вивід відповіді, але продовження виконання вашого скрипту, що може не бути тим, чого ви хочете. Ви можете використовуватиexitабоreturnпісля викликуFlight::stop()для запобігання подальшому виконанню, але загалом рекомендується використовуватиFlight::halt().
Це збереже ключ і значення заголовка в об'єкті відповіді. Наприкінці циклу життя запиту він побудує заголовки та надішле відповідь.
Розширене використання
Надсилання заголовка негайно
Можуть бути випадки, коли вам потрібно зробити щось власне з заголовком, і вам потрібно надіслати заголовок на тій самій лінії коду, з якою ви працюєте. Якщо ви встановлюєте потоковий маршрут, це те, що вам знадобиться. Це досягається через response()->setRealHeader().
Flight::route('/', function() {
Flight::response()->setRealHeader('Content-Type: text/plain');
echo 'Streaming response...';
sleep(5);
echo 'Done!';
})->stream();
JSONP
Для JSONP запитів ви можете опціонально передати назву параметра запиту, який ви використовуєте для визначення вашої функції зворотного виклику:
Flight::jsonp(['id' => 123], 'q');
Отже, коли робиться GET запит за допомогою ?q=my_func, ви повинні отримати вивід:
my_func({"id":123});
Якщо ви не передасте назву параметра запиту, він за замовчуванням буде jsonp.
Примітка: Якщо ви все ще використовуєте JSONP запити в 2025 році та пізніше, приєднуйтеся до чату та розкажіть нам чому! Нам подобається чути хороші історії битв/жахів!
Очищення даних відповіді
Ви можете очистити тіло відповіді та заголовки, використовуючи метод clear(). Це очистить будь-які заголовки, призначені для відповіді, очистить тіло відповіді та встановить код статусу на 200.
Flight::response()->clear();
Очищення тільки тіла відповіді
Якщо ви хочете очистити тільки тіло відповіді, ви можете використовувати метод clearBody():
// Це все ще збереже будь-які заголовки, встановлені на об'єкті response().
// Це все ще збереже будь-які заголовки, встановлені на об'єкті response().
Flight::response()->clearBody();
Кешування HTTP
Flight надає вбудовану підтримку для кешування на рівні HTTP. Якщо умова кешування виконана, Flight поверне HTTP відповідь 304 Not Modified. Наступного разу, коли клієнт запитує той самий ресурс, йому буде запропоновано використовувати локально кешовану версію.
Кешування на рівні маршруту
Якщо ви хочете кешувати всю вашу відповідь, ви можете використовувати метод cache() і передати час для кешування.
// Це кешуватиме відповідь на 5 хвилин
Flight::route('/news', function () {
Flight::response()->cache(time() + 300);
echo 'This content will be cached.';
});
// Альтернативно, ви можете використовувати рядок, який ви б передали
// методу strtotime()
Flight::route('/news', function () {
Flight::response()->cache('+5 minutes');
echo 'This content will be cached.';
});
Last-Modified
Ви можете використовувати метод lastModified і передати UNIX мітку часу для встановлення дати та часу, коли сторінка була востаннє змінена. Клієнт продовжуватиме використовувати свій кеш, доки значення останньої модифікації не зміниться.
Flight::route('/news', function () {
Flight::lastModified(1234567890);
echo 'This content will be cached.';
});
ETag
Кешування ETag подібне до Last-Modified, за винятком того, що ви можете вказати будь-який ідентифікатор, який ви хочете для ресурсу:
Flight::route('/news', function () {
Flight::etag('my-unique-id');
echo 'This content will be cached.';
});
Пам'ятайте, що виклик як lastModified, так і etag обидва встановлять і перевірять значення кешу. Якщо значення кешу однакове між запитами, Flight негайно надішле відповідь HTTP 304 і зупинить обробку.
Завантаження файлу
v3.12.0
Є допоміжний метод для потокової передачі файлу кінцевому користувачеві. Ви можете використовувати метод download і передати шлях.
Flight::route('/download', function () {
Flight::download('/path/to/file.txt');
// Починаючи з v3.17.1 ви можете вказати власну назву файлу для завантаження
Flight::download('/path/to/file.txt', 'custom_name.txt');
});
Дивіться також
- Маршрутизація - Як зіставляти маршрути з контролерами та рендерити перегляди.
- Запити - Розуміння, як обробляти вхідні запити.
- Middleware - Використання middleware з маршрутами для автентифікації, логування тощо.
- Чому фреймворк? - Розуміння переваг використання фреймворку, як Flight.
- Розширення - Як розширювати Flight власною функціональністю.
Вирішення проблем
- Якщо у вас проблеми з перенаправленнями, які не працюють, переконайтеся, що ви додаєте
return;до методу. stop()іhalt()— не те саме.halt()зупинить виконання негайно, тоді якstop()дозволить виконанню продовжуватися.
Журнал змін
- v3.17.1 - Додано
$fileNameдо методуdownloadFile(). - v3.12.0 - Додано допоміжний метод downloadFile.
- v3.10.0 - Додано
jsonHalt. - v1.0 - Початковий реліз.
Learn/events
Менеджер подій
станом на v3.15.0
Огляд
Події дозволяють реєструвати та активувати власну поведінку у вашому додатку. З додаванням Flight::onEvent() та Flight::triggerEvent(), ви тепер можете підключатися до ключових моментів життєвого циклу вашого додатку або визначати власні події (наприклад, сповіщення та email) для того, щоб зробити ваш код більш модульним та розширюваним. Ці методи є частиною mappable methods у Flight, що означає, що ви можете перевизначити їхню поведінку відповідно до ваших потреб.
Розуміння
Події дозволяють розділяти різні частини вашого додатку, щоб вони не залежали надто сильно одна від одної. Це розділення — часто називається розв’язанням залежностей — робить ваш код легшим для оновлення, розширення або налагодження. Замість того, щоб писати все в одному великому блоці, ви можете розбити вашу логіку на менші, незалежні частини, які реагують на конкретні дії (події).
Уявіть, що ви будуєте додаток для блогу:
- Коли користувач публікує коментар, ви можете захотіти:
- Зберегти коментар у базі даних.
- Надіслати email власнику блогу.
- Записати дію для безпеки.
Без подій ви б запхали все це в одну функцію. З подіями ви можете розбити це: одна частина зберігає коментар, інша активує подію на кшталт 'comment.posted', а окремі слухачі обробляють email та логування. Це робить ваш код чистішим та дозволяє додавати або видаляти функції (наприклад, сповіщення) без дотику до основної логіки.
Поширені випадки використання
У більшості випадків події корисні для речей, які є необов’язковими, але не є абсолютною основною частиною вашої системи. Наприклад, наступні є хорошими для наявності, але якщо вони з якихось причин не спрацюють, ваш додаток все одно повинен працювати:
- Логування: Записувати дії на кшталт логінів або помилок без засмічення основного коду.
- Сповіщення: Надсилати email або сповіщення, коли щось відбувається.
- Оновлення кешу: Оновлювати кеш або сповіщати інші системи про зміни.
Однак уявіть, що у вас є функція "забули пароль". Це повинно бути частиною вашої основної функціональності, а не подією, бо якщо той email не буде надісланий, користувач не зможе скинути пароль та використовувати ваш додаток.
Основне використання
Система подій у Flight побудована навколо двох основних методів: Flight::onEvent() для реєстрації слухачів подій та Flight::triggerEvent() для активації подій. Ось як ви можете їх використовувати:
Реєстрація слухачів подій
Щоб слухати подію, використовуйте Flight::onEvent(). Цей метод дозволяє вам визначити, що повинно відбуватися, коли подія виникає.
Flight::onEvent(string $event, callable $callback): void
$event: Назва для вашої події (наприклад,'user.login').$callback: Функція, яка виконується, коли подія активується.
Ви "підписуєтеся" на подію, повідомляючи Flight, що робити, коли вона відбувається. Callback може приймати аргументи, передані від активатора події.
Система подій у Flight є синхронною, що означає, що кожен слухач події виконується послідовно, один за одним. Коли ви активуєте подію, всі зареєстровані слухачі для цієї події виконаються до завершення, перш ніж ваш код продовжиться. Це важливо розуміти, оскільки це відрізняється від асинхронних систем подій, де слухачі можуть виконуватися паралельно або пізніше.
Простий приклад
Flight::onEvent('user.login', function ($username) {
echo "Ласкаво просимо назад, $username!";
// ви можете надіслати email, якщо логін з нового місця
});
Тут, коли подія 'user.login' активується, вона привітає користувача по імені та може також включати логіку для надсилання email, якщо потрібно.
Примітка: Callback може бути функцією, анонімною функцією або методом з класу.
Активація подій
Щоб зробити подію, використовуйте Flight::triggerEvent(). Це повідомляє Flight виконати всіх слухачів, зареєстрованих для цієї події, передаючи будь-які дані, які ви надаєте.
Flight::triggerEvent(string $event, ...$args): void
$event: Назва події, яку ви активуєте (має відповідати зареєстрованій події)....$args: Необов’язкові аргументи для надсилання слухачам (може бути будь-яка кількість аргументів).
Простий приклад
$username = 'alice';
Flight::triggerEvent('user.login', $username);
Це активує подію 'user.login' та надсилає 'alice' слухачу, який ми визначили раніше, що виведе: Ласкаво просимо назад, alice!.
- Якщо немає зареєстрованих слухачів, нічого не відбувається — ваш додаток не зламається.
- Використовуйте оператор розширення (
...) для гнучкого передавання кількох аргументів.
Зупинка подій
Якщо слухач повертає false, жодні додаткові слухачі для цієї події не будуть виконані. Це дозволяє зупинити ланцюг подій на основі конкретних умов. Пам’ятайте, порядок слухачів має значення, оскільки перший, що поверне false, зупинить решту від виконання.
Приклад:
Flight::onEvent('user.login', function ($username) {
if (isBanned($username)) {
logoutUser($username);
return false; // Зупиняє наступних слухачів
}
});
Flight::onEvent('user.login', function ($username) {
sendWelcomeEmail($username); // це ніколи не надсилається
});
Перевизначення методів подій
Flight::onEvent() та Flight::triggerEvent() доступні для розширення, що означає, що ви можете перевизначити, як вони працюють. Це чудово для просунутих користувачів, які хочуть налаштувати систему подій, наприклад, додавши логування або змінивши, як події розподіляються.
Приклад: Налаштування onEvent
Flight::map('onEvent', function (string $event, callable $callback) {
// Логувати кожну реєстрацію події
error_log("Додано нового слухача події для: $event");
// Викликати поведінку за замовчуванням (припускаючи внутрішню систему подій)
Flight::_onEvent($event, $callback);
});
Тепер щоразу, коли ви реєструєте подію, вона логуватиметься перед продовженням.
Чому перевизначати?
- Додати налагодження або моніторинг.
- Обмежити події в певних середовищах (наприклад, вимкнути в тестуванні).
- Інтегрувати з іншою бібліотекою подій.
Де розміщувати ваші події
Якщо ви новачок у концепціях подій у вашому проекті, ви можете запитати: де я реєструю всі ці події в моєму додатку? Простота Flight означає, що немає суворого правила — ви можете розміщувати їх де завгодно, що має сенс для вашого проекту. Однак, організація їх допомагає підтримувати код, коли додаток росте. Ось деякі практичні опції та найкращі практики, адаптовані до легкої природи Flight:
Опція 1: У вашому основному index.php
Для маленьких додатків або швидких прототипів ви можете реєструвати події прямо у файлі index.php поряд з маршрутами. Це тримає все в одному місці, що нормально, коли пріоритет — простота.
require 'vendor/autoload.php';
// Реєстрація подій
Flight::onEvent('user.login', function ($username) {
error_log("$username увійшов о " . date('Y-m-d H:i:s'));
});
// Визначення маршрутів
Flight::route('/login', function () {
$username = 'bob';
Flight::triggerEvent('user.login', $username);
echo "Увійшов!";
});
Flight::start();
- Переваги: Просто, без додаткових файлів, чудово для маленьких проектів.
- Недоліки: Може стати безладним, коли додаток росте з більше подіями та маршрутами.
Опція 2: Окремий файл events.php
Для трохи більшого додатку розгляньте перенесення реєстрацій подій у спеціальний файл на кшталт app/config/events.php. Включіть цей файл у ваш index.php перед маршрутами. Це імітує, як часто організовані маршрути в app/config/routes.php у проектах Flight.
// app/config/events.php
Flight::onEvent('user.login', function ($username) {
error_log("$username увійшов о " . date('Y-m-d H:i:s'));
});
Flight::onEvent('user.registered', function ($email, $name) {
echo "Email надіслано $email: Ласкаво просимо, $name!";
});
// index.php
require 'vendor/autoload.php';
require 'app/config/events.php';
Flight::route('/login', function () {
$username = 'bob';
Flight::triggerEvent('user.login', $username);
echo "Увійшов!";
});
Flight::start();
- Переваги: Тримає
index.phpзосередженим на маршрутизації, організовує події логічно, легко знайти та редагувати. - Недоліки: Додає крихітну структуру, що може здаватися надмірним для дуже маленьких додатків.
Опція 3: Близько до місця активації
Інший підхід — реєструвати події близько до місця їх активації, наприклад, всередині контролера або визначення маршруту. Це добре працює, якщо подія специфічна для однієї частини вашого додатку.
Flight::route('/signup', function () {
// Реєстрація події тут
Flight::onEvent('user.registered', function ($email) {
echo "Ласкаво просимо email надіслано $email!";
});
$email = 'jane@example.com';
Flight::triggerEvent('user.registered', $email);
echo "Зареєстровано!";
});
- Переваги: Тримає пов’язаний код разом, добре для ізольованих функцій.
- Недоліки: Розкидує реєстрації подій, роблячи важчим побачити всі події одразу; ризикує дублюванням реєстрацій, якщо не обережно.
Найкраща практика для Flight
- Починайте просто: Для крихітних додатків розміщуйте події в
index.php. Це швидко та узгоджується з мінімалізмом Flight. - Ростіть розумно: Коли додаток розширюється (наприклад, більше 5-10 подій), використовуйте файл
app/config/events.php. Це природний крок вгору, як організація маршрутів, і тримає код охайним без додавання складних фреймворків. - Уникайте надмірної інженерії: Не створюйте повноцінний клас "менеджер подій" або директорію, якщо додаток не величезний — Flight процвітає на простоті, тож тримайте це легким.
Порада: Групуйте за призначенням
У events.php групуйте пов’язані події (наприклад, всі події, пов’язані з користувачем, разом) з коментарями для ясності:
// app/config/events.php
// Події користувача
Flight::onEvent('user.login', function ($username) {
error_log("$username увійшов");
});
Flight::onEvent('user.registered', function ($email) {
echo "Ласкаво просимо $email!";
});
// Події сторінки
Flight::onEvent('page.updated', function ($pageId) {
Flight::cache()->delete("page_$pageId");
});
Ця структура добре масштабується та залишається дружньою для початківців.
Приклади з реального світу
Давайте пройдемося по деяких сценаріях з реального світу, щоб показати, як працюють події та чому вони корисні.
Приклад 1: Логування входу користувача
// Крок 1: Реєстрація слухача
Flight::onEvent('user.login', function ($username) {
$time = date('Y-m-d H:i:s');
error_log("$username увійшов о $time");
});
// Крок 2: Активація в додатку
Flight::route('/login', function () {
$username = 'bob'; // Уявіть, що це з форми
Flight::triggerEvent('user.login', $username);
echo "Привіт, $username!";
});
Чому це корисно: Код входу не потребує знати про логування — він просто активує подію. Ви можете пізніше додати більше слухачів (наприклад, надіслати email привітання) без зміни маршруту.
Приклад 2: Сповіщення про нових користувачів
// Слухач для нових реєстрацій
Flight::onEvent('user.registered', function ($email, $name) {
// Симулювати надсилання email
echo "Email надіслано $email: Ласкаво просимо, $name!";
});
// Активація, коли хтось реєструється
Flight::route('/signup', function () {
$email = 'jane@example.com';
$name = 'Jane';
Flight::triggerEvent('user.registered', $email, $name);
echo "Дякуємо за реєстрацію!";
});
Чому це корисно: Логіка реєстрації зосереджена на створенні користувача, тоді як подія обробляє сповіщення. Ви можете додати більше слухачів (наприклад, залогувати реєстрацію) пізніше.
Приклад 3: Очищення кешу
// Слухач для очищення кешу
Flight::onEvent('page.updated', function ($pageId) {
// якщо використовуєте плагін flightphp/cache
Flight::cache()->delete("page_$pageId");
echo "Кеш очищено для сторінки $pageId.";
});
// Активація при редагуванні сторінки
Flight::route('/edit-page/(@id)', function ($pageId) {
// Уявіть, що ми оновили сторінку
Flight::triggerEvent('page.updated', $pageId);
echo "Сторінка $pageId оновлена.";
});
Чому це корисно: Код редагування не турбується про кешування — він просто сигналізує оновлення. Інші частини додатку можуть реагувати за потреби.
Найкращі практики
- Називайте події ясно: Використовуйте конкретні назви на кшталт
'user.login'або'page.updated', щоб було очевидно, що вони роблять. - Тримайте слухачів простими: Не розміщуйте повільні або складні завдання в слухачах — тримайте додаток швидким.
- Тестуйте ваші події: Активуйте їх вручну, щоб переконатися, що слухачі працюють як очікується.
- Використовуйте події розумно: Вони чудові для розв’язання залежностей, але забагато може зробити код важким для слідкування — використовуйте їх, коли це має сенс.
Система подій у Flight PHP з Flight::onEvent() та Flight::triggerEvent() дає вам простий, але потужний спосіб будувати гнучкі додатки. Дозволяючи різним частинам додатку спілкуватися через події, ви можете тримати код організованим, повторно використовуваним та легким для розширення. Чи то логування дій, надсилання сповіщень, чи керування оновленнями, події допомагають робити це без заплутування логіки. Плюс, з можливістю перевизначення цих методів, у вас є свобода налаштувати систему під ваші потреби. Почніть з однієї події та спостерігайте, як це трансформує структуру вашого додатку!
Вбудовані події
Flight PHP має кілька вбудованих подій, які ви можете використовувати для підключення до життєвого циклу фреймворку. Ці події активуються в конкретних точках циклу запит/відповідь, дозволяючи виконувати власну логіку, коли певні дії відбуваються.
Список вбудованих подій
- flight.request.received:
function(Request $request)Активується, коли запит отримано, розібрано та оброблено. - flight.error:
function(Throwable $exception)Активується, коли виникає помилка під час життєвого циклу запиту. - flight.redirect:
function(string $url, int $status_code)Активується, коли ініціюється перенаправлення. - flight.cache.checked:
function(string $cache_key, bool $hit, float $executionTime)Активується, коли кеш перевіряється для конкретного ключа та чи був кеш хітом чи місом. - flight.middleware.before:
function(Route $route)Активується після виконання middleware перед. - flight.middleware.after:
function(Route $route)Активується після виконання middleware після. - flight.middleware.executed:
function(Route $route, $middleware, string $method, float $executionTime)Активується після виконання будь-якого middleware. - flight.route.matched:
function(Route $route)Активується, коли маршрут збігається, але ще не запущено. - flight.route.executed:
function(Route $route, float $executionTime)Активується після виконання та обробки маршруту.$executionTime— час, витрачений на виконання маршруту (виклик контролера тощо). - flight.view.rendered:
function(string $template_file_path, float $executionTime)Активується після рендерингу view.$executionTime— час, витрачений на рендеринг шаблону. Примітка: Якщо ви перевизначаєте методrender, вам потрібно буде повторно активувати цю подію. - flight.response.sent:
function(Response $response, float $executionTime)Активується після надсилання відповіді клієнту.$executionTime— час, витрачений на побудову відповіді.
Дивіться також
- Extending Flight - Як розширювати та налаштовувати основну функціональність Flight.
- Cache - Приклад використання подій для очищення кешу при оновленні сторінки.
Вирішення проблем
- Якщо ви не бачите виклику ваших слухачів подій, переконайтеся, що реєструєте їх перед активацією подій. Порядок реєстрації має значення.
Журнал змін
- v3.15.0 - Додано події до Flight.
Learn/templates
HTML-представлення та шаблони
Огляд
Flight надає базову функціональність HTML-шаблонізації за замовчуванням. Шаблонізація — це дуже ефективний спосіб відокремити логіку вашого застосунку від рівня представлення. Виділений рушій (Twig, Latte тощо) також дає інструментам ШІ для кодування знайомий, обмежений синтаксис, тож вони з меншою ймовірністю вкидатимуть бізнес-логіку у ваш HTML.
Розуміння
Коли ви створюєте застосунок, у вас, найімовірніше, буде HTML, який ви захочете повертати кінцевому користувачеві. Сам по собі PHP є мовою шаблонів, але дуже легко вплутати бізнес-логіку, як-от виклики бази даних, виклики API тощо, у ваш HTML-файл і зробити тестування та розділення дуже складним процесом. Передаючи дані в шаблон і дозволяючи шаблону відтворювати себе, набагато легше розділити та модульно тестувати ваш код. Ви будете нам вдячні, якщо користуватиметеся шаблонами!
Базове використання
Flight дозволяє замінити типовий рушій представлень, просто зіставивши render (або зареєструвавши клас представлення). Прокрутіть униз, щоб побачити Twig, Latte, Smarty, Blade тощо.
Типово для скелетона: Офіційний flightphp/skeleton використовує лише Twig у
app/views/(*.twig). Контролери викликають$this->app->render('welcome', $data)(розширення необов'язкове). Це вибір застосунку для нових проєктів, а не вимога ядра Flight. Latte та інші рушії залишаються повністю підтримуваними.
Twig
типово для скелетона
Twig — це гнучкий, швидкий і безпечний шаблонний рушій, який використовується Symfony та багатьма іншими PHP-проєктами. Інструменти ШІ для кодування, як правило, особливо добре знають Twig, і він за замовчуванням автоматично екранує вивід, що допомагає захистити від XSS.
Встановлення
composer require twig/twig
(Уже включено, коли ви виконуєте composer create-project flightphp/skeleton.)
Базова конфігурація
Перевизначте метод render, щоб використовувати Twig замість типового PHP-рендерера:
// перевизначаємо метод render, щоб використовувати Twig замість типового PHP-рендерера
Flight::map('render', function(string $template, array $data): void {
$loader = new \Twig\Loader\FilesystemLoader(Flight::get('flight.views.path'));
$twig = new \Twig\Environment($loader, [
// де Twig зберігає свої скомпільовані шаблони
'cache' => __DIR__ . '/../cache/twig',
'auto_reload' => true,
]);
// Дозволяємо "welcome" або "welcome.twig"
if (substr($template, -5) !== '.twig') {
$template .= '.twig';
}
echo $twig->render($template, $data);
});
У скелетоні ця прив'язка знаходиться у app/config/services.php (спільне середовище Twig, шлях до кешу, глобальні змінні, як-от base_url / nonce для CSP). Надавайте перевагу впровадженню Engine і викликайте $app->render() з контролерів, щоб код залишався дружнім до ШІ та тестування.
Використання Twig у Flight
Тепер, коли ви можете рендерити за допомогою Twig, ви можете зробити, наприклад, таке:
{# app/views/home.twig #}
<html>
<head>
<title>{% if title %}{{ title }} - {% endif %}My App</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Hello, {{ name }}!</h1>
</body>
</html>
// routes.php
Flight::route('/@name', function ($name) {
Flight::render('home.twig', [
'title' => 'Home Page',
'name' => $name
]);
});
Коли ви відвідаєте /Bob у своєму браузері, результат буде таким:
<html>
<head>
<title>Home Page - My App</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Hello, Bob!</h1>
</body>
</html>
Подальше читання
Більш повний приклад використання Twig із макетами наведено в розділі чудові плагіни цієї документації. Щодо метрик часу рендерингу на панелі Tracy, дивіться панель Twig у розширеннях Tracy.
Ви можете дізнатися більше про повні можливості Twig, прочитавши офіційну документацію.
Latte
чудова альтернатива
Latte — це повнофункціональний рушій із синтаксисом, схожим на PHP. Для застосунків на Flight це все ще чудовий вибір; скелетон просто стандартизує Twig як єдиний типовий рушій (особливо корисно, коли інструменти ШІ генерують шаблони).
Встановлення
composer require latte/latte
Базова конфігурація
Основна ідея полягає в тому, що ви перевизначаєте метод render, щоб використовувати Latte замість типового PHP-рендерера.
// перевизначаємо метод render, щоб використовувати Latte замість типового PHP-рендерера
Flight::map('render', function(string $template, array $data, ?string $block): void {
$latte = new Latte\Engine;
// де Latte зокрема зберігає свій кеш
$latte->setTempDirectory(__DIR__ . '/../cache/');
$finalPath = Flight::get('flight.views.path') . $template;
$latte->render($finalPath, $data, $block);
});
Використання Latte у Flight
Тепер, коли ви можете рендерити за допомогою Latte, ви можете зробити, наприклад, таке:
<!-- app/views/home.latte -->
<html>
<head>
<title>{$title ? $title . ' - '}My App</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Hello, {$name}!</h1>
</body>
</html>
// routes.php
Flight::route('/@name', function ($name) {
Flight::render('home.latte', [
'title' => 'Home Page',
'name' => $name
]);
});
Коли ви відвідаєте /Bob у своєму браузері, результат буде таким:
<html>
<head>
<title>Home Page - My App</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Hello, Bob!</h1>
</body>
</html>
Подальше читання
Більш складний приклад використання Latte з макетами наведено в розділі чудові плагіни цієї документації.
Ви можете дізнатися більше про повні можливості Latte, зокрема переклад і мовні можливості, прочитавши офіційну документацію.
Вбудований рушій представлень
застаріло
Примітка: Це все ще типова функціональність і технічно все ще працює.
Щоб відобразити шаблон представлення, викличте метод render із назвою файлу шаблону та необов'язковими даними шаблону:
Flight::render('hello.php', ['name' => 'Bob']);
Дані шаблону, які ви передаєте, автоматично впроваджуються в шаблон і можуть використовуватися як локальна змінна. Файли шаблонів — це просто PHP-файли. Якщо вміст файлу шаблону hello.php такий:
Hello, <?= $name ?>!
Результат буде таким:
Hello, Bob!
Ви також можете вручну встановити змінні представлення за допомогою методу set:
Flight::view()->set('name', 'Bob');
Змінна name тепер доступна у всіх ваших представленнях. Тож ви можете просто зробити:
Flight::render('hello');
Зверніть увагу, що під час вказання назви шаблону в методі render ви можете опустити розширення .php.
За замовчуванням Flight шукає каталог views для файлів шаблонів. Ви можете встановити альтернативний шлях для ваших шаблонів, задавши таку конфігурацію:
Flight::set('flight.views.path', '/path/to/views');
Макети
Для вебсайтів зазвичай використовують один файл макета зі змінним вмістом. Щоб відрендерити вміст, який буде використано в макеті, ви можете передати необов'язковий параметр методу render.
Flight::render('header', ['heading' => 'Hello'], 'headerContent');
Flight::render('body', ['body' => 'World'], 'bodyContent');
Ваше представлення тоді матиме збережені змінні headerContent та bodyContent. Після цього ви можете відрендерити макет так:
Flight::render('layout', ['title' => 'Home Page']);
Якщо файли шаблонів виглядають так:
header.php:
<h1><?= $heading ?></h1>
body.php:
<div><?= $body ?></div>
layout.php:
<html>
<head>
<title><?= $title ?></title>
</head>
<body>
<?= $headerContent ?>
<?= $bodyContent ?>
</body>
</html>
Результат буде таким:
<html>
<head>
<title>Home Page</title>
</head>
<body>
<h1>Hello</h1>
<div>World</div>
</body>
</html>
Smarty
Ось як ви можете використовувати рушій шаблонів Smarty для ваших представлень:
// Завантажуємо бібліотеку Smarty
require './Smarty/libs/Smarty.class.php';
// Реєструємо Smarty як клас представлення
// Також передаємо функцію зворотного виклику для налаштування Smarty під час завантаження
Flight::register('view', Smarty::class, [], function (Smarty $smarty) {
$smarty->setTemplateDir('./templates/');
$smarty->setCompileDir('./templates_c/');
$smarty->setConfigDir('./config/');
$smarty->setCacheDir('./cache/');
});
// Призначаємо дані шаблону
Flight::view()->assign('name', 'Bob');
// Відображаємо шаблон
Flight::view()->display('hello.tpl');
Для повноти варто також перевизначити типовий метод render у Flight:
Flight::map('render', function(string $template, array $data): void {
Flight::view()->assign($data);
Flight::view()->display($template);
});
Blade
Ось як ви можете використовувати рушій шаблонів Blade для ваших представлень:
Спершу потрібно встановити бібліотеку BladeOne через Composer:
composer require eftec/bladeone
Потім ви можете налаштувати BladeOne як клас представлення у Flight:
<?php
// Завантажуємо бібліотеку BladeOne
use eftec\bladeone\BladeOne;
// Реєструємо BladeOne як клас представлення
// Також передаємо функцію зворотного виклику для налаштування BladeOne під час завантаження
Flight::register('view', BladeOne::class, [], function (BladeOne $blade) {
$views = __DIR__ . '/../views';
$cache = __DIR__ . '/../cache';
$blade->setPath($views);
$blade->setCompiledPath($cache);
});
// Призначаємо дані шаблону
Flight::view()->share('name', 'Bob');
// Відображаємо шаблон
echo Flight::view()->run('hello', []);
Для повноти варто також перевизначити типовий метод render у Flight:
<?php
Flight::map('render', function(string $template, array $data): void {
echo Flight::view()->run($template, $data);
});
У цьому прикладі файл шаблону hello.blade.php може виглядати так:
<?php
Hello, {{ $name }}!
Результат буде таким:
Hello, Bob!
Дивіться також
- Встановлення — Розташування скелетона (
app/views/*.twig) для нових проєктів. - Розширення — Як перевизначити метод
render, щоб використовувати інший рушій шаблонів. - Маршрутизація — Як зіставляти маршрути з контролерами та рендерити представлення.
- Відповіді — Як налаштовувати HTTP-відповіді.
- Безпека — Автоматичне екранування та XSS.
- ШІ та досвід розробника — Чому один типовий рушій представлень допомагає агентам з кодування.
- Чому фреймворк? — Як шаблони вписуються в загальну картину.
Усунення проблем
- Якщо у вашому проміжному програмному забезпеченні є перенаправлення, але ваш застосунок, схоже, не перенаправляє, переконайтеся, що ви додали оператор
exit;у своє проміжне програмне забезпечення. - Якщо Twig не може знайти шаблон, перевірте
flight.views.pathі чи існує файл за цим шляхом із очікуваним розширенням (скелетон:app/views/).
Журнал змін
- Документація — Twig задокументовано як офіційний типовий рушій скелетона; Latte залишається повноцінною альтернативою.
- v2.0 — Початковий випуск.
Learn/simple_pdo
Клас помічника SimplePdo PDO
Огляд
Клас SimplePdo у Flight є сучасним, багатим на функції помічником для роботи з базами даних за допомогою PDO. Він розширює PdoWrapper та додає зручні методи-помічники для поширених операцій з базами даних, таких як insert(), update(), delete() та транзакції. Він спрощує завдання з базами даних, повертає результати як Collections для легкого доступу та підтримує логування запитів і моніторинг продуктивності додатка (APM) для просунутих випадків використання.
Розуміння
Клас SimplePdo розроблений для того, щоб зробити роботу з базами даних у PHP набагато простішою. Замість жонглювання підготовленими запитами, режимами отримання даних та громіздкими SQL-операціями ви отримуєте чисті, прості методи для поширених завдань. Кожен рядок повертається як Collection, тому ви можете використовувати як нотацію масиву ($row['name']), так і нотацію об'єкта ($row->name).
Цей клас є надмножиною PdoWrapper, тобто він включає всю функціональність PdoWrapper плюс додаткові методи-помічники, які роблять ваш код чистішим і легшим у підтримці. Якщо ви зараз використовуєте PdoWrapper, оновлення до SimplePdo є простим, оскільки він розширює PdoWrapper.
Ви можете зареєструвати SimplePdo як спільну послугу у Flight, а потім використовувати його будь-де у вашому додатку за допомогою Flight::db().
Основне використання
Реєстрація SimplePdo
Спочатку зареєструйте клас SimplePdo у Flight:
Flight::register('db', \flight\database\SimplePdo::class, [
'mysql:host=localhost;dbname=cool_db_name', 'user', 'pass', [
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'utf8mb4\'',
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_STRINGIFY_FETCHES => false,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
]
]);
ПРИМІТКА
Якщо ви не вкажете
PDO::ATTR_DEFAULT_FETCH_MODE,SimplePdoавтоматично встановить його наPDO::FETCH_ASSOCдля вас.
Тепер ви можете використовувати Flight::db() будь-де, щоб отримати з'єднання з базою даних.
Виконання запитів
runQuery()
function runQuery(string $sql, array $params = []): PDOStatement
Використовуйте це для INSERT, UPDATE або коли ви хочете отримати результати вручну:
$db = Flight::db();
$statement = $db->runQuery("SELECT * FROM users WHERE status = ?", ['active']);
while ($row = $statement->fetch()) {
// $row є масивом
}
Ви також можете використовувати його для записів:
$db->runQuery("INSERT INTO users (name) VALUES (?)", ['Alice']);
$db->runQuery("UPDATE users SET name = ? WHERE id = ?", ['Bob', 1]);
fetchField()
function fetchField(string $sql, array $params = []): mixed
Отримайте єдине значення з бази даних:
$count = Flight::db()->fetchField("SELECT COUNT(*) FROM users WHERE status = ?", ['active']);
fetchRow()
function fetchRow(string $sql, array $params = []): ?Collection
Отримайте єдиний рядок як Collection (доступ як до масиву/об'єкта):
$user = Flight::db()->fetchRow("SELECT * FROM users WHERE id = ?", [123]);
echo $user['name'];
// або
echo $user->name;
ПОРАДА
SimplePdoавтоматично додаєLIMIT 1до запитівfetchRow(), якщо його ще немає, роблячи ваші запити ефективнішими.
fetchAll()
function fetchAll(string $sql, array $params = []): array<Collection>
Отримайте всі рядки як масив Collections:
$users = Flight::db()->fetchAll("SELECT * FROM users WHERE status = ?", ['active']);
foreach ($users as $user) {
echo $user['name'];
// або
echo $user->name;
}
fetchColumn()
function fetchColumn(string $sql, array $params = []): array
Отримайте єдиний стовпець як масив:
$ids = Flight::db()->fetchColumn("SELECT id FROM users WHERE active = ?", [1]);
// Повертає: [1, 2, 3, 4, 5]
fetchPairs()
function fetchPairs(string $sql, array $params = []): array
Отримайте результати як пари ключ-значення (перший стовпець як ключ, другий як значення):
$userNames = Flight::db()->fetchPairs("SELECT id, name FROM users");
// Повертає: [1 => 'John', 2 => 'Jane', 3 => 'Bob']
Використання заповнювачів IN()
Ви можете використовувати єдиний ? у клаузі IN() та передати масив:
$ids = [1, 2, 3];
$users = Flight::db()->fetchAll("SELECT * FROM users WHERE id IN (?)", [$ids]);
Методи-помічники
Однією з основних переваг SimplePdo над PdoWrapper є додавання зручних методів-помічників для поширених операцій з базами даних.
insert()
function insert(string $table, array $data): string
Вставте один або більше рядків і поверніть останній ID вставки.
Єдина вставка:
$id = Flight::db()->insert('users', [
'name' => 'John',
'email' => 'john@example.com'
]);
Пакетна вставка:
$id = Flight::db()->insert('users', [
['name' => 'John', 'email' => 'john@example.com'],
['name' => 'Jane', 'email' => 'jane@example.com'],
]);
update()
function update(string $table, array $data, string $where, array $whereParams = []): int
Оновіть рядки та поверніть кількість уражених рядків:
$affected = Flight::db()->update(
'users',
['name' => 'Jane', 'email' => 'jane@example.com'],
'id = ?',
[1]
);
ПРИМІТКА
rowCount()у SQLite повертає кількість рядків, де дані дійсно змінилися. Якщо ви оновлюєте рядок тими самими значеннями, які він уже має,rowCount()поверне 0. Це відрізняється від поведінки MySQL при використанніPDO::MYSQL_ATTR_FOUND_ROWS.
delete()
function delete(string $table, string $where, array $whereParams = []): int
Видаліть рядки та поверніть кількість видалених рядків:
$deleted = Flight::db()->delete('users', 'id = ?', [1]);
transaction()
function transaction(callable $callback): mixed
Виконайте зворотний виклик у межах транзакції. Транзакція автоматично фіксується при успіху або скасовується при помилці:
$result = Flight::db()->transaction(function($db) {
$db->insert('users', ['name' => 'John']);
$db->insert('logs', ['action' => 'user_created']);
return $db->lastInsertId();
});
Якщо будь-яке виняток виникає в межах зворотного виклику, транзакція автоматично скасовується, а виняток повторно викидається.
Просунуте використання
Логування запитів та APM
Якщо ви хочете відстежувати продуктивність запитів, увімкніть відстеження APM під час реєстрації:
Flight::register('db', \flight\database\SimplePdo::class, [
'mysql:host=localhost;dbname=cool_db_name',
'user',
'pass',
[/* PDO options */],
[
'trackApmQueries' => true,
'maxQueryMetrics' => 1000
]
]);
Після виконання запитів ви можете логувати їх вручну, але APM логуватиме їх автоматично, якщо увімкнено:
Flight::db()->logQueries();
Це викличе подію (flight.db.queries) з метриками з'єднання та запитів, яку ви можете слухати за допомогою системи подій Flight.
Повний приклад
Flight::route('/users', function () {
// Отримайте всіх користувачів
$users = Flight::db()->fetchAll('SELECT * FROM users');
// Потоково отримайте всіх користувачів
$statement = Flight::db()->runQuery('SELECT * FROM users');
while ($user = $statement->fetch()) {
echo $user['name'];
}
// Отримайте єдиного користувача
$user = Flight::db()->fetchRow('SELECT * FROM users WHERE id = ?', [123]);
// Отримайте єдине значення
$count = Flight::db()->fetchField('SELECT COUNT(*) FROM users');
// Отримайте єдиний стовпець
$ids = Flight::db()->fetchColumn('SELECT id FROM users');
// Отримайте пари ключ-значення
$userNames = Flight::db()->fetchPairs('SELECT id, name FROM users');
// Спеціальний синтаксис IN()
$users = Flight::db()->fetchAll('SELECT * FROM users WHERE id IN (?)', [[1,2,3,4,5]]);
// Вставте нового користувача
$id = Flight::db()->insert('users', [
'name' => 'Bob',
'email' => 'bob@example.com'
]);
// Пакетна вставка користувачів
Flight::db()->insert('users', [
['name' => 'Bob', 'email' => 'bob@example.com'],
['name' => 'Jane', 'email' => 'jane@example.com']
]);
// Оновіть користувача
$affected = Flight::db()->update('users', ['name' => 'Bob'], 'id = ?', [123]);
// Видаліть користувача
$deleted = Flight::db()->delete('users', 'id = ?', [123]);
// Використовуйте транзакцію
$result = Flight::db()->transaction(function($db) {
$db->insert('users', ['name' => 'John', 'email' => 'john@example.com']);
$db->insert('audit_log', ['action' => 'user_created']);
return $db->lastInsertId();
});
});
Міграція з PdoWrapper
Якщо ви зараз використовуєте PdoWrapper, міграція до SimplePdo є простою:
-
Оновіть вашу реєстрацію:
// Старий Flight::register('db', \flight\database\PdoWrapper::class, [ /* ... */ ]); // Новий Flight::register('db', \flight\database\SimplePdo::class, [ /* ... */ ]); -
Усі існуючі методи
PdoWrapperпрацюють уSimplePdo- Немає руйнівних змін. Ваш існуючий код продовжить працювати. -
Опціонально використовуйте нові методи-помічники - Почніть використовувати
insert(),update(),delete()таtransaction(), щоб спростити ваш код.
Дивіться також
- Collections - Дізнайтеся, як використовувати клас Collection для легкого доступу до даних.
- PdoWrapper - Спадковий клас-помічник PDO (застарілий).
Вирішення проблем
- Якщо ви отримуєте помилку щодо з'єднання з базою даних, перевірте ваш DSN, ім'я користувача, пароль та опції.
- Усі рядки повертаються як Collections — якщо вам потрібен звичайний масив, використовуйте
$collection->getData(). - Для запитів
IN (?)переконайтеся, що ви передаєте масив. - Якщо ви стикаєтеся з проблемами пам'яті при логуванні запитів у довготривалих процесах, налаштуйте опцію
maxQueryMetrics.
Журнал змін
- v3.18.0 - Початковий реліз SimplePdo з методами-помічниками для insert, update, delete та транзакцій.
Learn/collections
Колекції
Огляд
Клас Collection у Flight — це зручний інструмент для керування наборами даних. Він дозволяє отримувати доступ до даних і маніпулювати ними, використовуючи як синтаксис масивів, так і синтаксис об'єктів, що робить ваш код чистішим і гнучкішим.
Розуміння
Collection — це, по суті, обгортка навколо масиву, але з додатковими можливостями. Ви можете використовувати її як масив, перебирати її, підраховувати елементи та навіть звертатися до елементів як до властивостей об'єкта. Це особливо корисно, коли потрібно передавати структуровані дані у вашому застосунку або коли ви хочете зробити код трохи зрозумілішим.
Колекції реалізують кілька інтерфейсів PHP:
ArrayAccess(тож ви можете використовувати синтаксис масивів)Iterator(тож ви можете перебирати за допомогоюforeach)Countable(тож ви можете використовуватиcount())JsonSerializable(тож ви можете легко конвертувати в JSON)
Базове використання
Створення колекції
Ви можете створити колекцію, просто передавши масив у її конструктор:
use flight\util\Collection;
$data = [
'name' => 'Flight',
'version' => 3,
'features' => ['routing', 'views', 'extending']
];
$collection = new Collection($data);
Доступ до елементів
Ви можете отримувати доступ до елементів, використовуючи синтаксис масиву або об'єкта:
// Синтаксис масиву
echo $collection['name']; // Результат: FlightPHP
// Синтаксис об'єкта
echo $collection->version; // Результат: 3
Якщо ви спробуєте отримати доступ до ключа, якого не існує, ви отримаєте null замість помилки.
Встановлення елементів
Ви також можете встановлювати елементи, використовуючи будь-який із синтаксисів:
// Синтаксис масиву
$collection['author'] = 'Mike Cao';
// Синтаксис об'єкта
$collection->license = 'MIT';
Перевірка та видалення елементів
Перевірте, чи існує елемент:
if (isset($collection['name'])) {
// Зробити щось
}
if (isset($collection->version)) {
// Зробити щось
}
Видаліть елемент:
unset($collection['author']);
unset($collection->license);
Перебір колекції
Колекції є ітерованими, тому ви можете використовувати їх у циклі foreach:
foreach ($collection as $key => $value) {
echo "$key: $value\n";
}
Підрахунок елементів
Ви можете підрахувати кількість елементів у колекції:
echo count($collection); // Результат: 4
Отримання всіх ключів або даних
Отримати всі ключі:
$keys = $collection->keys(); // ['name', 'version', 'features', 'license']
Отримати всі дані у вигляді масиву:
$data = $collection->getData();
Очищення колекції
Видалити всі елементи:
$collection->clear();
Серіалізація в JSON
Колекції можна легко конвертувати в JSON:
echo json_encode($collection);
// Результат: {"name":"FlightPHP","version":3,"features":["routing","views","extending"],"license":"MIT"}
Розширене використання
Ви можете повністю замінити внутрішній масив даних, якщо це необхідно:
$collection->setData(['foo' => 'bar']);
Колекції особливо корисні, коли потрібно передавати структуровані дані між компонентами або коли ви хочете забезпечити більш об'єктно-орієнтований інтерфейс до даних масиву.
Дивіться також
- Requests — дізнайтеся, як обробляти HTTP-запити та як колекції можна використовувати для керування даними запитів.
- SimplePdo — помічник бази даних, який повертає рядки запитів як колекції.
Усунення неполадок
- Якщо ви спробуєте отримати доступ до ключа, якого не існує, ви отримаєте
nullзамість помилки. - Пам'ятайте, що колекції не є рекурсивними: вкладені масиви автоматично не перетворюються на колекції.
- Якщо потрібно скинути колекцію, використовуйте
$collection->clear()або$collection->setData([]).
Журнал змін
- v3.0 — покращені підказки типів і підтримка PHP 8+.
- v1.0 — перший випуск класу Collection.
Learn/flight_vs_fat_free
Flight проти Fat-Free
Що таке Fat-Free?
Fat-Free (відомо як F3) — це потужний, але простий у використанні PHP-мікрофреймворк, створений, щоб допомогти вам швидко створювати динамічні та надійні веб-застосунки.
Flight багато в чому можна порівняти з Fat-Free, і, ймовірно, це найближчий родич за функціями та простотою. Fat-Free має багато функцій, яких немає у Flight, але також має багато функцій, які є у Flight. Fat-Free починає показувати свій вік, і він уже не такий популярний, як колись.
Оновлення стають дедалі рідшими, а спільнота вже не така активна, як раніше. Код досить простий, але іноді відсутність дисципліни в синтаксисі може ускладнювати читання та розуміння. Він працює з PHP 8.3, але сам код все ще виглядає так, ніби він живе у PHP 5.3.
Переваги порівняно з Flight
- Fat-Free має трохи більше зірок на GitHub, ніж Flight.
- Fat-Free має пристойну документацію, але в деяких місцях їй бракує чіткості.
- Fat-Free має кілька розрізнених ресурсів, як-от відеоуроки на YouTube та онлайн-статті, які можна використати для вивчення фреймворку.
- Fat-Free має кілька корисних плагінів, вбудованих у фреймворк, які іноді стають у пригоді.
- Fat-Free має вбудовану ORM під назвою Mapper, яку можна використовувати для взаємодії з базою даних. Flight має active-record.
- Fat-Free має вбудовані Сесії, Кешування та локалізацію. Flight вимагає використання сторонніх бібліотек, але це описано в документації.
- Fat-Free має невелику групу плагінів, створених спільнотою, які можна використовувати для розширення фреймворку. Flight має деякі з них, описані на сторінках документації та прикладів.
- Fat-Free, як і Flight, не має залежностей.
- Fat-Free, як і Flight, орієнтований на надання розробнику контролю над своїм застосунком і простий досвід розробника.
- Fat-Free зберігає зворотну сумісність, як і Flight (частково тому, що оновлення стають дедалі рідшими).
- Fat-Free, як і Flight, призначений для розробників, які вперше знайомляться зі світом фреймворків.
- Fat-Free має вбудований шаблонний рушій, який є надійнішим, ніж шаблонний рушій Flight. Flight рекомендує Latte для досягнення цієї мети.
- Fat-Free має унікальну команду типу CLI "route", за допомогою якої можна створювати CLI-застосунки прямо у Fat-Free і ставитися до них майже як до
GET-запиту. Flight досягає цього за допомогою runway.
Недоліки порівняно з Flight
- Fat-Free має деякі тести реалізації і навіть власний дуже базовий клас test. Однак він не покритий модульними тестами на 100%, як Flight.
- Щоб знайти щось у документації, доводиться використовувати пошукові системи на кшталт Google.
- Flight має темний режим на своєму сайті документації. (мікрофон впав)
- Fat-Free має деякі модулі, які, на жаль, не підтримуються.
- Flight має SimplePdo для доступу до бази даних, що трохи простіше, ніж вбудований клас
DB\SQLу Fat-Free (і є кращим вибором, ніж застарілий PdoWrapper). - Flight має плагін дозволів, який можна використовувати для захисту вашого застосунку. Fat-Free вимагає використання сторонньої бібліотеки.
- Flight має ORM під назвою active-record, який більше схожий на ORM, ніж Mapper у Fat-Free. Додаткова перевага
active-recordполягає в тому, що ви можете визначати зв'язки між записами для автоматичних з'єднань, тоді як Mapper у Fat-Free вимагає створення SQL-представлень. - Як не дивно, Fat-Free не має кореневого простору імен. Flight має простори імен наскрізь, щоб не конфліктувати з вашим власним кодом. Клас
Cacheтут є найбільшим порушником. - Fat-Free не має middleware. Натомість існують хуки
beforerouteтаafterroute, які можна використовувати для фільтрації запитів і відповідей у контролерах. - Fat-Free не вміє групувати маршрути.
- Fat-Free має обробник контейнера впровадження залежностей, але документація про його використання надзвичайно мізерна.
- Налагодження може бути дещо складним, оскільки практично все зберігається в так званому
HIVE.
Learn/extending
Розширення
Огляд
Flight розроблений як розширюваний фреймворк. Фреймворк постачається з набором типових методів та компонентів, але дозволяє вам відображати власні методи, реєструвати власні класи або навіть перевизначати існуючі класи та методи.
Розуміння
Існує 2 способи, якими ви можете розширити функціональність Flight:
- Відображення методів - Це використовується для створення простих власних методів, які ви можете викликати з будь-якого місця у вашому додатку. Ці методи зазвичай використовуються для утилітарних функцій, які ви хочете викликати з будь-якого місця у вашому коді.
- Реєстрація класів - Це використовується для реєстрації власних класів у Flight. Це зазвичай використовується для класів, які мають залежності або потребують конфігурації.
Ви також можете перевизначати існуючі методи фреймворку, щоб змінити їхню типову поведінку, щоб краще відповідати потребам вашого проекту.
Якщо ви шукаєте DIC (Dependency Injection Container), перейдіть до сторінки Dependency Injection Container.
Основне використання
Перевизначення методів фреймворку
Flight дозволяє вам перевизначати свою типову функціональність, щоб відповідати вашим потребам, без необхідності змінювати будь-який код. Ви можете переглянути всі методи, які можна перевизначити нижче.
Наприклад, коли Flight не може співставити URL з маршрутом, він викликає метод notFound,
який надсилає загальну відповідь HTTP 404. Ви можете перевизначити цю поведінку,
використовуючи метод map:
Flight::map('notFound', function() {
// Відображення власної сторінки 404
include 'errors/404.html';
});
Flight також дозволяє вам замінити основні компоненти фреймворку. Наприклад, ви можете замінити типовий клас Router на власний власний клас:
// create your custom Router class
class MyRouter extends \flight\net\Router {
// override methods here
// for example a shortcut for GET requests to remove
// the pass route feature
public function get($pattern, $callback, $alias = '') {
return parent::get($pattern, $callback, false, $alias);
}
}
// Register your custom class
Flight::register('router', MyRouter::class);
// When Flight loads the Router instance, it will load your class
$myRouter = Flight::router();
$myRouter->get('/hello', function() {
echo "Hello World!";
}, 'hello_alias');
Методи фреймворку, такі як map та register, однак не можуть бути перевизначені. Ви отримаєте
помилку, якщо спробуєте це зробити (знову ж таки див. нижче для списку методів).
Методи фреймворку, що можна відображати
Наступне є повним набором методів для фреймворку. Він складається з основних методів, які є звичайними статичними методами, та розширюваних методів, які є відображеними методами, що можуть бути відфільтровані або перевизначені.
Основні методи
Ці методи є основними для фреймворку і не можуть бути перевизначені.
Flight::map(string $name, callable $callback, bool $pass_route = false) // Creates a custom framework method.
Flight::register(string $name, string $class, array $params = [], ?callable $callback = null) // Registers a class to a framework method.
Flight::unregister(string $name) // Unregisters a class to a framework method.
Flight::before(string $name, callable $callback) // Adds a filter before a framework method.
Flight::after(string $name, callable $callback) // Adds a filter after a framework method.
Flight::path(string $path) // Adds a path for autoloading classes.
Flight::get(string $key) // Gets a variable set by Flight::set().
Flight::set(string $key, mixed $value) // Sets a variable within the Flight engine.
Flight::has(string $key) // Checks if a variable is set.
Flight::clear(array|string $key = []) // Clears a variable.
Flight::init() // Initializes the framework to its default settings.
Flight::app() // Gets the application object instance
Flight::request() // Gets the request object instance
Flight::response() // Gets the response object instance
Flight::router() // Gets the router object instance
Flight::view() // Gets the view object instance
Розширювані методи
Flight::start() // Starts the framework.
Flight::stop() // Stops the framework and sends a response.
Flight::halt(int $code = 200, string $message = '') // Stop the framework with an optional status code and message.
Flight::route(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') // Maps a URL pattern to a callback.
Flight::post(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') // Maps a POST request URL pattern to a callback.
Flight::put(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') // Maps a PUT request URL pattern to a callback.
Flight::patch(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') // Maps a PATCH request URL pattern to a callback.
Flight::delete(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') // Maps a DELETE request URL pattern to a callback.
Flight::group(string $pattern, callable $callback) // Creates grouping for urls, pattern must be a string.
Flight::getUrl(string $name, array $params = []) // Generates a URL based on a route alias.
Flight::redirect(string $url, int $code) // Redirects to another URL.
Flight::download(string $filePath) // Downloads a file.
Flight::render(string $file, array $data, ?string $key = null) // Renders a template file.
Flight::error(Throwable $error) // Sends an HTTP 500 response.
Flight::notFound() // Sends an HTTP 404 response.
Flight::etag(string $id, string $type = 'string') // Performs ETag HTTP caching.
Flight::lastModified(int $time) // Performs last modified HTTP caching.
Flight::json(mixed $data, int $code = 200, bool $encode = true, string $charset = 'utf8', int $option) // Sends a JSON response.
Flight::jsonp(mixed $data, string $param = 'jsonp', int $code = 200, bool $encode = true, string $charset = 'utf8', int $option) // Sends a JSONP response.
Flight::jsonHalt(mixed $data, int $code = 200, bool $encode = true, string $charset = 'utf8', int $option) // Sends a JSON response and stops the framework.
Flight::onEvent(string $event, callable $callback) // Registers an event listener.
Flight::triggerEvent(string $event, ...$args) // Triggers an event.
Будь-які власні методи, додані з map та register, також можуть бути відфільтровані. Для прикладів, як фільтрувати ці методи, див. посібник Filtering Methods.
Розширювані класи фреймворку
Існує кілька класів, функціональність яких ви можете перевизначити, розширюючи їх і реєструючи власний клас. Ці класи є:
Flight::app() // Application class - extend the flight\Engine class
Flight::request() // Request class - extend the flight\net\Request class
Flight::response() // Response class - extend the flight\net\Response class
Flight::router() // Router class - extend the flight\net\Router class
Flight::view() // View class - extend the flight\template\View class
Flight::eventDispatcher() // Event Dispatcher class - extend the flight\core\Dispatcher class
Відображення власних методів
Щоб відобразити власний простий власний метод, ви використовуєте функцію map:
// Map your method
Flight::map('hello', function (string $name) {
echo "hello $name!";
});
// Call your custom method
Flight::hello('Bob');
Хоча можливо створювати прості власні методи, рекомендується просто створювати стандартні функції в PHP. Це має автодоповнення в IDE та легше читати. Еквівалент наведеного вище коду буде:
function hello(string $name) {
echo "hello $name!";
}
hello('Bob');
Це використовується більше, коли вам потрібно передавати змінні у ваш метод, щоб отримати очікуване
значення. Використання методу register() нижче більше для передачі конфігурації,
а потім виклику вашого попередньо налаштованого класу.
Реєстрація власних класів
Щоб зареєструвати власний клас і налаштувати його, ви використовуєте функцію register. Перевага цього над map() полягає в тому, що ви можете повторно використовувати той самий клас, коли викликаєте цю функцію (було б корисно з Flight::db(), щоб ділитися тим самим екземпляром).
// Register your class
Flight::register('user', User::class);
// Get an instance of your class
$user = Flight::user();
Метод register також дозволяє вам передавати параметри конструктору вашого класу. Отже, коли ви завантажуєте ваш власний клас, він буде попередньо ініціалізований. Ви можете визначити параметри конструктора, передавши додатковий масив. Ось приклад завантаження з'єднання з базою даних:
// Register class with constructor parameters
Flight::register('db', PDO::class, ['mysql:host=localhost;dbname=test', 'user', 'pass']);
// Get an instance of your class
// This will create an object with the defined parameters
//
// new PDO('mysql:host=localhost;dbname=test','user','pass');
//
$db = Flight::db();
// and if you needed it later in your code, you just call the same method again
class SomeController {
public function __construct() {
$this->db = Flight::db();
}
}
Якщо ви передасте додатковий параметр callback, він буде виконаний відразу після конструкції класу. Це дозволяє вам виконувати будь-які процедури налаштування для вашого нового об'єкта. Функція callback приймає один параметр, екземпляр нового об'єкта.
// The callback will be passed the object that was constructed
Flight::register(
'db',
PDO::class,
['mysql:host=localhost;dbname=test', 'user', 'pass'],
function (PDO $db) {
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
);
За замовчуванням, кожного разу, коли ви завантажуєте ваш клас, ви отримаєте спільний екземпляр.
Щоб отримати новий екземпляр класу, просто передайте false як параметр:
// Shared instance of the class
$shared = Flight::db();
// New instance of the class
$new = Flight::db(false);
Note: Keep in mind that mapped methods have precedence over registered classes. If you declare both using the same name, only the mapped method will be invoked.
Приклади
Ось деякі приклади того, як ви можете розширити Flight функціональністю, яка не вбудована в ядро.
Логування
Flight не має вбудованої системи логування, однак, дуже легко використовувати бібліотеку логування з Flight. Ось приклад використання бібліотеки Monolog:
// services.php
// Register the logger with Flight
Flight::register('log', Monolog\Logger::class, [ 'name' ], function(Monolog\Logger $log) {
$log->pushHandler(new Monolog\Handler\StreamHandler('path/to/your.log', Monolog\Logger::WARNING));
});
Тепер, коли це зареєстровано, ви можете використовувати його у вашому додатку:
// In your controller or route
Flight::log()->warning('This is a warning message');
Це запише повідомлення у файл логу, який ви вказали. А що, якщо ви хочете записати щось, коли виникає
помилка? Ви можете використовувати метод error:
// In your controller or route
Flight::map('error', function(Throwable $ex) {
Flight::log()->error($ex->getMessage());
// Display your custom error page
include 'errors/500.html';
});
Ви також можете створити базову систему APM (Application Performance Monitoring),
використовуючи методи before та after:
// In your services.php file
Flight::before('start', function() {
Flight::set('start_time', microtime(true));
});
Flight::after('start', function() {
$end = microtime(true);
$start = Flight::get('start_time');
Flight::log()->info('Request '.Flight::request()->url.' took ' . round($end - $start, 4) . ' seconds');
// You could also add your request or response headers
// to log them as well (be careful as this would be a
// lot of data if you have a lot of requests)
Flight::log()->info('Request Headers: ' . json_encode(Flight::request()->headers));
Flight::log()->info('Response Headers: ' . json_encode(Flight::response()->headers));
});
Кешування
Flight не має вбудованої системи кешування, однак, дуже легко використовувати бібліотеку кешування з Flight. Ось приклад використання бібліотеки PHP File Cache:
// services.php
// Register the cache with Flight
Flight::register('cache', \flight\Cache::class, [ __DIR__ . '/../cache/' ], function(\flight\Cache $cache) {
$cache->setDevMode(ENVIRONMENT === 'development');
});
Тепер, коли це зареєстровано, ви можете використовувати його у вашому додатку:
// In your controller or route
$data = Flight::cache()->get('my_cache_key');
if (empty($data)) {
// Do some processing to get the data
$data = [ 'some' => 'data' ];
Flight::cache()->set('my_cache_key', $data, 3600); // cache for 1 hour
}
Легке створення об'єктів DIC
Якщо ви використовуєте DIC (Dependency Injection Container) у вашому додатку, ви можете використовувати Flight, щоб допомогти вам створювати ваші об'єкти. Ось приклад використання бібліотеки Dice:
// services.php
// create a new container
$container = new \Dice\Dice;
// don't forget to reassign it to itself like below!
$container = $container->addRule('PDO', [
// shared means that the same object will be returned each time
'shared' => true,
'constructParams' => ['mysql:host=localhost;dbname=test', 'user', 'pass' ]
]);
// now we can create a mappable method to create any object.
Flight::map('make', function($class, $params = []) use ($container) {
return $container->create($class, $params);
});
// This registers the container handler so Flight knows to use it for controllers/middleware
Flight::registerContainerHandler(function($class, $params) {
Flight::make($class, $params);
});
// lets say we have the following sample class that takes a PDO object in the constructor
class EmailCron {
protected PDO $pdo;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
public function send() {
// code that sends an email
}
}
// And finally you can create objects using dependency injection
$emailCron = Flight::make(EmailCron::class);
$emailCron->send();
Snazzy right?
Див. також
- Dependency Injection Container - Як використовувати DIC з Flight.
- File Cache - Приклад використання бібліотеки кешування з Flight.
Вирішення проблем
- Пам'ятайте, що відображені методи мають пріоритет над зареєстрованими класами. Якщо ви оголосите обидва з однаковим іменем, буде викликано лише відображений метод.
Журнал змін
- v2.0 - Початковий реліз.
Learn/json
JSON Wrapper
Огляд
Клас Json у Flight надає простий, послідовний спосіб кодування та декодування JSON-даних у вашому додатку. Він обгортає вбудовані функції JSON PHP з кращою обробкою помилок і деякими корисними налаштуваннями за замовчуванням, роблячи роботу з JSON простішою та безпечнішою.
Розуміння
Робота з JSON є надзвичайно поширеною в сучасних PHP-додатках, особливо під час створення API або обробки AJAX-запитів. Клас Json централізує все кодування та декодування JSON, тому вам не потрібно турбуватися про дивні крайні випадки або загадкові помилки від вбудованих функцій PHP.
Ключові особливості:
- Послідовна обробка помилок (викидає винятки у разі невдачі)
- Налаштування за замовчуванням для кодування/декодування (наприклад, неекрановані слеші)
- Утилітарні методи для красивого виведення та перевірки
Основне використання
Кодування даних у JSON
Щоб перетворити PHP-даний у рядок JSON, використовуйте Json::encode():
use flight\util\Json;
$data = [
'framework' => 'Flight',
'version' => 3,
'features' => ['routing', 'views', 'extending']
];
$json = Json::encode($data);
echo $json;
// Output: {"framework":"Flight","version":3,"features":["routing","views","extending"]}
Якщо кодування не вдається, ви отримаєте виняток з корисним повідомленням про помилку.
Красиве виведення
Хочете, щоб ваш JSON був читабельним для людини? Використовуйте prettyPrint():
echo Json::prettyPrint($data);
/*
{
"framework": "Flight",
"version": 3,
"features": [
"routing",
"views",
"extending"
]
}
*/
Декодування рядків JSON
Щоб перетворити рядок JSON назад у PHP-даний, використовуйте Json::decode():
$json = '{"framework":"Flight","version":3}';
$data = Json::decode($json);
echo $data->framework; // Output: Flight
Якщо ви хочете асоціативний масив замість об'єкта, передайте true як другий аргумент:
$data = Json::decode($json, true);
echo $data['framework']; // Output: Flight
Якщо декодування не вдається, ви отримаєте виняток з чітким повідомленням про помилку.
Перевірка JSON
Перевірте, чи є рядок валідним JSON:
if (Json::isValid($json)) {
// Він валідний!
} else {
// Не валідний JSON
}
Отримання останньої помилки
Якщо ви хочете перевірити останнє повідомлення про помилку JSON (з вбудованих функцій PHP):
$error = Json::getLastError();
if ($error !== '') {
echo "Last JSON error: $error";
}
Розширене використання
Ви можете налаштувати параметри кодування та декодування, якщо вам потрібно більше контролю (див. опції json_encode PHP):
// Кодування з опцією HEX_TAG
$json = Json::encode($data, JSON_HEX_TAG);
// Декодування з користувацькою глибиною
$data = Json::decode($json, false, 1024);
Дивіться також
- Collections - Для роботи зі структурованими даними, які можна легко перетворити на JSON.
- Configuration - Як налаштувати ваш додаток Flight.
- Extending - Як додати власні утиліти або перевизначити основні класи.
Вирішення проблем
- Якщо кодування або декодування не вдається, викидається виняток — обгорніть ваші виклики в try/catch, якщо хочете обробляти помилки граціозно.
- Якщо ви отримуєте несподівані результати, перевірте ваші дані на циклічні посилання або не-UTF8 символи.
- Використовуйте
Json::isValid(), щоб перевірити, чи є рядок валідним JSON перед декодуванням.
Журнал змін
- v3.16.0 - Додано утиліту класу обгортки JSON.
Learn/flight_vs_slim
Flight проти Slim
Що таке Slim?
Slim — це PHP мікрофреймворк, який допомагає швидко створювати прості, але потужні веб-додатки та API.
Значна частина натхнення для деяких функцій v3 у Flight насправді прийшла від Slim. Групування маршрутів та виконання middleware у певному порядку — це дві функції, натхненні Slim. Slim v3 вийшов орієнтованим на простоту, але щодо v4 є суперечливі відгуки.
Переваги порівняно з Flight
- Slim має більшу спільноту розробників, які, своєю чергою, створюють зручні модулі, щоб допомогти вам не винаходити велосипед.
- Slim дотримується багатьох інтерфейсів і стандартів, поширених у PHP-спільноті, що підвищує інтероперабельність.
- Slim має пристойну документацію та навчальні посібники, які можна використовувати для вивчення фреймворку (хоча це ніщо порівняно з Laravel або Symfony).
- Slim має різноманітні ресурси, як-от відеоуроки на YouTube та онлайн-статті, які можна використовувати для вивчення фреймворку.
- Slim дозволяє використовувати будь-які компоненти для обробки основних функцій маршрутизації, оскільки він сумісний з PSR-7.
Недоліки порівняно з Flight
- Як не дивно, Slim не такий швидкий, як можна було б очікувати від мікрофреймворку. Детальніше див. у бенчмарках TechEmpower.
- Flight орієнтований на розробника, який хоче створити легкий, швидкий та простий у використанні веб-додаток.
- Flight не має залежностей, тоді як Slim має декілька залежностей, які необхідно встановити.
- Flight орієнтований на простоту та зручність використання.
- Однією з ключових особливостей Flight є те, що він докладає всіх зусиль для збереження зворотної сумісності. Перехід від Slim v3 до v4 був руйнівною зміною.
- Flight призначений для розробників, які вперше занурюються у світ фреймворків.
- Flight також може створювати застосунки корпоративного рівня, але він не має стільки прикладів і посібників, скільки Slim. Це також вимагатиме більшої дисципліни з боку розробника, щоб підтримувати організованість і структурованість.
- Flight дає розробнику більше контролю над застосунком, тоді як Slim може непомітно додавати трохи магії за лаштунками.
- Flight має SimplePdo для доступу до бази даних (йому надають перевагу перед застарілим PdoWrapper). Slim вимагає використання сторонньої бібліотеки.
- Flight має плагін дозволів, який можна використовувати для захисту вашого застосунку. Slim вимагає використання сторонньої бібліотеки.
- Flight має ORM під назвою active-record, який можна використовувати для взаємодії з вашою базою даних. Slim вимагає використання сторонньої бібліотеки.
- Flight має CLI-застосунок під назвою runway, який можна використовувати для запуску вашого застосунку з командного рядка. Slim такого не має.
Learn/autoloading
Автозавантаження
Огляд
Автозавантаження — це концепція в PHP, коли ви вказуєте каталог або каталоги для завантаження класів. Це набагато корисніше, ніж використання require або include для завантаження класів. Це також вимога для використання пакетів Composer.
Правильне налаштування автозавантаження важливе і для розробки з підтримкою AI: агенти розміщують файли там, куди вказує простір імен. Якщо регістр папки та простору імен не збігаються, на Linux з'являтимуться помилки "клас не знайдено", навіть якщо все "працювало" на нечутливому до регістру диску Mac.
Розуміння
За замовчуванням будь-який клас Flight автозавантажується автоматично завдяки Composer. Для ваших класів застосунку є два поширені підходи:
- Composer PSR-4 (те, що використовує офіційний скелет): зіставте префікс простору імен з каталогом у
composer.json, потім виконайтеcomposer dump-autoload. Flight::path(): вкажіть завантажувачу Flight каталоги (зручно для простих застосунків або коли ви не використовуєте Composer для коду застосунку).
Використання автозавантажувача значно спрощує ваш код. Замість стіни include / require на початку кожного файлу класи завантажуються, коли ви вперше їх використовуєте.
Чутливість до регістру (прочитайте двічі)
Простори імен мають збігатися зі структурою каталогів і регістром літер цих каталогів.
| Працює | Ламається на Linux |
|---|---|
App\Controller\HomeController → app/Controller/HomeController.php |
App\Controller\… з папкою app/controllers/ |
app\controllers\MyController → app/controllers/MyController.php |
Змішування App\ з нижнім регістром controllers |
Простори імен PHP не чутливі до регістру в деяких контекстах, але Composer і файлова система — ні. Офіційний скелет стандартизує так:
- Composer:
"App\\": "app/" - Папки:
Controller,Middleware,Model,Utils(PascalCase), а неcontrollers/middlewares
У старішій документації та прикладах спільноти іноді використовувався нижній регістр app\controllers. Це досі працює, якщо ваші папки в нижньому регістрі — але нові проєкти-скелети використовують App\ + папки PascalCase. Виберіть одну конвенцію для проєкту й дотримуйтеся її, щоб люди та AI-інструменти не створювали другу структуру.
Скелет (рекомендовано для нових проєктів)
Після composer create-project flightphp/skeleton код застосунку автозавантажується через Composer — Flight::path() не потрібен для класів App\:
{
"autoload": {
"psr-4": {
"App\\": "app/"
}
}
}
// app/Controller/HomeController.php
namespace App\Controller;
use flight\Engine;
class HomeController
{
protected Engine $app;
public function __construct(Engine $app)
{
$this->app = $app;
}
public function index(): void
{
$this->app->render('welcome', ['message' => 'Hello!']);
}
}
// app/config/routes.php — Dice розв'язує App\Controller\… через контейнер
$router->get('/', [HomeController::class, 'index']);
Дивіться Встановлення для повного дерева та AI та досвід розробника для інформації про те, як AGENTS.md документує цю структуру для помічників з кодування.
Базове використання (Flight::path())
Припустимо, у нас є дерево каталогів, подібне до наступного:
# Приклад шляху
/home/user/project/my-flight-project/
├── app
│ ├── cache
│ ├── config
│ ├── controllers - містить контролери для цього проєкту
│ ├── translations
│ ├── UTILS - містить класи лише для цього застосунку (цей каталог у верхньому регістрі навмисно, для прикладу нижче)
│ └── views
└── public
└── css
└── js
└── index.php
Ви могли помітити, що це схоже на типове дерево застосунку (сайт документації використовує структурований макет). Нижній регістр controllers тут є допустимим вибором — це просто не поточний стандарт скелета.
Ви можете вказати кожен каталог для завантаження ось так:
/**
* public/index.php
*/
// Додаємо шлях до автозавантажувача
Flight::path(__DIR__.'/../app/controllers/');
Flight::path(__DIR__.'/../app/utils/');
/**
* app/controllers/MyController.php
*/
// простір імен не потрібен
// Усі автозавантажені класи рекомендується називати в Pascal Case (кожне слово з великої літери, без пробілів)
class MyController {
public function index() {
// щось робимо
}
}
Простори імен з Flight::path()
Якщо у вас є простори імен, це насправді стає дуже простим. Ви повинні використовувати метод Flight::path() для вказівки кореневого каталогу (не кореня документа або папки public/) вашого застосунку.
/**
* public/index.php
*/
// Додаємо шлях до автозавантажувача
Flight::path(__DIR__.'/../');
Тепер ось як може виглядати ваш контролер. Подивіться на приклад нижче, але зверніть увагу на коментарі з важливою інформацією.
/**
* app/controllers/MyController.php
*/
// простори імен обов'язкові
// простори імен збігаються зі структурою каталогів
// простори імен мають відповідати регістру структури каталогів
// простори імен і каталоги не можуть містити символів підкреслення (якщо не встановлено Loader::setV2ClassLoading(false))
namespace app\controllers;
// Усі автозавантажені класи рекомендується називати в Pascal Case (кожне слово з великої літери, без пробілів)
// Починаючи з 3.7.2, ви можете використовувати Pascal_Snake_Case для назв класів, викликавши Loader::setV2ClassLoading(false);
class MyController {
public function index() {
// щось робимо
}
}
І якщо ви хотіли автозавантажити клас у вашому каталозі utils, ви б зробили практично те саме:
/**
* app/UTILS/ArrayHelperUtil.php
*/
// простір імен має відповідати структурі каталогів і регістру (зверніть увагу, що каталог UTILS у верхньому регістрі,
// як у дереві файлів вище)
namespace app\UTILS;
class ArrayHelperUtil {
public function changeArrayCase(array $array) {
// щось робимо
}
}
Простір імен у стилі скелета (ті самі правила, інший регістр)
/**
* app/Controller/MyController.php
*/
namespace App\Controller;
class MyController {
// ...
}
Правило не змінилося — змінився лише вибраний регістр папок/просторів імен у скелеті. Який би регістр ви не використовували для папок, ваш рядок namespace має йому відповідати.
Символи підкреслення в назвах класів
Починаючи з 3.7.2, ви можете використовувати Pascal_Snake_Case для назв класів, викликавши Loader::setV2ClassLoading(false);.
Це дозволить вам використовувати символи підкреслення в назвах класів.
Це не рекомендовано, але доступно для тих, кому це потрібно.
use flight\core\Loader;
/**
* public/index.php
*/
// Додаємо шлях до автозавантажувача
Flight::path(__DIR__.'/../app/controllers/');
Flight::path(__DIR__.'/../app/utils/');
Loader::setV2ClassLoading(false);
/**
* app/controllers/My_Controller.php
*/
// простір імен не потрібен
class My_Controller {
public function index() {
// щось робимо
}
}
Дивіться також
- Встановлення - Дерево скелета та стандартні значення
App\для нових проєктів. - Маршрутизація - Як зіставляти маршрути з контролерами та рендерити подання.
- Впровадження залежностей - Як контролери отримують
Engineта сервіси. - AI та досвід розробника - Узгоджуйте агентів зі своєю структурою за допомогою
AGENTS.md. - Чому фреймворк? - Розуміння переваг використання фреймворку, такого як Flight.
Виправлення неполадок
- Якщо ви не можете зрозуміти, чому ваші класи з просторами імен не знаходяться, пам'ятайте: за допомогою
Flight::path()вказуйте корінь проєкту (або правильну базу для вашого простору імен), а не лише вкладену папку, яку ви забули віддзеркалити в просторі імен. - З Composer PSR-4 запустіть
composer dump-autoloadпісля зміни зіставлень уcomposer.json. - На Linux CI або у продакшені неправильний регістр папки — дуже поширена помилка "працює на моїй машині".
Клас не знайдено (автозавантаження не працює)
Це може статися з кількох причин. Нижче наведено кілька прикладів.
Неправильна назва файлу
Найпоширеніша причина — назва класу не збігається з назвою файлу.
Якщо у вас є клас з назвою MyClass, то файл має називатися MyClass.php. Якщо у вас є клас з назвою MyClass, а файл називається myclass.php,
то автозавантажувач не зможе його знайти.
Неправильний простір імен або регістр папки
Якщо ви використовуєте простори імен, простір імен має відповідати структурі каталогів, включно з регістром.
// ...код...
// якщо ваш MyController знаходиться в app/Controller (скелет) і має простір імен App\Controller
// це не працюватиме:
Flight::route('/hello', 'MyController->hello');
// Стиль скелета:
use App\Controller\MyController;
Flight::route('/hello', [ MyController::class, 'hello' ]);
// Старіший макет з нижнім регістром (тільки якщо ваші папки насправді app/controllers):
use app\controllers\MyController;
Flight::route('/hello', [ MyController::class, 'hello' ]);
// або повністю кваліфікований:
Flight::route('/hello', [ 'App\Controller\MyController', 'hello' ]);
path() не визначено (код застосунку без Composer)
Якщо ви покладаєтеся на Flight::path() замість Composer для класів застосунку, визначте шлях перед маршрутами, які посилаються на ці класи (часто на початку bootstrap або public/index.php):
// Додаємо шлях до автозавантажувача (корінь проєкту для застосунків з просторами імен)
Flight::path(__DIR__.'/../');
Офіційний скелет переважно використовує Composer PSR-4 для App\, тому зазвичай вам не потрібен Flight::path() для контролерів і моделей там.
Журнал змін
- Документація – Документування скелета
App\+ папок PascalCase і проблем із чутливістю до регістру для людей та AI-інструментів. - v3.7.2 - Ви можете використовувати Pascal_Snake_Case для назв класів, викликавши
Loader::setV2ClassLoading(false); - v2.0 - Додано функціонал автозавантаження.
Learn/uploaded_file
Обробник Завантаженого Файлу
Огляд
Клас UploadedFile у Flight полегшує та робить безпечним обробку завантаження файлів у вашому додатку. Він обгортає деталі процесу завантаження файлів PHP, надаючи вам простий, об'єктно-орієнтований спосіб доступу до інформації про файл та переміщення завантажених файлів.
Розуміння
Коли користувач завантажує файл через форму, PHP зберігає інформацію про файл у суперглобальній змінній $_FILES. У Flight ви рідко взаємодієте з $_FILES безпосередньо. Натомість об'єкт Request у Flight (доступний через Flight::request()) надає метод getUploadedFiles(), який повертає масив об'єктів UploadedFile, роблячи обробку файлів набагато зручнішою та надійнішою.
Клас UploadedFile надає методи для:
- Отримання оригінальної назви файлу, типу MIME, розміру та тимчасового розташування
- Перевірки помилок завантаження
- Переміщення завантаженого файлу до постійного розташування
Цей клас допомагає уникнути поширених помилок з завантаженням файлів, таких як обробка помилок чи безпечне переміщення файлів.
Основне Використання
Доступ до Завантажених Файлів з Запиту
Рекомендований спосіб доступу до завантажених файлів — через об'єкт запиту:
Flight::route('POST /upload', function() {
// Для поля форми з назвою <input type="file" name="myFile">
$uploadedFiles = Flight::request()->getUploadedFiles();
$file = $uploadedFiles['myFile'];
// Тепер ви можете використовувати методи UploadedFile
if ($file->getError() === UPLOAD_ERR_OK) {
$file->moveTo('/path/to/uploads/' . $file->getClientFilename());
echo "Файл успішно завантажено!";
} else {
echo "Завантаження не вдалося: " . $file->getError();
}
});
Обробка Кількох Завантажень Файлів
Якщо ваша форма використовує name="myFiles[]" для кількох завантажень, ви отримаєте масив об'єктів UploadedFile:
Flight::route('POST /upload', function() {
// Для поля форми з назвою <input type="file" name="myFiles[]">
$uploadedFiles = Flight::request()->getUploadedFiles();
foreach ($uploadedFiles['myFiles'] as $file) {
if ($file->getError() === UPLOAD_ERR_OK) {
$file->moveTo('/path/to/uploads/' . $file->getClientFilename());
echo "Завантажено: " . $file->getClientFilename() . "<br>";
} else {
echo "Не вдалося завантажити: " . $file->getClientFilename() . "<br>";
}
}
});
Створення Екземпляра UploadedFile Вручну
Зазвичай ви не створюватимете UploadedFile вручну, але можете, якщо потрібно:
use flight\net\UploadedFile;
$file = new UploadedFile(
$_FILES['myfile']['name'],
$_FILES['myfile']['type'],
$_FILES['myfile']['size'],
$_FILES['myfile']['tmp_name'],
$_FILES['myfile']['error']
);
Доступ до Інформації про Файл
Ви можете легко отримати деталі про завантажений файл:
echo $file->getClientFilename(); // Оригінальна назва файлу з комп'ютера користувача
echo $file->getClientMediaType(); // Тип MIME (наприклад, image/png)
echo $file->getSize(); // Розмір файлу в байтах
echo $file->getTempName(); // Тимчасовий шлях до файлу на сервері
echo $file->getError(); // Код помилки завантаження (0 означає відсутність помилки)
Переміщення Завантаженого Файлу
Після перевірки файлу перемістіть його до постійного розташування:
try {
$file->moveTo('/path/to/uploads/' . $file->getClientFilename());
echo "Файл успішно завантажено!";
} catch (Exception $e) {
echo "Завантаження не вдалося: " . $e->getMessage();
}
Метод moveTo() викличе виняток, якщо щось піде не так (наприклад, помилка завантаження чи проблема з правами доступу).
Обробка Помилок Завантаження
Якщо під час завантаження виникла проблема, ви можете отримати повідомлення про помилку, зрозуміле для людини:
if ($file->getError() !== UPLOAD_ERR_OK) {
// Ви можете використовувати код помилки або зловити виняток від moveTo()
echo "Виникла помилка під час завантаження файлу.";
}
Дивіться Також
- Requests - Дізнайтеся, як доступатися до завантажених файлів з HTTP-запитів та перегляньте більше прикладів завантаження файлів.
- Configuration - Як налаштувати ліміти завантаження та директорії в PHP.
- Extending - Як налаштувати або розширити основні класи Flight.
Вирішення Проблем
- Завжди перевіряйте
$file->getError()перед переміщенням файлу. - Переконайтеся, що директорія завантаження доступна для запису веб-сервером.
- Якщо
moveTo()не вдається, перевірте повідомлення винятку для деталей. - Налаштування
upload_max_filesizeтаpost_max_sizeу PHP можуть обмежувати завантаження файлів. - Для кількох завантажень файлів завжди перебирайте масив об'єктів
UploadedFile.
Журнал Змін
- v3.12.0 - Додано клас
UploadedFileдо об'єкта запиту для легшої обробки файлів.
Guides/unit_testing
Unit-тестування у Flight PHP з PHPUnit
Цей посібник знайомить з unit-тестуванням у Flight PHP за допомогою PHPUnit, і розрахований на початківців, які хочуть зрозуміти чому unit-тестування важливе та як застосовувати його на практиці. Ми зосередимось на тестуванні поведінки — перевірці, що ваш застосунок робить те, що ви очікуєте, наприклад, надсилає електронний лист або зберігає запис, а не на тривіальних обчисленнях. Ми почнемо з простого обробника маршрутів і перейдемо до складнішого контролера, використовуючи впровадження залежностей (DI) та макетування сторонніх сервісів.
Чому unit-тестування?
Unit-тестування гарантує, що ваш код поводиться очікувано, виявляючи помилки до того, як вони потрапляють у продакшн. Воно особливо корисне у Flight, де легка маршрутизація та гнучкість можуть призводити до складних взаємодій. Для соло-розробників або команд unit-тести слугують страхувальною сіткою, документуючи очікувану поведінку та запобігаючи регресіям, коли ви повертаєтесь до коду пізніше. Вони також покращують дизайн: код, який важко тестувати, часто сигналізує про надто складні або тісно пов'язані класи.
На відміну від спрощених прикладів (наприклад, тестування x * y = z), ми зосередимось на реальних поведінках, таких як перевірка вхідних даних, збереження даних або ініціювання дій на кшталт надсилання листів. Наша мета — зробити тестування доступним і значущим.
Загальні керівні принципи
- Тестуйте поведінку, а не реалізацію: Зосереджуйтесь на результатах (наприклад, «лист надіслано» або «запис збережено»), а не на внутрішніх деталях. Це робить тести стійкими до рефакторингу.
- Перестаньте використовувати
Flight::: Статичні методи Flight неймовірно зручні, але ускладнюють тестування. Вам варто звикнути використовувати змінну$appз$app = Flight::app();.$appмає всі ті ж методи, що йFlight::. Ви все одно зможете використовувати$app->route()або$this->app->json()у своєму контролері тощо. Також варто використовувати справжній маршрутизатор Flight через$router = $app->router(), і тоді ви зможете використовувати$router->get(),$router->post(),$router->group()тощо. Див. Маршрутизація. - Тримайте тести швидкими: Швидкі тести спонукають до частого запуску. Уникайте повільних операцій, як-от виклики бази даних в unit-тестах. Якщо у вас повільний тест, це ознака того, що ви пишете інтеграційний тест, а не unit-тест. Інтеграційні тести — це коли ви фактично задіюєте реальні бази даних, реальні HTTP-виклики, реальне надсилання листів тощо. Вони мають своє місце, але вони повільні та можуть бути нестабільними, тобто іноді падають з невідомої причини.
- Використовуйте описові назви: Назви тестів мають чітко описувати поведінку, яка тестується. Це покращує читабельність і супроводжуваність.
- Уникайте глобальних змінних, як чуми: Мінімізуйте використання
$app->set()і$app->get(), оскільки вони діють як глобальний стан, вимагаючи макетів у кожному тесті. Віддавайте перевагу DI або контейнеру DI (див. Контейнер впровадження залежностей). Навіть використання методу$app->map()технічно є «глобальним» станом, і його варто уникати на користь DI. Використовуйте бібліотеку сесій, наприклад flightphp/session, щоб мати змогу макетувати об'єкт сесії у ваших тестах. Не викликайте$_SESSIONбезпосередньо у вашому коді, оскільки це вносить глобальну змінну у ваш код, що ускладнює тестування. - Використовуйте впровадження залежностей: Впроваджуйте залежності (наприклад,
PDO, поштові сервіси) у контролери, щоб ізолювати логіку та спростити макетування. Якщо у вас клас із забагато залежностей, розгляньте можливість рефакторингу його на менші класи, кожен з яких має єдину відповідальність згідно з принципами SOLID. - Макетуйте сторонні сервіси: Макетуйте бази даних, HTTP-клієнти (cURL) або поштові сервіси, щоб уникнути зовнішніх викликів. Тестуйте на один-два рівні вглиб, але дайте вашій основній логіці виконуватись. Наприклад, якщо ваш застосунок надсилає SMS, ви НЕ хочете реально надсилати SMS щоразу під час запуску тестів, бо ці витрати накопичуватимуться (і це буде повільніше). Натомість змакетуйте сервіс SMS і просто перевірте, що ваш код викликав сервіс SMS із правильними параметрами.
- Прагніть високого покриття, а не досконалості: 100% покриття рядків — це добре, але це не означає, що все у вашому коді протестовано так, як треба (можете дослідити покриття гілок/шляхів у PHPUnit). Пріоритетними є критичні поведінки (наприклад, реєстрація користувача, відповіді API та фіксація невдалих відповідей).
- Використовуйте контролери для маршрутів: У визначеннях маршрутів використовуйте контролери, а не замикання.
flight\Engine $appвпроваджується в кожен контролер через конструктор за замовчуванням. У тестах використовуйте$app = new Flight\Engine(), щоб створити екземпляр Flight у межах тесту, впровадіть його у ваш контролер і викликайте методи безпосередньо (наприклад,$controller->register()). Див. Розширення Flight та Маршрутизація. - Оберіть стиль макетування і дотримуйтесь його: PHPUnit підтримує кілька стилів макетування (наприклад, prophecy, вбудовані макети), або ви можете використовувати анонімні класи, які мають свої переваги, як-от автодоповнення коду, ламання, якщо ви змінюєте визначення методу, тощо. Просто будьте послідовні у своїх тестах. Див. PHPUnit Mock Objects.
- Використовуйте видимість
protectedдля методів/властивостей, які ви хочете тестувати в підкласах: Це дозволяє перевизначати їх у тестових підкласах, не роблячи їх публічними; це особливо корисно для анонімних класів-макетів.
Налаштування PHPUnit
Спершу налаштуйте PHPUnit у вашому проєкті Flight PHP за допомогою Composer для зручного тестування. Більше деталей див. у посібнику PHPUnit для початківців.
-
У каталозі вашого проєкту виконайте:
composer require --dev phpunit/phpunitЦе встановить останню версію PHPUnit як залежність для розробки.
-
Створіть каталог
testsу корені проєкту для тестових файлів. -
Додайте тестовий скрипт до
composer.jsonдля зручності:// інший вміст composer.json "scripts": { "test": "phpunit --configuration phpunit.xml" } -
Створіть файл
phpunit.xmlу корені:<?xml version="1.0" encoding="UTF-8"?> <phpunit bootstrap="vendor/autoload.php"> <testsuites> <testsuite name="Flight Tests"> <directory>tests</directory> </testsuite> </testsuites> </phpunit>
Тепер, коли ваші тести створені, ви можете запускати composer test для їх виконання.
Тестування простого обробника маршруту
Почнімо з базового маршруту, який перевіряє email користувача. Ми тестуватимемо його поведінку: повернення повідомлення про успіх для коректних email та помилку для некоректних. Для перевірки email ми використовуємо filter_var.
// index.php
$app->route('POST /register', [ UserController::class, 'register' ]);
// UserController.php
class UserController {
protected $app;
public function __construct(flight\Engine $app) {
$this->app = $app;
}
public function register() {
$email = $this->app->request()->data->email;
$responseArray = [];
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$responseArray = ['status' => 'error', 'message' => 'Invalid email'];
} else {
$responseArray = ['status' => 'success', 'message' => 'Valid email'];
}
$this->app->json($responseArray);
}
}
Щоб протестувати це, створіть тестовий файл. Більше про структуру тестів див. у розділі Unit-тестування та принципи SOLID:
// tests/UserControllerTest.php
use PHPUnit\Framework\TestCase;
use Flight;
use flight\Engine;
class UserControllerTest extends TestCase {
public function testValidEmailReturnsSuccess() {
$app = new Engine();
$request = $app->request();
$request->data->email = 'test@example.com'; // Імітація POST-даних
$UserController = new UserController($app);
$UserController->register($request->data->email);
$response = $app->response()->getBody();
$output = json_decode($response, true);
$this->assertEquals('success', $output['status']);
$this->assertEquals('Valid email', $output['message']);
}
public function testInvalidEmailReturnsError() {
$app = new Engine();
$request = $app->request();
$request->data->email = 'invalid-email'; // Імітація POST-даних
$UserController = new UserController($app);
$UserController->register($request->data->email);
$response = $app->response()->getBody();
$output = json_decode($response, true);
$this->assertEquals('error', $output['status']);
$this->assertEquals('Invalid email', $output['message']);
}
}
Ключові моменти:
- Ми імітуємо POST-дані за допомогою класу запиту. Не використовуйте глобальні змінні, як-от
$_POST,$_GETтощо, оскільки це ускладнює тестування (вам доведеться щоразу скидати ці значення, інакше інші тести можуть впасти). - Усі контролери за замовчуванням отримують екземпляр
flight\Engine, впроваджений у них, навіть без налаштованого DIC-контейнера. Це значно спрощує безпосереднє тестування контролерів. - Тут взагалі немає використання
Flight::, що робить код легшим для тестування. - Тести перевіряють поведінку: правильний статус і повідомлення для коректних/некоректних email.
Запустіть composer test, щоб перевірити, що маршрут поводиться очікувано. Більше про запити та відповіді у Flight див. у відповідній документації.
Використання впровадження залежностей для тестованих контролерів
Для складніших сценаріїв використовуйте впровадження залежностей (DI), щоб зробити контролери тестованими. Уникайте глобальних методів Flight (наприклад, Flight::set(), Flight::map(), Flight::register()), оскільки вони діють як глобальний стан, вимагаючи макетів для кожного тесту. Натомість використовуйте DI-контейнер Flight, DICE, PHP-DI або ручне DI.
Використаймо flight\database\SimplePdo замість сирого PDO. Цей хелпер набагато легше макетувати та тестувати (і він є кращим вибором порівняно із застарілим PdoWrapper).
Ось контролер, який зберігає користувача в базу даних і надсилає вітальний лист:
use flight\database\SimplePdo;
class UserController {
protected $app;
protected $db;
protected $mailer;
public function __construct(Engine $app, SimplePdo $db, MailerInterface $mailer) {
$this->app = $app;
$this->db = $db;
$this->mailer = $mailer;
}
public function register() {
$email = $this->app->request()->data->email;
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
// додавання return тут допомагає unit-тестуванню зупинити виконання
return $this->app->jsonHalt(['status' => 'error', 'message' => 'Invalid email']);
}
$this->db->runQuery('INSERT INTO users (email) VALUES (?)', [$email]);
$this->mailer->sendWelcome($email);
return $this->app->json(['status' => 'success', 'message' => 'User registered']);
}
}
Ключові моменти:
- Контролер залежить від екземпляра
SimplePdoтаMailerInterface(вигаданого стороннього поштового сервісу). - Залежності впроваджуються через конструктор, що дозволяє уникнути глобальних змінних.
Тестування контролера з макетами
Тепер протестуємо поведінку UserController: перевірку email, збереження в базу даних і надсилання листів. Ми змакетуємо базу даних і поштовий сервіс, щоб ізолювати контролер.
// tests/UserControllerDICTest.php
use flight\database\SimplePdo;
use PHPUnit\Framework\TestCase;
class UserControllerDICTest extends TestCase {
public function testValidEmailSavesAndSendsEmail() {
// Іноді необхідно змішувати стилі макетування
// Тут ми використовуємо вбудований макет PHPUnit для PDOStatement
$statementMock = $this->createMock(PDOStatement::class);
$statementMock->method('execute')->willReturn(true);
// Використовуємо анонімний клас для макетування SimplePdo
$mockDb = new class($statementMock) extends SimplePdo {
protected $statementMock;
public function __construct($statementMock) {
$this->statementMock = $statementMock;
}
// Коли ми так макетуємо, ми насправді не викликаємо базу даних.
// Ми можемо додатково налаштувати це, щоб змінити макет PDOStatement для симуляції помилок тощо.
public function runQuery(string $sql, array $params = []): PDOStatement {
return $this->statementMock;
}
};
$mockMailer = new class implements MailerInterface {
public $sentEmail = null;
public function sendWelcome($email): bool {
$this->sentEmail = $email;
return true;
}
};
$app = new Engine();
$app->request()->data->email = 'test@example.com';
$controller = new UserControllerDIC($app, $mockDb, $mockMailer);
$controller->register();
$response = $app->response()->getBody();
$result = json_decode($response, true);
$this->assertEquals('success', $result['status']);
$this->assertEquals('User registered', $result['message']);
$this->assertEquals('test@example.com', $mockMailer->sentEmail);
}
public function testInvalidEmailSkipsSaveAndEmail() {
$mockDb = new class() extends SimplePdo {
// Порожній конструктор обходить конструктор батьківського класу
public function __construct() {}
public function runQuery(string $sql, array $params = []): PDOStatement {
throw new Exception('Should not be called');
}
};
$mockMailer = new class implements MailerInterface {
public $sentEmail = null;
public function sendWelcome($email): bool {
throw new Exception('Should not be called');
}
};
$app = new Engine();
$app->request()->data->email = 'invalid-email';
// Потрібно зіставити jsonHalt, щоб уникнути виходу
$app->map('jsonHalt', function($data) use ($app) {
$app->json($data, 400);
});
$controller = new UserControllerDIC($app, $mockDb, $mockMailer);
$controller->register();
$response = $app->response()->getBody();
$result = json_decode($response, true);
$this->assertEquals('error', $result['status']);
$this->assertEquals('Invalid email', $result['message']);
}
}
Ключові моменти:
- Ми макетуємо
SimplePdoтаMailerInterface, щоб уникнути реальних викликів бази даних або email. - Тести перевіряють поведінку: коректні email ініціюють вставки в базу даних і надсилання листів; некоректні email пропускають обидві дії.
- Макетуйте сторонні залежності (наприклад,
SimplePdo,MailerInterface), дозволяючи логіці контролера виконуватись.
Надмірне макетування
Будьте обережні, щоб не макетувати занадто багато вашого коду. Наведу приклад нижче, чому це може бути погано, на прикладі нашого UserController. Ми змінимо цю перевірку на метод під назвою isEmailValid (використовуючи filter_var), а інші нові доповнення — в окремий метод під назвою registerUser.
use flight\database\SimplePdo;
use flight\Engine;
// UserControllerDICV2.php
class UserControllerDICV2 {
protected $app;
protected $db;
protected $mailer;
public function __construct(Engine $app, SimplePdo $db, MailerInterface $mailer) {
$this->app = $app;
$this->db = $db;
$this->mailer = $mailer;
}
public function register() {
$email = $this->app->request()->data->email;
if (!$this->isEmailValid($email)) {
// додавання return тут допомагає unit-тестуванню зупинити виконання
return $this->app->jsonHalt(['status' => 'error', 'message' => 'Invalid email']);
}
$this->registerUser($email);
$this->app->json(['status' => 'success', 'message' => 'User registered']);
}
protected function isEmailValid($email) {
return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
}
protected function registerUser($email) {
$this->db->runQuery('INSERT INTO users (email) VALUES (?)', [$email]);
$this->mailer->sendWelcome($email);
}
}
А тепер надмірно змакетований unit-тест, який насправді нічого не тестує:
use PHPUnit\Framework\TestCase;
class UserControllerTest extends TestCase {
public function testValidEmailSavesAndSendsEmail() {
$app = new Engine();
$app->request()->data->email = 'test@example.com';
// ми пропускаємо додаткове впровадження залежностей тут, бо це «легко»
$controller = new class($app) extends UserControllerDICV2 {
protected $app;
// Обходимо залежності в конструкторі
public function __construct($app) {
$this->app = $app;
}
// Просто примусово робимо це валідним.
protected function isEmailValid($email) {
return true; // Завжди повертаємо true, обходячи реальну перевірку
}
// Обходимо реальні виклики БД та поштового сервісу
protected function registerUser($email) {
return false;
}
};
$controller->register();
$response = $app->response()->getBody();
$result = json_decode($response, true);
$this->assertEquals('success', $result['status']);
$this->assertEquals('User registered', $result['message']);
}
}
Ура, у нас є unit-тести, і вони проходять! Але зачекайте, що як я насправді зміню внутрішню роботу isEmailValid або registerUser? Мої тести все одно проходитимуть, бо я змакетував усю функціональність. Покажу, що я маю на увазі.
// UserControllerDICV2.php
class UserControllerDICV2 {
// ... інші методи ...
protected function isEmailValid($email) {
// Змінена логіка
$validEmail = filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
// Тепер він має мати лише певний домен
$validDomain = strpos($email, '@example.com') !== false;
return $validEmail && $validDomain;
}
}
Якщо я запущу мої наведені вище unit-тести, вони все одно пройдуть! Але оскільки я не тестував поведінку (насправді дозволяючи деякому коду виконуватися), я, можливо, написав баг, який чекає на появу в продакшні. Тест слід змінити, щоб врахувати нову поведінку, а також протилежний випадок, коли поведінка не та, яку ми очікуємо.
Повний приклад
Повний приклад проєкту Flight PHP з unit-тестами ви можете знайти на GitHub: n0nag0n/flight-unit-tests-guide. Для глибшого розуміння див. Unit-тестування та принципи SOLID.
Типові помилки
- Надмірне макетування: Не макетуйте кожну залежність; дозвольте частині логіки (наприклад, валідації контролера) виконуватися, щоб тестувати реальну поведінку. Див. Unit-тестування та принципи SOLID.
- Глобальний стан: Активне використання глобальних PHP-змінних (наприклад,
$_SESSION,$_COOKIE) робить тести крихкими. Те саме стосуєтьсяFlight::. Рефакторіть код, щоб передавати залежності явно. - Складне налаштування: Якщо налаштування тесту є громіздким, ваш клас, можливо, має забагато залежностей або обов'язків, що порушує принципи SOLID.
Масштабування з unit-тестами
Unit-тести особливо корисні у більших проєктах або коли ви повертаєтесь до коду через місяці. Вони документують поведінку та виявляють регресії, рятуючи вас від повторного вивчення вашого застосунку. Для соло-розробників тестуйте критичні шляхи (наприклад, реєстрацію користувача, обробку платежів). Для команд тести забезпечують узгоджену поведінку серед усіх внесків. Більше про переваги фреймворків і тестів див. у розділі Чому фреймворки?.
Долучайтеся до репозиторію документації Flight PHP зі своїми порадами щодо тестування!
Автор: n0nag0n, 2025
Guides/blog
Створення простого блогу за допомогою Flight PHP
Цей посібник проведе вас через створення базового блогу з використанням PHP-фреймворку Flight. Ви налаштуєте проєкт, визначите маршрути, керуватимете публікаціями за допомогою JSON та відображатимете їх за допомогою шаблонізатора Latte — все це демонструє простоту та гнучкість Flight. Наприкінці у вас буде функціональний блог із головною сторінкою, сторінками окремих публікацій та формою створення.
Передумови
- PHP 7.4+: встановлений у вашій системі.
- Composer: для керування залежностями.
- Текстовий редактор: будь-який редактор, як-от VS Code або PHPStorm.
- Базові знання PHP та веб-розробки.
Крок 1: Налаштування вашого проєкту
Почніть зі створення нового каталогу проєкту та встановлення Flight через Composer.
-
Створіть каталог:
mkdir flight-blog cd flight-blog -
Встановіть Flight:
composer require flightphp/core -
Створіть публічний каталог: Flight використовує єдину точку входу (
index.php). Створіть папкуpublic/для неї:mkdir public -
Базовий
index.php: Створітьpublic/index.phpіз простим маршрутом "hello world":<?php require '../vendor/autoload.php'; Flight::route('/', function () { echo 'Hello, Flight!'; }); Flight::start(); -
Запустіть вбудований сервер: Перевірте ваше налаштування за допомогою сервера розробки PHP:
php -S localhost:8000 -t public/Відвідайте
http://localhost:8000, щоб побачити "Hello, Flight!".
Крок 2: Організація структури проєкту
Для чистого налаштування структуруйте свій проєкт так:
flight-blog/
├── app/
│ ├── config/
│ └── views/
├── data/
├── public/
│ └── index.php
├── vendor/
└── composer.json
app/config/: файли конфігурації (наприклад, події, маршрути).app/views/: шаблони для відображення сторінок.data/: файл JSON для зберігання публікацій блогу.public/: корінь веб-сайту зindex.php.
Крок 3: Встановлення та налаштування Latte
Latte — це легкий шаблонізатор, який добре інтегрується з Flight.
-
Встановіть Latte:
composer require latte/latte -
Налаштуйте Latte у Flight: Оновіть
public/index.php, щоб зареєструвати Latte як механізм перегляду:<?php require '../vendor/autoload.php'; use Latte\Engine; Flight::register('view', Engine::class, [], function ($latte) { $latte->setTempDirectory(__DIR__ . '/../cache/'); $latte->setLoader(new \Latte\Loaders\FileLoader(__DIR__ . '/../app/views/')); }); Flight::route('/', function () { Flight::view()->render('home.latte', ['title' => 'My Blog']); }); Flight::start(); -
Створіть шаблон макета: У
app/views/layout.latte:<!DOCTYPE html> <html> <head> <title>{$title}</title> </head> <body> <header> <h1>My Blog</h1> <nav> <a href="/">Home</a> | <a href="/create">Create a Post</a> </nav> </header> <main> {block content}{/block} </main> <footer> <p>© {date('Y')} Flight Blog</p> </footer> </body> </html> -
Створіть головний шаблон: У
app/views/home.latte:{extends 'layout.latte'} {block content} <h2>{$title}</h2> <ul> {foreach $posts as $post} <li><a href="/post/{$post['slug']}">{$post['title']}</a></li> {/foreach} </ul> {/block}Перезапустіть сервер, якщо ви вийшли з нього, і відвідайте
http://localhost:8000, щоб побачити відрендерену сторінку. -
Створіть файл даних:
Використовуйте файл JSON для імітації бази даних задля простоти.
У
data/posts.json:[ { "slug": "first-post", "title": "My First Post", "content": "This is my very first blog post with Flight PHP!" } ]
Крок 4: Визначення маршрутів
Виділіть свої маршрути в окремий файл конфігурації для кращої організації.
-
Створіть
routes.php: Уapp/config/routes.php:<?php Flight::route('/', function () { Flight::view()->render('home.latte', ['title' => 'My Blog']); }); Flight::route('/post/@slug', function ($slug) { Flight::view()->render('post.latte', ['title' => 'Post: ' . $slug, 'slug' => $slug]); }); Flight::route('GET /create', function () { Flight::view()->render('create.latte', ['title' => 'Create a Post']); }); -
Оновіть
index.php: Підключіть файл маршрутів:<?php require '../vendor/autoload.php'; use Latte\Engine; Flight::register('view', Engine::class, [], function ($latte) { $latte->setTempDirectory(__DIR__ . '/../cache/'); $latte->setLoader(new \Latte\Loaders\FileLoader(__DIR__ . '/../app/views/')); }); require '../app/config/routes.php'; Flight::start();
Крок 5: Зберігання та отримання публікацій блогу
Додайте методи для завантаження та збереження публікацій.
-
Додайте метод для публікацій: У
index.phpдодайте метод для завантаження публікацій:Flight::map('posts', function () { $file = __DIR__ . '/../data/posts.json'; return json_decode(file_get_contents($file), true); }); -
Оновіть маршрути: Змініть
app/config/routes.php, щоб використовувати публікації:<?php Flight::route('/', function () { $posts = Flight::posts(); Flight::view()->render('home.latte', [ 'title' => 'My Blog', 'posts' => $posts ]); }); Flight::route('/post/@slug', function ($slug) { $posts = Flight::posts(); $post = array_filter($posts, fn($p) => $p['slug'] === $slug); $post = reset($post) ?: null; if (!$post) { Flight::notFound(); return; } Flight::view()->render('post.latte', [ 'title' => $post['title'], 'post' => $post ]); }); Flight::route('GET /create', function () { Flight::view()->render('create.latte', ['title' => 'Create a Post']); });
Крок 6: Створення шаблонів
Оновіть шаблони для відображення публікацій.
-
Сторінка публікації (
app/views/post.latte):{extends 'layout.latte'} {block content} <h2>{$post['title']}</h2> <div class="post-content"> <p>{$post['content']}</p> </div> {/block}
Крок 7: Додавання створення публікацій
Обробка надсилання форми для додавання нових публікацій.
-
Форма (
app/views/create.latte):{extends 'layout.latte'} {block content} <h2>{$title}</h2> <form method="POST" action="/create"> <div class="form-group"> <label for="title">Title:</label> <input type="text" name="title" id="title" required> </div> <div class="form-group"> <label for="content">Content:</label> <textarea name="content" id="content" required></textarea> </div> <button type="submit">Save Post</button> </form> {/block} -
Додайте POST-маршрут: У
app/config/routes.php:Flight::route('POST /create', function () { $request = Flight::request(); $title = $request->data['title']; $content = $request->data['content']; $slug = strtolower(str_replace(' ', '-', $title)); $posts = Flight::posts(); $posts[] = ['slug' => $slug, 'title' => $title, 'content' => $content]; file_put_contents(__DIR__ . '/../../data/posts.json', json_encode($posts, JSON_PRETTY_PRINT)); Flight::redirect('/'); }); -
Перевірте:
- Відвідайте
http://localhost:8000/create. - Надішліть нову публікацію (наприклад, "Second Post" із якимось вмістом).
- Перевірте головну сторінку, щоб побачити її у списку.
- Відвідайте
Крок 8: Покращення обробки помилок
Перевизначте метод notFound для кращого досвіду з помилкою 404.
У index.php:
Flight::map('notFound', function () {
Flight::view()->render('404.latte', ['title' => 'Page Not Found']);
});
Створіть app/views/404.latte:
{extends 'layout.latte'}
{block content}
<h2>404 - {$title}</h2>
<p>Sorry, that page doesn't exist!</p>
{/block}
Наступні кроки
- Додайте стилі: Використовуйте CSS у своїх шаблонах для кращого вигляду.
- База даних: Замініть
posts.jsonна базу даних, як-от SQLite, використовуючи SimplePdo. - Валідація: Додайте перевірки на дублікати slug або порожні поля.
- Проміжне програмне забезпечення: Впровадьте автентифікацію для створення публікацій.
Висновок
Ви створили простий блог за допомогою Flight PHP! Цей посібник демонструє основні функції, як-от маршрутизацію, шаблонізацію за допомогою Latte та обробку надсилання форм — усе це залишається легким. Вивчайте документацію Flight, щоб дізнатися про більш розширені функції та розвинути свій блог далі!
License
Ліцензія MIT (MIT)
Авторське право © 2024 @mikecao, @n0nag0n
Цим надається дозвіл, безкоштовно, будь-якій особі, яка отримала копію цього програмного забезпечення та супутньої документації файлів (далі — “Програмне забезпечення”), користуватися Програмним забезпеченням без обмежень, включаючи без обмежень права на використання, копіювання, модифікацію, об’єднання, публікацію, розповсюдження, підліцензування та/або продаж копій Програмного забезпечення, а також дозволяти особам, яким Програмне забезпечення надано, робити це, за умов дотримання наступних умов:
Вищезазначене повідомлення про авторські права та це повідомлення про дозвіл повинні бути включені в усі копії або значні частини Програмного забезпечення.
ПРОГРАМНЕ ЗАБЕЗПЕЧЕННЯ НАДАЄТЬСЯ “ЯК Є”, БЕЗ ГАРАНТІЙ БУДЬ-ЯКОГО РОДУ, ЯВНИХ АБО ПРИХОВАНИХ, ВКЛЮЧАЮЧИ, АЛЕ НЕ ОБМЕЖУЮЧИСЬ ГАРАНТІЯМИ КОМЕРЦІЙНОЇ РІЗНИЧКИ, ПРИДАТНОСТІ ДЛЯ ПЕВНОЇ МЕТИ ТА НЕПORУШЕННЯ. У ЖОДНОМУ ВИПАДКУ АВТОРИ АБО ВЛАСНИКИ АВТОРСЬКИХ ПРАВ НЕ НЕСУТЬ ВІДПОВІДАЛЬНОСТІ ЗА БУДЬ-ЯКІ ПРЕТЕНЗІЇ, ЗБИТКИ АБО ІНШІ ЗОБОВ'ЯЗАННЯ, ЧИ У ПРАВОВІЙ СПРАВІ, ДЕЛІКТІ АБО ІНШОМУ, ЩО ВИНИКЛО, ВИРІСШЕ З, АБО У ЗВ'ЯЗКУ З ПРОГРАМНИМ ЗАБЕЗПЕЧЕННЯМ АБО ВИКОРИСТАННЯМ АБО ІНШИМИ УГОДАМИ У ПРОГРАМНОМУ ЗАБЕЗПЕЧЕННІ.
About
Flight PHP Framework
Flight — це швидкий, простий, розширюваний фреймворк для PHP, створений для розробників, які хочуть швидко виконувати роботу без зайвих проблем. Незалежно від того, чи створюєте ви класичний веб-додаток, блискавично швидкий API, чи працюєте з AI-асистентами програмування, низький розмір і проста конструкція Flight роблять його ідеальним вибором. Flight призначений бути легким, але також може задовольняти вимоги архітектури корпоративного рівня.
Чому обрати Flight?
- Зручний для початківців: Flight — чудова відправна точка для нових PHP-розробників. Його чітка структура і простий синтаксис допомагають вивчати веб-розробку без зайвого коду.
- Улюблений професіоналами: Досвідчені розробники люблять Flight за його гнучкість і контроль. Ви можете масштабувати від маленького прототипу до повноцінного додатка без зміни фреймворків.
- Зворотна сумісність: Ми цінуємо ваш час. Flight v3 є доповненням до v2, зберігаючи майже весь той самий API. Ми віримо в еволюцію, а не революцію — без "ламання світу" при кожному виході нової версії.
- Нуль залежностей: Ядро Flight повністю без залежностей — без поліфілів, без зовнішніх пакетів, навіть без PSR-інтерфейсів. Це означає менше векторів атак, менший розмір і відсутність несподіваних змін через залежності. Опціональні плагіни можуть мати залежності, але ядро завжди залишатиметься легким і безпечним.
- AI-дружній: Невелика поверхня API Flight та офіційний скелет (одна компоновка,
AGENTS.md, ін'єкція через конструктор) полегшують AI-інструментам програмування дотримання шаблонів. Та сама кодова база, незалежно від того, чи ви пишете кожен рядок самі, чи працюєте з агентом. Дізнайтеся більше про використання AI з Flight.
Відеогляд
Швидкий старт
Щоб швидко встановити базову версію, встановіть через Composer:
composer require flightphp/core
Або ви можете завантажити zip-архів репозиторію тут. Тоді у вас буде базовий файл index.php такого вигляду:
<?php
// якщо встановлено через composer
require 'vendor/autoload.php';
// або якщо встановлено вручну через zip-файл
// require 'flight/Flight.php';
Flight::route('/', function() {
echo 'hello world!';
});
Flight::route('/json', function() {
Flight::json([
'hello' => 'world'
]);
});
Flight::start();
Ось і все! У вас є базовий додаток Flight. Тепер ви можете запустити цей файл командою php -S localhost:8000 і перейти за адресою http://localhost:8000 у браузері, щоб побачити результат.
Короткі приклади Flight:: як цей чудово підходять для навчання та мікрододатків. Для повного макета проєкту, яким можуть користуватися люди та AI-інструменти, використовуйте скелет нижче.
Скелет/Boilerplate-додаток
Є офіційний стартер, щоб допомогти вам почати будь-який новий проєкт Flight. Він налаштовує структуру, конфігурацію, скрипти Composer та AI-дружні інструкції з самого початку.
Перегляньте flightphp/skeleton для готового проєкту або відвідайте сторінку прикладів для натхнення. Хочете деталі AI-робочого процесу? Дослідіть AI та досвід розробки.
Що ви отримуєте (високий рівень):
- Простори імен
App\з папками у PascalCase (app/Controller/,app/Middleware/,app/Model/, …) — регістр папки має відповідати простору імен (див. Автозавантаження) - Ін'єкція Dice +
Engine, щоб контролери залишалися тестованими (віддавайте перевагу$this->appнадFlight::у коді додатка) - Twig для представлень, SimplePdo + зразок ActiveRecord, Runway migrate
- Кореневий
AGENTS.md(плюс локальні копії) таSECURITY.mdдля асистентів і політики безпеки
Встановлення скелета додатка
Досить просто!
# Створити новий проєкт
composer create-project flightphp/skeleton my-project/
# Перейти до директорії нового проєкту
cd my-project/
# Запустити локальний dev-сервер, щоб одразу почати роботу!
composer start
Це створить структуру проєкту, скопіює config_sample.php → config.php (та .env.example → .env, якщо присутній), і ви готові до роботи. Опціональні зразки даних:
php runway migrate
# потім відвідайте /posts та /api/posts
Висока продуктивність
Flight — один з найшвидших PHP-фреймворків. Його легке ядро означає менші накладні витрати та більшу швидкість — ідеально як для традиційних додатків, так і для сучасних AI-асистованих робочих процесів. Ви можете переглянути всі бенчмарки на TechEmpower
Дивіться бенчмарк нижче з деякими іншими популярними PHP-фреймворками.
| Фреймворк | Звичайний текст Req/сек | JSON Req/сек |
|---|---|---|
| Flight | 190,421 | 182,491 |
| Yii | 145,749 | 131,434 |
| Fat-Free | 139,238 | 133,952 |
| Slim | 89,588 | 87,348 |
| Phalcon | 95,911 | 87,675 |
| Symfony | 65,053 | 63,237 |
| Lumen | 40,572 | 39,700 |
| Laravel | 26,657 | 26,901 |
| CodeIgniter | 20,628 | 19,901 |
Flight та AI
Цікавить, як Flight поєднується з LLM для програмування? Дізнайтеся, як AGENTS.md, команди Runway ai:* та макет скелета тримають асистентів на правильному шляху.
Стабільність та зворотна сумісність
Ми цінуємо ваш час. Ми всі бачили фреймворки, які повністю переосмислюють себе кожні кілька років, залишаючи розробників з непрацюючим кодом та дорогими міграціями. Flight інший. Flight v3 був розроблений як доповнення до v2, що означає, що API, яке ви знаєте та любите, не було видалено. Насправді, більшість проєктів v2 працюватимуть без змін у v3.
Ми прагнемо зберігати Flight стабільним, щоб ви могли зосередитися на створенні свого додатка, а не на виправленні фреймворку. Скелет може бути думкою для нових проєктів; основні API залишаються знайомими для всіх інших.
Спільнота
Ми в Matrix Chat
Та Discord
Внесок
Є два способи внести свій внесок у Flight:
- Внесок у основний фреймворк, відвідавши репозиторій ядра.
- Допомогти покращити документацію! Цей сайт документації розміщений на Github. Якщо ви помітили помилку або хочете щось покращити, можете надіслати pull request. Ми любимо оновлення та нові ідеї — особливо щодо AI та нових технологій!
Вимоги
Flight потребує PHP 7.4 або вище.
Примітка: PHP 7.4 підтримується, оскільки на момент написання (2024) PHP 7.4 є стандартною версією для деяких дистрибутивів Linux з LTS. Примусовий перехід на PHP >8 завдасть багато незручностей цим користувачам. Фреймворк також підтримує PHP >8.
Ліцензія
Flight випущений під ліцензією MIT.
Awesome-plugins/php_cookie
Cookies
overclokk/cookie — це проста бібліотека для керування куки у вашому додатку.
Installation
Встановлення є простим за допомогою composer.
composer require overclokk/cookie
Usage
Використання таке ж просте, як реєстрація нового методу в класі Flight.
use Overclokk\Cookie\Cookie;
/*
* Встановіть у вашому bootstrap або public/index.php файлі
*/
Flight::register('cookie', Cookie::class);
/**
* ExampleController.php
*/
class ExampleController {
public function login() {
// Встановіть куки
// ви захочете, щоб це було false, щоб отримати новий екземпляр
// використовуйте наведену нижче коментар, якщо хочете автозаповнення
/** @var \Overclokk\Cookie\Cookie $cookie */
$cookie = Flight::cookie(false);
$cookie->set(
'stay_logged_in', // назва куки
'1', // значення, яке ви хочете встановити
86400, // кількість секунд, протягом яких куки повинні існувати
'/', // шлях, за яким куки будуть доступні
'example.com', // домен, за яким куки будуть доступні
true, // куки будуть передаватися лише через безпечне HTTPS з'єднання
true // куки будуть доступні лише через HTTP протокол
);
// за бажанням, якщо ви хочете зберегти значення за замовчуванням
// і мати швидкий спосіб встановити куки на тривалий час
$cookie->forever('stay_logged_in', '1');
}
public function home() {
// Перевірте, чи маєте ви куки
if (Flight::cookie()->has('stay_logged_in')) {
// помістіть їх у область інформаційної панелі, наприклад.
Flight::redirect('/dashboard');
}
}
}Awesome-plugins/php_encryption
PHP Шифрування
defuse/php-encryption — це бібліотека, яку можна використовувати для шифрування та дешифрування даних. Запуск і налаштування досить прості, щоб почати шифрування та дешифрування даних. У них є чудовий посібник, який допомагає пояснити основи використання бібліотеки, а також важливі питання безпеки, пов’язані із шифруванням.
Встановлення
Встановлення просте за допомогою composer.
composer require defuse/php-encryption
Налаштування
Потім вам потрібно згенерувати ключ шифрування.
vendor/bin/generate-defuse-key
Це видасть ключ, який вам потрібно зберегти в безпеці. Ви можете зберегти ключ у вашому app/config/config.php файлі в масиві внизу файлу. Хоча це не ідеальне місце, але принаймні щось.
Використання
Тепер, коли у вас є бібліотека та ключ шифрування, ви можете почати шифрування та дешифрування даних.
use Defuse\Crypto\Crypto;
use Defuse\Crypto\Key;
/*
* Встановіть у вашому bootstrap або public/index.php файлі
*/
// Метод шифрування
Flight::map('encrypt', function($raw_data) {
$encryption_key = /* $config['encryption_key'] або file_get_contents з того, де ви помістили ключ */;
return Crypto::encrypt($raw_data, Key::loadFromAsciiSafeString($encryption_key));
});
// Метод дешифрування
Flight::map('decrypt', function($encrypted_data) {
$encryption_key = /* $config['encryption_key'] або file_get_contents з того, де ви помістили ключ */;
try {
$raw_data = Crypto::decrypt($encrypted_data, Key::loadFromAsciiSafeString($encryption_key));
} catch (Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException $ex) {
// Атака! Або був завантажений неправильний ключ, або шифротекст було
// змінено з моменту його створення — або пошкоджено в базі даних, або
// навмисно змінено Евой, яка намагається здійснити атаку.
// ... обробіть цей випадок таким чином, як це підходить для вашого застосунку ...
}
return $raw_data;
});
Flight::route('/encrypt', function() {
$encrypted_data = Flight::encrypt('Це секрет');
echo $encrypted_data;
});
Flight::route('/decrypt', function() {
$encrypted_data = '...'; // Отримати зашифровані дані звідкись
$decrypted_data = Flight::decrypt($encrypted_data);
echo $decrypted_data;
});Awesome-plugins/php_file_cache
flightphp/cache
Легка, проста та автономна PHP-клас для кешування у файлах, відгалужений від Wruczek/PHP-File-Cache
Переваги
- Легкий, автономний і простий
- Весь код в одному файлі - ніяких зайвих драйверів.
- Безпечний - кожен згенерований файл кешу має PHP-заголовок з die, що робить прямий доступ неможливим, навіть якщо хтось знає шлях і ваш сервер не налаштований належним чином
- Добре задокументований і протестований
- Правильно обробляє паралельність через flock
- Підтримує PHP 7.4+
- Безкоштовний за ліцензією MIT
Цей сайт документації використовує цю бібліотеку для кешування кожної зі сторінок!
Натисніть тут, щоб переглянути код.
Встановлення
Встановіть через composer:
composer require flightphp/cache
Використання
Використання досить просте. Це зберігає файл кешу в директорії кешу.
use flight\Cache;
$app = Flight::app();
// Ви передаєте директорію, де буде зберігатися кеш, у конструктор
$app->register('cache', Cache::class, [ __DIR__ . '/../cache/' ], function(Cache $cache) {
// Це гарантує, що кеш буде використовуватися лише в режимі production
// ENVIRONMENT - це константа, яка встановлюється у вашому bootstrap-файлі або в іншому місці вашого додатку
$cache->setDevMode(ENVIRONMENT === 'development');
});
Отримання значення кешу
Ви використовуєте метод get() для отримання кешованого значення. Якщо вам потрібен зручний метод, який оновить кеш, якщо він застарів, ви можете використовувати refreshIfExpired().
// Отримати екземпляр кешу
$cache = Flight::cache();
$data = $cache->refreshIfExpired('simple-cache-test', function () {
return date("H:i:s"); // повернути дані для кешування
}, 10); // 10 секунд
// або
$data = $cache->get('simple-cache-test');
if(empty($data)) {
$data = date("H:i:s");
$cache->set('simple-cache-test', $data, 10); // 10 секунд
}
Зберігання значення кешу
Ви використовуєте метод set() для зберігання значення в кеші.
Flight::cache()->set('simple-cache-test', 'my cached data', 10); // 10 секунд
Видалення значення кешу
Ви використовуєте метод delete() для видалення значення в кеші.
Flight::cache()->delete('simple-cache-test');
Перевірка наявності значення кешу
Ви використовуєте метод exists() для перевірки, чи існує значення в кеші.
if(Flight::cache()->exists('simple-cache-test')) {
// виконати щось
}
Очищення кешу
Ви використовуєте метод flush() для очищення всього кешу.
Flight::cache()->flush();
Отримання метаданих з кешу
Якщо ви хочете отримати мітки часу та інші метадані про запис кешу, переконайтеся, що ви передаєте true як правильний параметр.
$data = $cache->refreshIfExpired("simple-cache-meta-test", function () {
echo "Refreshing data!" . PHP_EOL;
return date("H:i:s"); // повернути дані для кешування
}, 10, true); // true = повернути з метаданими
// або
$data = $cache->get("simple-cache-meta-test", true); // true = повернути з метаданими
/*
Приклад кешованого елемента, отриманого з метаданими:
{
"time":1511667506, <-- unix timestamp збереження
"expire":10, <-- час закінчення в секундах
"data":"04:38:26", <-- десеріалізовані дані
"permanent":false
}
Використовуючи метадані, ми можемо, наприклад, обчислити, коли елемент був збережений або коли він закінчується
Ми також можемо отримати доступ до самих даних за допомогою ключа "data"
*/
$expiresin = ($data["time"] + $data["expire"]) - time(); // отримати unix timestamp, коли дані закінчуються, і відняти від нього поточний timestamp
$cacheddate = $data["data"]; // ми отримуємо доступ до самих даних за допомогою ключа "data"
echo "Latest cache save: $cacheddate, expires in $expiresin seconds";
Вихідний код
Відвідайте https://github.com/flightphp/cache, щоб переглянути код.
Awesome-plugins/permissions
FlightPHP/Дозволи
Це модуль дозволів, який можна використовувати у ваших проєктах, якщо у вас є кілька ролей у вашому додатку, і кожна роль має дещо різну функціональність. Цей модуль дозволяє вам визначати дозволи для кожної ролі, а потім перевіряти, чи має поточний користувач дозвіл на доступ до певної сторінки чи виконання певної дії.
Натисніть тут для репозиторію на GitHub.
Встановлення
Запустіть composer require flightphp/permissions і ви на шляху!
Використання
Спочатку вам потрібно налаштувати ваші дозволи, потім ви вказуєте вашому додатку, що означають ці дозволи. Зрештою ви будете перевіряти ваші дозволи за допомогою $Permissions->has(), ->can() або is(). has() і can() мають однакову функціональність, але називаються по-різному, щоб зробити ваш код більш читабельним.
Базовий приклад
Припустимо, у вас є функція у вашому додатку, яка перевіряє, чи користувач увійшов у систему. Ви можете створити об'єкт дозволів так:
// index.php
require 'vendor/autoload.php';
// some code
// then you probably have something that tells you who the current role is of the person
// likely you have something where you pull the current role
// from a session variable which defines this
// after someone logs in, otherwise they will have a 'guest' or 'public' role.
$current_role = 'admin';
// setup permissions
$permission = new \flight\Permission($current_role);
$permission->defineRule('loggedIn', function($current_role) {
return $current_role !== 'guest';
});
// You'll probably want to persist this object in Flight somewhere
Flight::set('permission', $permission);
Потім у контролері десь може бути щось таке.
<?php
// some controller
class SomeController {
public function someAction() {
$permission = Flight::get('permission');
if ($permission->has('loggedIn')) {
// do something
} else {
// do something else
}
}
}
Ви також можете використовувати це для відстеження, чи мають вони дозвіл на щось робити у вашому додатку. Наприклад, якщо у вас є спосіб, яким користувачі можуть взаємодіяти з публікаціями у вашому програмному забезпеченні, ви можете перевірити, чи мають вони дозвіл на виконання певних дій.
$current_role = 'admin';
// setup permissions
$permission = new \flight\Permission($current_role);
$permission->defineRule('post', function($current_role) {
if($current_role === 'admin') {
$permissions = ['create', 'read', 'update', 'delete'];
} else if($current_role === 'editor') {
$permissions = ['create', 'read', 'update'];
} else if($current_role === 'author') {
$permissions = ['create', 'read'];
} else if($current_role === 'contributor') {
$permissions = ['create'];
} else {
$permissions = [];
}
return $permissions;
});
Flight::set('permission', $permission);
Потім у контролері десь...
class PostController {
public function create() {
$permission = Flight::get('permission');
if ($permission->can('post.create')) {
// do something
} else {
// do something else
}
}
}
Ін'єкція залежностей
Ви можете ін'єктувати залежності у замикання, яке визначає дозволи. Це корисно, якщо у вас є якийсь перемикач, ідентифікатор або будь-яка інша точка даних, яку ви хочете перевірити. Те саме працює для викликів типу Class->Method, за винятком того, що ви визначаєте аргументи у методі.
Замикання
$Permission->defineRule('order', function(string $current_role, MyDependency $MyDependency = null) {
// ... code
});
// in your controller file
public function createOrder() {
$MyDependency = Flight::myDependency();
$permission = Flight::get('permission');
if ($permission->can('order.create', $MyDependency)) {
// do something
} else {
// do something else
}
}
Класи
namespace MyApp;
class Permissions {
public function order(string $current_role, MyDependency $MyDependency = null) {
// ... code
}
}
Ярлик для встановлення дозволів за допомогою класів
Ви також можете використовувати класи для визначення ваших дозволів. Це корисно, якщо у вас є багато дозволів і ви хочете зберегти ваш код чистим. Ви можете зробити щось таке:
<?php
// bootstrap code
$Permissions = new \flight\Permission($current_role);
$Permissions->defineRule('order', 'MyApp\Permissions->order');
// myapp/Permissions.php
namespace MyApp;
class Permissions {
public function order(string $current_role, int $user_id) {
// Assuming you set this up beforehand
/** @var \flight\database\SimplePdo $db */
$db = Flight::db();
$allowed_permissions = [ 'read' ]; // everyone can view an order
if($current_role === 'manager') {
$allowed_permissions[] = 'create'; // managers can create orders
}
$some_special_toggle_from_db = $db->fetchField('SELECT some_special_toggle FROM settings WHERE id = ?', [ $user_id ]);
if($some_special_toggle_from_db) {
$allowed_permissions[] = 'update'; // if the user has a special toggle, they can update orders
}
if($current_role === 'admin') {
$allowed_permissions[] = 'delete'; // admins can delete orders
}
return $allowed_permissions;
}
}
Крута частина полягає в тому, що є також ярлик, який ви можете використовувати (який також можна кешувати!!!), де ви просто вказуєте класу дозволів зіставити всі методи у класі з дозволами. Тож якщо у вас є метод з назвою order() і метод з назвою company(), вони автоматично будуть зіставлені, тож ви можете просто запустити $Permissions->has('order.read') або $Permissions->has('company.read'), і це спрацює. Визначити це дуже складно, тож тримайтеся за мене. Вам просто потрібно зробити це:
Створіть клас дозволів, який ви хочете згрупувати разом.
class MyPermissions {
public function order(string $current_role, int $order_id = 0): array {
// code to determine permissions
return $permissions_array;
}
public function company(string $current_role, int $company_id): array {
// code to determine permissions
return $permissions_array;
}
}
Потім зробіть дозволи доступними для виявлення за допомогою цієї бібліотеки.
$Permissions = new \flight\Permission($current_role);
$Permissions->defineRulesFromClassMethods(MyApp\Permissions::class);
Flight::set('permissions', $Permissions);
Нарешті, викличте дозвіл у вашій кодовій базі, щоб перевірити, чи користувачу дозволено виконувати даний дозвіл.
class SomeController {
public function createOrder() {
if(Flight::get('permissions')->can('order.create') === false) {
die('You can\'t create an order. Sorry!');
}
}
}
Кешування
Щоб увімкнути кешування, дивіться просту бібліотеку wruczak/phpfilecache. Приклад увімкнення цього наведено нижче.
// this $app can be part of your code, or
// you can just pass null and it will
// pull from Flight::app() in the constructor
$app = Flight::app();
// For now it accepts this as a file cache. Others can easily
// be added in the future.
$Cache = new Wruczek\PhpFileCache\PhpFileCache;
$Permissions = new \flight\Permission($current_role, $app, $Cache);
$Permissions->defineRulesFromClassMethods(MyApp\Permissions::class, 3600); // 3600 is how many seconds to cache this for. Leave this off to not use caching
І вперед!
Awesome-plugins/simple_job_queue
Проста черга завдань
Проста черга завдань - це бібліотека, яка може використовуватися для асинхронної обробки завдань. Її можна використовувати з beanstalkd, MySQL/MariaDB, SQLite і PostgreSQL.
Встановлення
composer require n0nag0n/simple-job-queue
Використання
Щоб це працювало, вам потрібен спосіб додати завдання до черги та спосіб обробляти завдання (робітник). Нижче наведені приклади того, як додати завдання до черги та як обробити завдання.
Додавання до Flight
Додавання цього до Flight є простим і здійснюється за допомогою методу register(). Нижче наведено приклад того, як додати це до Flight.
<?php
require 'vendor/autoload.php';
// Змініть ['mysql'] на ['beanstalkd'], якщо ви хочете використовувати beanstalkd
Flight::register('queue', n0nag0n\Job_Queue::class, ['mysql'], function($Job_Queue) {
// якщо у вас вже є з'єднання PDO в Flight::db();
$Job_Queue->addQueueConnection(Flight::db());
// або якщо ви використовуєте beanstalkd/Pheanstalk
$pheanstalk = Pheanstalk\Pheanstalk::create('127.0.0.1');
$Job_Queue->addQueueConnection($pheanstalk);
});
Додавання нового завдання
Коли ви додаєте завдання, вам потрібно вказати конвеєр (чергу). Це можна порівняти з каналом у RabbitMQ або трубою в beanstalkd.
<?php
Flight::queue()->selectPipeline('send_important_emails');
Flight::queue()->addJob(json_encode([ 'something' => 'that', 'ends' => 'up', 'a' => 'string' ]));
Запуск робітника
Ось приклад файлу про те, як запустити робітника.
<?php
require 'vendor/autoload.php';
$Job_Queue = new n0nag0n\Job_Queue('mysql');
// З'єднання PDO
$PDO = new PDO('mysql:dbname=testdb;host=127.0.0.1', 'user', 'pass');
$Job_Queue->addQueueConnection($PDO);
// або якщо ви використовуєте beanstalkd/Pheanstalk
$pheanstalk = Pheanstalk\Pheanstalk::create('127.0.0.1');
$Job_Queue->addQueueConnection($pheanstalk);
$Job_Queue->watchPipeline('send_important_emails');
while(true) {
$job = $Job_Queue->getNextJobAndReserve();
// налаштуйте так, як вам краще спати вночі (тільки для черг бази даних, beanstalkd не потребує цієї умови)
if(empty($job)) {
usleep(500000);
continue;
}
echo "Обробка {$job['id']}\n";
$payload = json_decode($job['payload'], true);
try {
$result = doSomethingThatDoesSomething($payload);
if($result === true) {
$Job_Queue->deleteJob($job);
} else {
// це забирає його з готової черги та ставить його в іншу чергу, яку можна буде забрати та "вибити" пізніше.
$Job_Queue->buryJob($job);
}
} catch(Exception $e) {
$Job_Queue->buryJob($job);
}
}
Обробка тривалих процесів із Supervisord
Supervisord - це система контролю процесів, яка забезпечує безперервну роботу ваших робочих процесів. Ось більш детальний посібник із налаштування його з вашим робітником Simple Job Queue:
Встановлення Supervisord
# На Ubuntu/Debian
sudo apt-get install supervisor
# На CentOS/RHEL
sudo yum install supervisor
# На macOS з Homebrew
brew install supervisor
Створення скрипта робітника
Спочатку збережіть код вашого робітника в окремому файлі PHP:
<?php
require 'vendor/autoload.php';
$Job_Queue = new n0nag0n\Job_Queue('mysql');
// З'єднання PDO
$PDO = new PDO('mysql:dbname=your_database;host=127.0.0.1', 'username', 'password');
$Job_Queue->addQueueConnection($PDO);
// Встановіть конвеєр для моніторингу
$Job_Queue->watchPipeline('send_important_emails');
// Лог початку робітника
echo date('Y-m-d H:i:s') . " - Робітник запущений\n";
while(true) {
$job = $Job_Queue->getNextJobAndReserve();
if(empty($job)) {
usleep(500000); // Спати 0,5 секунди
continue;
}
echo date('Y-m-d H:i:s') . " - Обробка завдання {$job['id']}\n";
$payload = json_decode($job['payload'], true);
try {
$result = doSomethingThatDoesSomething($payload);
if($result === true) {
$Job_Queue->deleteJob($job);
echo date('Y-m-d H:i:s') . " - Завдання {$job['id']} успішно завершено\n";
} else {
$Job_Queue->buryJob($job);
echo date('Y-m-d H:i:s') . " - Завдання {$job['id']} не вдалося, закопано\n";
}
} catch(Exception $e) {
$Job_Queue->buryJob($job);
echo date('Y-m-d H:i:s') . " - Виключення при обробці завдання {$job['id']}: {$e->getMessage()}\n";
}
}
Налаштування Supervisord
Створіть файл конфігурації для вашого робітника:
[program:email_worker]
command=php /path/to/worker.php
directory=/path/to/project
autostart=true
autorestart=true
startretries=3
stderr_logfile=/var/log/simple_job_queue_err.log
stdout_logfile=/var/log/simple_job_queue.log
user=www-data
numprocs=2
process_name=%(program_name)s_%(process_num)02d
Основні параметри конфігурації:
command: Команда для запуску вашого робітникаdirectory: Робоча папка для робітникаautostart: Автоматичний запуск при запуску supervisordautorestart: Автоматичний перезапуск, якщо процес завершуєтьсяstartretries: Кількість спроб перезапустити, якщо це не вдаєтьсяstderr_logfile/stdout_logfile: Місцезнаходження файлів журналуuser: Системний користувач для запуску процесуnumprocs: Кількість екземплярів робітника для запускуprocess_name: Формат іменування для кількох процесів робітників
Управління робітниками за допомогою Supervisorctl
Після створення або модифікації конфігурації:
# Перезавантаження конфігурації супервайзера
sudo supervisorctl reread
sudo supervisorctl update
# Управління конкретними процесами робітників
sudo supervisorctl start email_worker:*
sudo supervisorctl stop email_worker:*
sudo supervisorctl restart email_worker:*
sudo supervisorctl status email_worker:*
Запуск кількох конвеєрів
Для кількох конвеєрів створіть окремі файли робітників і конфігурації:
[program:email_worker]
command=php /path/to/email_worker.php
# ... інші конфігурації ...
[program:notification_worker]
command=php /path/to/notification_worker.php
# ... інші конфігурації ...
Моніторинг та журнали
Перевірте журнали, щоб контролювати активність робітника:
# Перегляд журналів
sudo tail -f /var/log/simple_job_queue.log
# Перевірка статусу
sudo supervisorctl status
Ця настройка забезпечує неперервну роботу ваших робочих процесів, навіть після аварій, перезавантажень сервера або інших проблем, що робить вашу систему черги надійною для виробничих середовищ.
Awesome-plugins/jwt
Firebase JWT - Аутентифікація JSON Web Token
JWT (JSON Web Tokens) — це компактний, безпечний для URL спосіб представлення тверджень між вашим додатком та клієнтом. Вони ідеальні для безстанної аутентифікації API — немає потреби в зберіганні сесій на сервері! Цей посібник показує, як інтегрувати Firebase JWT з Flight для безпечної аутентифікації на основі токенів.
Відвідайте репозиторій Github для повної документації та деталей.
Що таке JWT?
JSON Web Token — це рядок, який містить три частини:
- Заголовок: Метадані про токен (алгоритм, тип)
- Навантаження: Ваші дані (ID користувача, ролі, термін дії тощо)
- Підпис: Криптографічний підпис для перевірки автентичності
Приклад JWT: eyJ0eXAiOiJKV1QiLCJhbGc... (виглядає як нісенітниця, але це структуровані дані!)
Чому використовувати JWT?
- Безстанний: Не потрібно зберігати сесії на сервері — ідеально для мікросервісів та API
- Масштабований: Добре працює з балансувальниками навантаження, оскільки немає вимоги до афінності сесій
- Крос-доменний: Може використовуватися між різними доменами та сервісами
- Дружній до мобільних: Чудово підходить для мобільних додатків, де куки можуть не працювати добре
- Стандартизований: Підхід промислового стандарту (RFC 7519)
Встановлення
Встановіть за допомогою Composer:
composer require firebase/php-jwt
Основне використання
Ось швидкий приклад створення та перевірки JWT:
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
// Ваш секретний ключ (ЗБЕРІГАЙТЕ ЦЕ БЕЗПЕЧНО!)
$secretKey = 'your-256-bit-secret-key-here-keep-it-safe';
// Створення токена
$payload = [
'user_id' => 123,
'username' => 'johndoe',
'role' => 'admin',
'iat' => time(), // Виданий о
'exp' => time() + 3600 // Термін дії 1 година
];
$jwt = JWT::encode($payload, $secretKey, 'HS256');
echo "Token: " . $jwt;
// Перевірка та декодування токена
try {
$decoded = JWT::decode($jwt, new Key($secretKey, 'HS256'));
echo "User ID: " . $decoded->user_id;
} catch (Exception $e) {
echo "Invalid token: " . $e->getMessage();
}
JWT Middleware для Flight (Рекомендований підхід)
Найпоширеніший та корисний спосіб використання JWT з Flight — як middleware для захисту маршрутів API. Ось повний, готовий до виробництва приклад:
Крок 1: Створення класу JWT Middleware
// app/middleware/JwtMiddleware.php
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Firebase\JWT\ExpiredException;
use Firebase\JWT\SignatureInvalidException;
use flight\Engine;
class JwtMiddleware {
protected Engine $app;
protected string $secretKey;
public function __construct(Engine $app) {
$this->app = $app;
// Зберігайте ваш секретний ключ у app/config/config.php, НЕ хардкодьте!
$this->secretKey = $app->get('config')['jwt_secret'];
}
public function before(array $params) {
$authHeader = $this->app->request()->getHeader('Authorization');
// Перевірка існування заголовка Authorization
if (empty($authHeader)) {
$this->app->jsonHalt(['error' => 'No authorization token provided'], 401);
}
// Витягнення токена з формату "Bearer <token>"
if (!preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches)) {
$this->app->jsonHalt(['error' => 'Invalid authorization format. Use: Bearer <token>'], 401);
}
$jwt = $matches[1];
try {
// Декодування та перевірка токена
$decoded = JWT::decode($jwt, new Key($this->secretKey, 'HS256'));
// Збереження даних користувача в запиті для використання в обробниках маршрутів
$this->app->request()->data->user = $decoded;
} catch (ExpiredException $e) {
$this->app->jsonHalt(['error' => 'Token has expired'], 401);
} catch (SignatureInvalidException $e) {
$this->app->jsonHalt(['error' => 'Invalid token signature'], 401);
} catch (Exception $e) {
$this->app->jsonHalt(['error' => 'Invalid token: ' . $e->getMessage()], 401);
}
}
}
Крок 2: Реєстрація JWT Secret у вашій конфігурації
// app/config/config.php
return [
'jwt_secret' => getenv('JWT_SECRET') ?: 'your-fallback-secret-for-development'
];
// app/config/bootstrap.php або index.php
// переконайтеся, що додали цей рядок, якщо хочете надати доступ до конфігурації в додатку
$app->set('config', $config);
Примітка щодо безпеки: Ніколи не хардкодьте ваш секретний ключ! Використовуйте змінні середовища в продакшені.
Крок 3: Захист ваших маршрутів за допомогою Middleware
// Захист одного маршруту
Flight::route('GET /api/user/profile', function() {
$user = Flight::request()->data->user; // Встановлено middleware
Flight::json([
'user_id' => $user->user_id,
'username' => $user->username,
'role' => $user->role
]);
})->addMiddleware(JwtMiddleware::class);
// Захист цілої групи маршрутів (більш поширено!)
Flight::group('/api', function() {
Flight::route('GET /users', function() { /* ... */ });
Flight::route('GET /posts', function() { /* ... */ });
Flight::route('POST /posts', function() { /* ... */ });
Flight::route('DELETE /posts/@id', function($id) { /* ... */ });
}, [ JwtMiddleware::class ]); // Всі маршрути в цій групі захищені!
Для отримання додаткових деталей щодо middleware дивіться документацію middleware.
Поширені випадки використання
1. Ендпоінт входу (Генерація токена)
Створіть маршрут, який генерує JWT після успішної аутентифікації:
Flight::route('POST /api/login', function() {
$data = Flight::request()->data;
$username = $data->username ?? '';
$password = $data->password ?? '';
// Перевірка облікових даних (приклад — використовуйте свою логіку!)
$user = validateUserCredentials($username, $password);
if (!$user) {
Flight::jsonHalt(['error' => 'Invalid credentials'], 401);
}
// Генерація JWT
$secretKey = Flight::get('config')['jwt_secret'];
$payload = [
'user_id' => $user->id,
'username' => $user->username,
'role' => $user->role,
'iat' => time(),
'exp' => time() + (60 * 60) // Термін дії 1 година
];
$jwt = JWT::encode($payload, $secretKey, 'HS256');
Flight::json([
'success' => true,
'token' => $jwt,
'expires_in' => 3600
]);
});
function validateUserCredentials($username, $password) {
// Ваш пошук у базі даних та перевірка пароля тут
// Приклад:
$db = Flight::db();
$user = $db->fetchRow("SELECT * FROM users WHERE username = ?", [$username]);
if ($user && password_verify($password, $user['password_hash'])) {
return (object) [
'id' => $user['id'],
'username' => $user['username'],
'role' => $user['role']
];
}
return null;
}
2. Потік оновлення токена
Реалізуйте систему токенів оновлення для довготривалих сесій:
Flight::route('POST /api/login', function() {
// ... перевірка облікових даних ...
$secretKey = Flight::get('config')['jwt_secret'];
$refreshSecret = Flight::get('config')['jwt_refresh_secret'];
// Короткочасний токен доступу (15 хвилин)
$accessToken = JWT::encode([
'user_id' => $user->id,
'type' => 'access',
'iat' => time(),
'exp' => time() + (15 * 60)
], $secretKey, 'HS256');
// Довготривалий токен оновлення (7 днів)
$refreshToken = JWT::encode([
'user_id' => $user->id,
'type' => 'refresh',
'iat' => time(),
'exp' => time() + (7 * 24 * 60 * 60)
], $refreshSecret, 'HS256');
Flight::json([
'access_token' => $accessToken,
'refresh_token' => $refreshToken,
'expires_in' => 900
]);
});
Flight::route('POST /api/refresh', function() {
$refreshToken = Flight::request()->data->refresh_token ?? '';
$refreshSecret = Flight::get('config')['jwt_refresh_secret'];
try {
$decoded = JWT::decode($refreshToken, new Key($refreshSecret, 'HS256'));
// Перевірка, що це токен оновлення
if ($decoded->type !== 'refresh') {
Flight::jsonHalt(['error' => 'Invalid token type'], 401);
}
// Генерація нового токена доступу
$secretKey = Flight::get('config')['jwt_secret'];
$accessToken = JWT::encode([
'user_id' => $decoded->user_id,
'type' => 'access',
'iat' => time(),
'exp' => time() + (15 * 60)
], $secretKey, 'HS256');
Flight::json([
'access_token' => $accessToken,
'expires_in' => 900
]);
} catch (Exception $e) {
Flight::jsonHalt(['error' => 'Invalid refresh token'], 401);
}
});
3. Контроль доступу на основі ролей
Розширте ваш middleware для перевірки ролей користувача:
class JwtRoleMiddleware {
protected Engine $app;
protected array $allowedRoles;
public function __construct(Engine $app, array $allowedRoles = []) {
$this->app = $app;
$this->allowedRoles = $allowedRoles;
}
public function before(array $params) {
// Припустимо, що JwtMiddleware вже виконався та встановив дані користувача
$user = $this->app->request()->data->user ?? null;
if (!$user) {
$this->app->jsonHalt(['error' => 'Authentication required'], 401);
}
// Перевірка, чи має користувач необхідну роль
if (!empty($this->allowedRoles) && !in_array($user->role, $this->allowedRoles)) {
$this->app->jsonHalt(['error' => 'Insufficient permissions'], 403);
}
}
}
// Використання: маршрут тільки для адміністраторів
Flight::route('DELETE /api/users/@id', function($id) {
// Логіка видалення користувача
})->addMiddleware([
JwtMiddleware::class,
new JwtRoleMiddleware(Flight::app(), ['admin'])
]);
4. Публічний API з обмеженням швидкості за користувачем
Використовуйте JWT для відстеження та обмеження швидкості користувачів без сесій:
class RateLimitMiddleware {
public function before(array $params) {
$user = Flight::request()->data->user ?? null;
$userId = $user ? $user->user_id : Flight::request()->ip;
$cacheKey = "rate_limit:$userId";
// Переконайтеся, що налаштували сервіс кешу в app/config/services.php
$requests = Flight::cache()->get($cacheKey, 0);
if ($requests >= 100) { // 100 запитів на годину
Flight::jsonHalt(['error' => 'Rate limit exceeded'], 429);
}
Flight::cache()->set($cacheKey, $requests + 1, 3600);
}
}
Найкращі практики безпеки
1. Використовуйте сильні секретні ключі
// Генерація безпечного секретного ключа (виконайте один раз, збережіть у .env файл)
$secretKey = base64_encode(random_bytes(32));
echo $secretKey; // Збережіть це у вашому .env файлі!
2. Зберігайте секрети у змінних середовища
// Ніколи не комітьте секрети до контролю версій!
// Використовуйте .env файл та бібліотеку як vlucas/phpdotenv
// .env файл:
// JWT_SECRET=your-base64-encoded-secret-here
// JWT_REFRESH_SECRET=another-base64-encoded-secret-here
// Ви також можете використовувати файл app/config/config.php для зберігання секретів
// просто переконайтеся, що файл конфігурації не комітиться до контролю версій
// return [
// 'jwt_secret' => 'your-base64-encoded-secret-here',
// 'jwt_refresh_secret' => 'another-base64-encoded-secret-here',
// ];
// У вашому додатку:
$secretKey = getenv('JWT_SECRET');
3. Встановіть відповідні терміни дії
// Хороша практика: короткочасні токени доступу
'exp' => time() + (15 * 60) // 15 хвилин
// Для токенів оновлення: довший термін дії
'exp' => time() + (7 * 24 * 60 * 60) // 7 днів
4. Використовуйте HTTPS у продакшені
JWT завжди повинні передаватися через HTTPS. Ніколи не надсилайте токени через звичайний HTTP у продакшені!
5. Перевіряйте твердження токена
Завжди перевіряйте твердження, які вас цікавлять:
$decoded = JWT::decode($jwt, new Key($secretKey, 'HS256'));
// Перевірка терміну дії обробляється автоматично бібліотекою
// Але ви можете додати власні перевірки:
if ($decoded->iat > time()) {
throw new Exception('Token used before it was issued');
}
if (isset($decoded->nbf) && $decoded->nbf > time()) {
throw new Exception('Token not yet valid');
}
6. Розгляньте чорний список токенів для виходу
Для додаткової безпеки підтримуйте чорний список недійсних токенів:
Flight::route('POST /api/logout', function() {
$authHeader = Flight::request()->getHeader('Authorization');
preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches);
$jwt = $matches[1];
// Витягнення терміну дії токена
$decoded = Flight::request()->data->user;
$ttl = $decoded->exp - time();
// Збереження в кеші/redis до терміну дії
Flight::cache()->set("blacklist:$jwt", true, $ttl);
Flight::json(['message' => 'Successfully logged out']);
});
// Додайте до вашого JwtMiddleware:
public function before(array $params) {
// ... витягнення JWT ...
// Перевірка чорного списку
if (Flight::cache()->get("blacklist:$jwt")) {
$this->app->jsonHalt(['error' => 'Token has been revoked'], 401);
}
// ... перевірка токена ...
}
Алгоритми та типи ключів
Firebase JWT підтримує кілька алгоритмів:
Симетричні алгоритми (HMAC)
- HS256 (Рекомендовано для більшості додатків): Використовує один секретний ключ
- HS384, HS512: Сильніші варіанти
$jwt = JWT::encode($payload, $secretKey, 'HS256');
$decoded = JWT::decode($jwt, new Key($secretKey, 'HS256'));
Асиметричні алгоритми (RSA/ECDSA)
- RS256, RS384, RS512: Використовує пари публічних/приватних ключів
- ES256, ES384, ES512: Варіанти на еліптичних кривих
// Генерація ключів: openssl genrsa -out private.key 2048
// openssl rsa -in private.key -pubout -out public.key
$privateKey = file_get_contents('/path/to/private.key');
$publicKey = file_get_contents('/path/to/public.key');
// Кодування з приватним ключем
$jwt = JWT::encode($payload, $privateKey, 'RS256');
// Декодування з публічним ключем
$decoded = JWT::decode($jwt, new Key($publicKey, 'RS256'));
Коли використовувати RSA: Використовуйте RSA, коли потрібно розповсюджувати публічний ключ для перевірки (наприклад, мікросервіси, інтеграції з третіми сторонами). Для одного додатка HS256 простіший та достатній.
Вирішення проблем
Помилка "Expired token"
Твердження exp вашого токена в минулому. Видайте новий токен або реалізуйте оновлення токена.
"Signature verification failed"
- Ви використовуєте інший секретний ключ для декодування, ніж для кодування
- Токен був змінений
- Розбіжність годин між серверами (додайте буфер leeway)
use Firebase\JWT\JWT;
JWT::$leeway = 60; // Дозволити 60 секунд розбіжності годин
$decoded = JWT::decode($jwt, new Key($secretKey, 'HS256'));
Токен не надсилається в запитах
Переконайтеся, що ваш клієнт надсилає заголовок Authorization:
// Приклад JavaScript
fetch('/api/users', {
headers: {
'Authorization': 'Bearer ' + token
}
});
Методи
Бібліотека Firebase JWT надає ці основні методи:
JWT::encode(array $payload, string $key, string $alg): Створює JWT з навантаженняJWT::decode(string $jwt, Key $key): Декодує та перевіряє JWTJWT::urlsafeB64Encode(string $input): Безпечне для URL кодування Base64JWT::urlsafeB64Decode(string $input): Безпечне для URL декодування Base64JWT::$leeway: Статична властивість для встановлення leeway часу для перевірки (у секундах)
Чому використовувати цю бібліотеку?
- Промисловий стандарт: Firebase JWT — найпопулярніша та широко довірена JWT бібліотека для PHP
- Активне обслуговування: Підтримується командою Google/Firebase
- Фокус на безпеці: Регулярні оновлення та патчі безпеки
- Простий API: Легко зрозуміти та реалізувати
- Добре документована: Розгорнута документація та підтримка спільноти
- Гнучка: Підтримує кілька алгоритмів та конфігурованих опцій
Дивіться також
- Репозиторій Firebase JWT Github
- JWT.io - Налагодження та декодування JWT
- RFC 7519 - Офіційна специфікація JWT
- Документація Middleware Flight
- Плагін Session Flight - Для традиційної аутентифікації на основі сесій
Ліцензія
Бібліотека Firebase JWT ліцензована за BSD 3-Clause License. Дивіться репозиторій Github для деталей.
Awesome-plugins/n0nag0n_wordpress
Інтеграція WordPress: n0nag0n/wordpress-integration-for-flight-framework
Хочете використовувати Flight PHP усередині вашого сайту WordPress? Цей плагін робить це легким! З n0nag0n/wordpress-integration-for-flight-framework ви можете запускати повноцінний додаток Flight прямо поруч з вашою інсталяцією WordPress — ідеально для створення власних API, мікросервісів або навіть повноцінних додатків, не залишаючи комфорту WordPress.
Що він робить?
- Безшовно інтегрує Flight PHP з WordPress
- Маршрутизує запити до Flight або WordPress на основі шаблонів URL
- Організовує ваш код за допомогою контролерів, моделей і представлень (MVC)
- Легко налаштовує рекомендовану структуру папок Flight
- Використовує підключення до бази даних WordPress або ваше власне
- Тонко налаштовує, як взаємодіють Flight і WordPress
- Простий адміністративний інтерфейс для конфігурації
Встановлення
- Завантажте папку
flight-integrationдо вашої директорії/wp-content/plugins/. - Активуйте плагін в адмін-панелі WordPress (меню Плагіни).
- Перейдіть до Налаштувань > Flight Framework, щоб налаштувати плагін.
- Вкажіть шлях до вашої інсталяції Flight (або використовуйте Composer для встановлення Flight).
- Налаштуйте шлях до папки вашого додатку та створіть структуру папок (плагін може допомогти з цим!).
- Почніть створювати свій додаток Flight!
Приклади використання
Приклад базового маршруту
У вашому файлі app/config/routes.php:
Flight::route('GET /api/hello', function() {
Flight::json(['message' => 'Hello World!']);
});
Приклад контролера
Створіть контролер у app/controllers/ApiController.php:
namespace app\controllers;
use Flight;
class ApiController {
public function getUsers() {
// Ви можете використовувати функції WordPress усередині Flight!
$users = get_users();
$result = [];
foreach($users as $user) {
$result[] = [
'id' => $user->ID,
'name' => $user->display_name,
'email' => $user->user_email
];
}
Flight::json($result);
}
}
Тоді, у вашому routes.php:
Flight::route('GET /api/users', [app\controllers\ApiController::class, 'getUsers']);
Поширені запитання
Питання: Чи потрібно мені знати Flight, щоб використовувати цей плагін?
Відповідь: Так, це для розробників, які хочуть використовувати Flight у WordPress. Рекомендується базове знання маршрутизації та обробки запитів Flight.
Питання: Чи це сповільнить мій сайт WordPress?
Відповідь: Ні! Плагін обробляє лише запити, які відповідають вашим маршрутам Flight. Усі інші запити йдуть до WordPress, як зазвичай.
Питання: Чи можу я використовувати функції WordPress у своєму додатку Flight?
Відповідь: Абсолютно! У вас є повний доступ до всіх функцій, хуків і глобальних змінних WordPress зсередини ваших маршрутів і контролерів Flight.
Питання: Як створити власні маршрути?
Відповідь: Визначте ваші маршрути у файлі config/routes.php у папці вашого додатку. Перегляньте зразковий файл, створений генератором структури папок, для прикладів.
Журнал змін
1.0.0
Початкова версія.
Для більшої інформації, перегляньте GitHub repo.
Awesome-plugins/ghost_session
Ghostff/Session
PHP Session Manager (неблокувальний, флеш, сегмент, шифрування сесій). Використовує PHP open_ssl для необов'язкового шифрування/розшифрування даних сесій. Підтримує File, MySQL, Redis, and Memcached.
Натисніть here, щоб переглянути код.
Встановлення
Встановіть за допомогою composer.
composer require ghostff/session
Основна Конфігурація
Вам не потрібно передавати нічого, щоб використовувати налаштування за замовчуванням для вашої сесії. Ви можете прочитати про інші налаштування в Github Readme.
use Ghostff\Session\Session;
require 'vendor/autoload.php';
$app = Flight::app();
$app->register('session', Session::class);
// одна річ, яку потрібно пам'ятати, це те, що ви повинні зафіксувати свою сесію на кожному завантаженні сторінки
// або вам доведеться запустити auto_commit у вашій конфігурації.
Простий Приклад
Ось простий приклад того, як ви можете використовувати це.
Flight::route('POST /login', function() {
$session = Flight::session();
// виконайте вашу логіку входу тут
// перевірте пароль тощо.
// якщо вхід успішний
$session->set('is_logged_in', true);
$session->set('user', $user);
// щоразу, коли ви записуєте в сесію, ви повинні зафіксувати її навмисно.
$session->commit();
});
// Ця перевірка може бути в логіці обмеженої сторінки або обгорнута в middleware.
Flight::route('/some-restricted-page', function() {
$session = Flight::session();
if(!$session->get('is_logged_in')) {
Flight::redirect('/login');
}
// виконайте вашу логіку обмеженої сторінки тут
});
// версія з middleware
Flight::route('/some-restricted-page', function() {
// регулярна логіка сторінки
})->addMiddleware(function() {
$session = Flight::session();
if(!$session->get('is_logged_in')) {
Flight::redirect('/login');
}
});
Більш Складний Приклад
Ось більш складний приклад того, як ви можете використовувати це.
use Ghostff\Session\Session;
require 'vendor/autoload.php';
$app = Flight::app();
// вкажіть шлях до вашого файлу конфігурації сесії як перший аргумент
// або передайте йому спеціальний масив
$app->register('session', Session::class, [
[
// якщо ви хочете зберігати дані сесії в базі даних (добре для чогось на кшталт "вийти з усіх пристроїв" функціональності)
Session::CONFIG_DRIVER => Ghostff\Session\Drivers\MySql::class,
Session::CONFIG_ENCRYPT_DATA => true,
Session::CONFIG_SALT_KEY => hash('sha256', 'my-super-S3CR3T-salt'), // будь ласка, змініть це на щось інше
Session::CONFIG_AUTO_COMMIT => true, // робіть це тільки якщо це вимагається і/або важко викликати commit() для вашої сесії.
// додатково ви можете зробити Flight::after('start', function() { Flight::session()->commit(); });
Session::CONFIG_MYSQL_DS => [
'driver' => 'mysql', # драйвер бази даних для PDO dns, наприклад (mysql:host=...;dbname=...)
'host' => '127.0.0.1', # хост бази даних
'db_name' => 'my_app_database', # назва бази даних
'db_table' => 'sessions', # таблиця бази даних
'db_user' => 'root', # ім'я користувача бази даних
'db_pass' => '', # пароль бази даних
'persistent_conn'=> false, # Уникайте накладних витрат на встановлення нового з'єднання кожного разу, коли скрипт потребує спілкування з базою даних, що призводить до швидшої веб-додатки. ЗНАЙДІТЬ ЗВОРОТНІЙ БІК САМІ
]
]
]);
Допомога! Мої Дані Сесії Не Зберігаються!
Ви встановлюєте дані сесії, але вони не зберігаються між запитами? Можливо, ви забули зафіксувати дані сесії. Ви можете зробити це, викликавши $session->commit() після встановлення даних сесії.
Flight::route('POST /login', function() {
$session = Flight::session();
// виконайте вашу логіку входу тут
// перевірте пароль тощо.
// якщо вхід успішний
$session->set('is_logged_in', true);
$session->set('user', $user);
// щоразу, коли ви записуєте в сесію, ви повинні зафіксувати її навмисно.
$session->commit();
});
Інший спосіб — коли ви налаштовуєте службу сесії, ви повинні встановити auto_commit на true у вашій конфігурації. Це автоматично зафіксує дані сесії після кожного запиту.
$app->register('session', Session::class, [ 'path/to/session_config.php', bin2hex(random_bytes(32)) ], function(Session $session) {
$session->updateConfiguration([
Session::CONFIG_AUTO_COMMIT => true,
]);
}
);
Додатково ви можете зробити Flight::after('start', function() { Flight::session()->commit(); });, щоб зафіксувати дані сесії після кожного запиту.
Документація
Відвідайте Github Readme для повної документації. Параметри конфігурації добре задокументовані в файлі default_config.php. Код простий для розуміння, якщо ви хочете переглянути цей пакет самостійно.
Awesome-plugins/mcp
FlightPHP MCP Server
FlightPHP MCP Server надає будь-якому AI-асистенту для кодування, сумісному з MCP, миттєвий структурований доступ до всієї документації FlightPHP — маршрутизація, middleware, плагіни, посібники та інше. Замість того, щоб ваш AI вигадував деталі API або вгадував сигнатури методів, він отримує реальні документи на вимогу. Без API-ключів, без встановлення для хостованої версії.
Відвідайте репозиторій на Github для повного вихідного коду та деталей.
Швидкий старт
Сервер публічно хостується і готовий до використання:
https://mcp.flightphp.com/mcp
Просто додайте цю URL до вашого розширення для AI-кодування. Без реєстрації, без облікових даних. Дивіться розділ Конфігурація IDE нижче для готових конфігурацій для найпопулярніших інструментів.
Що він робить
Після підключення ваш AI-асистент може:
- Переглядати всі доступні документи — перелічити всі основні теми, посібники та сторінки плагінів
- Отримувати будь-яку сторінку документації — отримувати повний вміст для маршрутизації, middleware, запитів, безпеки та іншого
- Шукати документи плагінів — отримувати повну документацію для ActiveRecord, Session, Tracy, Runway та всіх інших офіційних плагінів
- Дотримуватися покрокових посібників — отримувати повні інструкції для створення блогів, REST API та тестування додатків
- Шукати по всьому — знаходити релевантні сторінки серед основних документів, посібників та плагінів одночасно
Ключові моменти
- Нуль налаштувань — хостований сервер на
https://mcp.flightphp.com/mcpне вимагає встановлення чи API-ключів. - Завжди актуальний — сервер отримує документи в реальному часі з docs.flightphp.com, тому завжди оновлений.
- Працює скрізь — будь-який інструмент, що підтримує транспорт MCP Streamable HTTP, може підключитися.
- Можна хостити самостійно — запустіть власний екземпляр з PHP >= 8.1 та Composer, якщо бажаєте.
Конфігурація IDE / AI-розширення
Сервер використовує транспорт Streamable HTTP. Оберіть ваше розширення нижче та вставте конфігурацію.
Claude Code (CLI)
Запустіть таку команду, щоб додати до вашого проекту:
claude mcp add --transport http flightphp-docs https://mcp.flightphp.com/mcp
Або додайте вручну до .mcp.json вашого проекту:
{
"mcpServers": {
"flightphp-docs": {
"type": "http",
"url": "https://mcp.flightphp.com/mcp"
}
}
}
GitHub Copilot (VS Code)
Додайте до .vscode/mcp.json у вашому робочому просторі:
{
"servers": {
"flightphp-docs": {
"type": "http",
"url": "https://mcp.flightphp.com/mcp"
}
}
}
Kilo Code (VS Code)
Додайте до settings.json вашого VS Code:
{
"kilocode.mcpServers": {
"flightphp-docs": {
"url": "https://mcp.flightphp.com/mcp",
"transport": "streamable-http"
}
}
}
Continue.dev (VS Code / JetBrains)
Додайте до ~/.continue/config.json:
{
"mcpServers": [
{
"name": "flightphp-docs",
"transport": {
"type": "http",
"url": "https://mcp.flightphp.com/mcp"
}
}
]
}
Доступні інструменти
MCP-сервер надає такі інструменти вашому AI-асистенту:
| Tool | Description |
|---|---|
list_docs_pages |
Перелічує всі доступні теми основної документації з слагами та описами |
get_docs_page |
Отримує сторінку основної документації за слагом теми (наприклад, routing, middleware, security) |
list_guide_pages |
Перелічує всі доступні покрокові посібники |
get_guide_page |
Отримує повний посібник за слагом (наприклад, blog, unit-testing) |
list_plugin_pages |
Перелічує всі доступні сторінки плагінів та розширень |
get_plugin_docs |
Отримує повну документацію плагіна за слагом (наприклад, active-record, session, jwt) |
search_docs |
Шукає по всіх документах, посібниках та плагінах за ключовим словом або темою |
fetch_url |
Отримує будь-яку сторінку безпосередньо за її повною URL docs.flightphp.com |
Самостійне хостування
Бажаєте запустити власний екземпляр? Вам знадобиться PHP >= 8.1 та Composer.
git clone https://github.com/flightphp/mcp.git
cd mcp
composer install
php server.php
Сервер запускається за замовчуванням на http://0.0.0.0:8890/mcp. Оновіть конфігурацію IDE, щоб вказати на вашу локальну адресу:
{
"mcpServers": {
"flightphp-docs": {
"type": "http",
"url": "http://localhost:8890/mcp"
}
}
}Awesome-plugins/async
Async
Async — це невеликий пакет для фреймворку Flight, який дозволяє запускати ваші додатки Flight у асинхронних серверах та середовищах виконання, таких як Swoole, AdapterMan, ReactPHP, Amp, RoadRunner, Workerman тощо. З коробки він включає адаптери для Swoole та AdapterMan.
Мета: розробка та налагодження з PHP-FPM (або вбудованим сервером) та перехід на Swoole (або інший асинхронний драйвер) для продакшену з мінімальними змінами.
Вимоги
- PHP 7.4 або вище
- Фреймворк Flight 3.16.1 або вище
- Розширення Swoole
Встановлення
Встановіть через composer:
composer require flightphp/async
Якщо плануєте запускати з Swoole, встановіть розширення:
# за допомогою pecl
pecl install swoole
# або openswoole
pecl install openswoole
# або з менеджером пакетів (приклад для Debian/Ubuntu)
sudo apt-get install php-swoole
Швидкий приклад Swoole
Нижче наведено мінімальну конфігурацію, яка показує, як підтримувати як PHP-FPM (або вбудований сервер), так і Swoole, використовуючи один і той самий код.
Файли, які знадобляться у вашому проєкті:
- index.php
- swoole_server.php
- SwooleServerDriver.php
index.php
Цей файл — проста перемикачка, яка змушує додаток працювати в режимі PHP для розробки.
// index.php
<?php
define('NOT_SWOOLE', true);
include 'swoole_server.php';
swoole_server.php
Цей файл ініціалізує ваш додаток Flight і запустить драйвер Swoole, коли NOT_SWOOLE не визначено.
// swoole_server.php
<?php
require_once __DIR__ . '/vendor/autoload.php';
$app = Flight::app();
$app->route('/', function() use ($app) {
$app->json(['hello' => 'world']);
});
if (!defined('NOT_SWOOLE')) {
// Require the SwooleServerDriver class when running in Swoole mode.
require_once __DIR__ . '/SwooleServerDriver.php';
Swoole\Runtime::enableCoroutine();
$Swoole_Server = new SwooleServerDriver('127.0.0.1', 9501, $app);
$Swoole_Server->start();
} else {
$app->start();
}
SwooleServerDriver.php
Стислий драйвер, який показує, як передавати запити Swoole у Flight за допомогою AsyncBridge та адаптерів Swoole.
// SwooleServerDriver.php
<?php
use flight\adapter\SwooleAsyncRequest;
use flight\adapter\SwooleAsyncResponse;
use flight\AsyncBridge;
use flight\Engine;
use Swoole\HTTP\Server as SwooleServer;
use Swoole\HTTP\Request as SwooleRequest;
use Swoole\HTTP\Response as SwooleResponse;
class SwooleServerDriver {
protected $Swoole;
protected $app;
public function __construct(string $host, int $port, Engine $app) {
$this->Swoole = new SwooleServer($host, $port);
$this->app = $app;
$this->setDefault();
$this->bindWorkerEvents();
$this->bindHttpEvent();
}
protected function setDefault() {
$this->Swoole->set([
'daemonize' => false,
'dispatch_mode' => 1,
'max_request' => 8000,
'open_tcp_nodelay' => true,
'reload_async' => true,
'max_wait_time' => 60,
'enable_reuse_port' => true,
'enable_coroutine' => true,
'http_compression' => false,
'enable_static_handler' => true,
'document_root' => __DIR__,
'static_handler_locations' => ['/css', '/js', '/images', '/.well-known'],
'buffer_output_size' => 4 * 1024 * 1024,
'worker_num' => 4,
]);
$app = $this->app;
$app->map('stop', function (?int $code = null) use ($app) {
if ($code !== null) {
$app->response()->status($code);
}
});
}
protected function bindHttpEvent() {
$app = $this->app;
$AsyncBridge = new AsyncBridge($app);
$this->Swoole->on('Start', function(SwooleServer $server) {
echo "Swoole http server is started at http://127.0.0.1:9501\n";
});
$this->Swoole->on('Request', function (SwooleRequest $request, SwooleResponse $response) use ($AsyncBridge) {
$SwooleAsyncRequest = new SwooleAsyncRequest($request);
$SwooleAsyncResponse = new SwooleAsyncResponse($response);
$AsyncBridge->processRequest($SwooleAsyncRequest, $SwooleAsyncResponse);
$response->end();
gc_collect_cycles();
});
}
protected function bindWorkerEvents() {
$createPools = function() {
// create worker-specific connection pools here
};
$closePools = function() {
// close pools / cleanup here
};
$this->Swoole->on('WorkerStart', $createPools);
$this->Swoole->on('WorkerStop', $closePools);
$this->Swoole->on('WorkerError', $closePools);
}
public function start() {
$this->Swoole->start();
}
}
Запуск сервера
- Розробка (вбудований сервер PHP / PHP-FPM):
- php -S localhost:8000 (або додайте -t public/ якщо ваш index у public/)
- Продакшен (Swoole):
- php swoole_server.php
Порада: Для продакшену використовуйте реверс-проксі (Nginx) перед Swoole для обробки TLS, статичних файлів та балансування навантаження.
Нотатки щодо конфігурації
Драйвер Swoole надає кілька опцій конфігурації:
- worker_num: кількість процесів робочих
- max_request: запити на робочий перед перезапуском
- enable_coroutine: використання корутин для конкурентності
- buffer_output_size: розмір буфера виводу
Налаштуйте ці параметри відповідно до ресурсів вашого хоста та шаблонів трафіку.
Обробка помилок
AsyncBridge перетворює помилки Flight у правильні HTTP-відповіді. Ви також можете додати обробку помилок на рівні маршруту:
$app->route('/*', function() use ($app) {
try {
// route logic
} catch (Exception $e) {
$app->response()->status(500);
$app->json(['error' => $e->getMessage()]);
}
});
AdapterMan та інші середовища виконання
AdapterMan підтримується як альтернативний адаптер середовища виконання. Пакет розроблено для адаптивності — додавання або використання інших адаптерів загалом слідує тому ж шаблону: перетворення серверного запиту/відповіді у запит/відповідь Flight через AsyncBridge та адаптери, специфічні для середовища.
Awesome-plugins/migrations
Міграції
Міграція для вашого проєкту – це відстеження всіх змін бази даних, пов’язаних з вашим проєктом. byjg/php-migration – це справді корисна основна бібліотека, яка допоможе вам розпочати.
Встановлення
PHP Бібліотека
Якщо ви хочете використовувати тільки PHP бібліотеку у вашому проєкті:
composer require "byjg/migration"
Інтерфейс командного рядка
Інтерфейс командного рядка є самостійним і не вимагає, щоб ви встановлювали його разом із вашим проєктом.
Ви можете встановити його глобально і створити символічне посилання
composer require "byjg/migration-cli"
Будь ласка, відвідайте byjg/migration-cli, щоб отримати більше інформації про Migration CLI.
Підтримувані бази даних
| База даних | Драйвер | Строка з’єднання |
|---|---|---|
| Sqlite | pdo_sqlite | sqlite:///path/to/file |
| MySql/MariaDb | pdo_mysql | mysql://username:password@hostname:port/database |
| Postgres | pdo_pgsql | pgsql://username:password@hostname:port/database |
| Sql Server | pdo_dblib, pdo_sysbase Linux | dblib://username:password@hostname:port/database |
| Sql Server | pdo_sqlsrv Windows | sqlsrv://username:password@hostname:port/database |
Як це працює?
Міграція бази даних використовує ЧИСТИЙ SQL для управління версіонуванням бази даних. Щоб це працювало, вам потрібно:
- Створити SQL скрипти
- Керувати за допомогою командного рядка або API.
SQL Скрипти
Скрипти поділені на три набори скриптів:
- БАЗОВИЙ скрипт містить УСІ sql команди для створення нової бази даних;
- UP скрипти містять усі sql команди міграції для "підняття" версії бази даних;
- DOWN скрипти містять усі sql команди міграції для "зниження" або повернення версії бази даних;
Директорія зі скриптами виглядає так:
<root dir>
|
+-- base.sql
|
+-- /migrations
|
+-- /up
|
+-- 00001.sql
+-- 00002.sql
+-- /down
|
+-- 00000.sql
+-- 00001.sql
- "base.sql" – це базовий скрипт
- Папка "up" містить скрипти для підняття версії. Наприклад: 00002.sql – це скрипт для переходу бази даних з версії '1' на '2'.
- Папка "down" містить скрипти для зниження версії. Наприклад: 00001.sql – це скрипт для повернення бази даних з версії '2' на '1'. Папка "down" є необов'язковою.
Багаторазове середовище розробки
Якщо ви працюєте з кількома розробниками та кількома гілками, важко визначити, яке наступне число.
У цьому випадку ви можете додати суфікс "-dev" після номера версії.
Погляньте на сценарій:
- Розробник 1 створює гілку, а найновіша версія, наприклад, 42.
- Розробник 2 одночасно створює гілку і має те саме число версії бази даних.
У обох випадках розробники створять файл під назвою 43-dev.sql. Обидва розробники зможуть мігрувати UP і DOWN без проблем, а ваша локальна версія буде 43.
Але розробник 1 об'єднав свої зміни і створив фінальну версію 43.sql (git mv 43-dev.sql 43.sql). Якщо розробник 2 оновить вашу локальну гілку, він отримає файл 43.sql (від розробника 1) і ваш файл 43-dev.sql.
Якщо він спробує мігрувати UP або DOWN, скрипт міграції повідомить про помилку і сповістить його про те, що існує ДВІ версії 43. У такому випадку розробник 2 повинен оновити свій файл на 44-dev.sql і продовжити працювати, поки не об’єднає свої зміни і не створить фінальну версію.
Використання PHP API та інтеграція його у ваші проєкти
Основне використання:
- Створити з'єднання з об'єктом ConnectionManagement. Для отримання додаткової інформації див. компонент "byjg/anydataset"
- Створити об'єкт Migration з цим з’єднанням та папкою, в якій знаходяться sql скрипти.
- Використати відповідну команду для "скидання", "підняття" або "зниження" скриптів міграцій.
Дивіться приклад:
<?php
// Створіть URI з'єднання
// Дивіться більше: https://github.com/byjg/anydataset#connection-based-on-uri
$connectionUri = new \ByJG\Util\Uri('mysql://migrateuser:migratepwd@localhost/migratedatabase');
// Зареєструйте базу даних або бази даних, які можуть обробляти цей URI:
\ByJG\DbMigration\Migration::registerDatabase(\ByJG\DbMigration\Database\MySqlDatabase::class);
// Створіть екземпляр Migration
$migration = new \ByJG\DbMigration\Migration($connectionUri, '.');
// Додайте функцію зворотного виклику для отримання інформації про виконання
$migration->addCallbackProgress(function ($action, $currentVersion, $fileInfo) {
echo "$action, $currentVersion, ${fileInfo['description']}\n";
});
// Відновіть базу даних за допомогою скрипта "base.sql"
// і виконайте УСІ існуючі скрипти, щоб підняти версію бази даних до останньої
$migration->reset();
// Виконайте УСІ існуючі скрипти для підняття чи зниження версії бази даних
// з поточної версії до номера $version;
// Якщо номер версії не вказано, мігруйте до останньої версії бази даних
$migration->update($version = null);
Об'єкт Migration контролює версію бази даних.
Створення контролю версій у вашому проєкті
<?php
// Зареєструйте базу даних або бази даних, які можуть обробляти цей URI:
\ByJG\DbMigration\Migration::registerDatabase(\ByJG\DbMigration\Database\MySqlDatabase::class);
// Створіть екземпляр Migration
$migration = new \ByJG\DbMigration\Migration($connectionUri, '.');
// Ця команда створить таблицю версій у вашій базі даних
$migration->createVersion();
Отримання поточної версії
<?php
$migration->getCurrentVersion();
Додати зворотний виклик для контролю прогресу
<?php
$migration->addCallbackProgress(function ($command, $version, $fileInfo) {
echo "Виконання команди: $command на версії $version - ${fileInfo['description']}, ${fileInfo['exists']}, ${fileInfo['file']}, ${fileInfo['checksum']}\n";
});
Отримання екземпляра драйвера бази даних
<?php
$migration->getDbDriver();
Щоб використовувати це, будь ласка, відвідайте: https://github.com/byjg/anydataset-db
Уникнення часткової міграції (не доступно для MySQL)
Часткова міграція – це коли скрипт міграції переривається посеред процесу через помилку або ручну переривання.
Таблиця міграції буде мати статус partial up або partial down, і її потрібно буде виправити вручну, перш ніж можна буде мігрувати знову.
Щоб уникнути цієї ситуації, ви можете вказати, що міграція буде виконуватись у транзакційному контексті. Якщо скрипт міграції не вдасться, транзакція буде скасована, а таблиця міграції буде позначена як complete, і версія буде одразу попередньою версією до скрипта, який спричинив помилку.
Щоб увімкнути цю функцію, вам потрібно викликати метод withTransactionEnabled, передавши true як параметр:
<?php
$migration->withTransactionEnabled(true);
ПРИМІТКА: Ця функція недоступна для MySQL, оскільки він не підтримує DDL команди всередині транзакції. Якщо ви використовуєте цей метод з MySQL, Migration проігнорує його тихо. Більше інформації: https://dev.mysql.com/doc/refman/8.0/en/cannot-roll-back.html
Поради щодо написання SQL міграцій для Postgres
При створенні тригерів і SQL-функцій
-- DO
CREATE FUNCTION emp_stamp() RETURNS trigger AS $emp_stamp$
BEGIN
-- Перевірте, що ім'я працівника та зарплата вказані
IF NEW.empname IS NULL THEN
RAISE EXCEPTION 'empname не може бути null'; -- не має значення, якщо ці коментарі пусті
END IF; --
IF NEW.salary IS NULL THEN
RAISE EXCEPTION '% не може мати null зарплату', NEW.empname; --
END IF; --
-- Хто працює на нас, коли вони повинні це оплачувати?
IF NEW.salary < 0 THEN
RAISE EXCEPTION '% не може мати негативну зарплату', NEW.empname; --
END IF; --
-- Запам'ятайте, хто змінив платіж від коли
NEW.last_date := current_timestamp; --
NEW.last_user := current_user; --
RETURN NEW; --
END; --
$emp_stamp$ LANGUAGE plpgsql;
-- DON'T
CREATE FUNCTION emp_stamp() RETURNS trigger AS $emp_stamp$
BEGIN
-- Перевірте, що ім'я працівника та зарплата вказані
IF NEW.empname IS NULL THEN
RAISE EXCEPTION 'empname не може бути null';
END IF;
IF NEW.salary IS NULL THEN
RAISE EXCEPTION '% не може мати null зарплату', NEW.empname;
END IF;
-- Хто працює на нас, коли вони повинні це оплачувати?
IF NEW.salary < 0 THEN
RAISE EXCEPTION '% не може мати негативну зарплату', NEW.empname;
END IF;
-- Запам'ятайте, хто змінив платіж від коли
NEW.last_date := current_timestamp;
NEW.last_user := current_user;
RETURN NEW;
END;
$emp_stamp$ LANGUAGE plpgsql;
Оскільки абстрактний рівень бази даних PDO не може виконувати партії SQL заявок, коли byjg/migration читає файл міграції, він повинен розділити весь вміст SQL файлу на частини за крапками з коми та виконати заяви одну за одною. Однак є один вид заяви, який може містити кілька крапок з коми між його тілом: функції.
Щоб мати можливість правильно парсити функції, byjg/migration 2.1.0 почав розділяти файли міграцій за послідовністю semicolon + EOL замість лише за крапкою з комою. Таким чином, якщо ви додасте пустий коментар після кожної внутрішньої крапки з комою у визначенні функції, byjg/migration зможе їх правильно розпізнати.
На жаль, якщо ви забудете додати будь-який з цих коментарів, бібліотека розділить заяву CREATE FUNCTION на кілька частин, і міграція завершиться невдачею.
Уникнення символа двокрапки (:)
-- DO
CREATE TABLE bookings (
booking_id UUID PRIMARY KEY,
booked_at TIMESTAMPTZ NOT NULL CHECK (CAST(booked_at AS DATE) <= check_in),
check_in DATE NOT NULL
);
-- DON'T
CREATE TABLE bookings (
booking_id UUID PRIMARY KEY,
booked_at TIMESTAMPTZ NOT NULL CHECK (booked_at::DATE <= check_in),
check_in DATE NOT NULL
);
Оскільки PDO використовує символ двокрапки для префікса названих параметрів у підготовлених запитах, його використання призведе до помилки в інших контекстах.
Наприклад, заяви PostgreSQL можуть використовувати :: для приведення значень між типами. З іншого боку, PDO прочитає це як недійсний названий параметр в недійсному контексті і завершить спробу його виконання з помилкою.
Єдиний спосіб виправити цю невідповідність – це уникати двокрапок взагалі (у цьому випадку PostgreSQL також має альтернативний синтаксис: CAST(value AS type)).
Використовуйте SQL редактор
Нарешті, написання ручних SQL міграцій може бути виснажливим, але це набагато простіше, якщо ви використовуєте редактор, здатний розуміти синтаксис SQL, надавати автозаповнення, досліджувати вашу поточну схему бази даних та/або автоматично форматувати ваш код.
Обробка різних міграцій всередині однієї схеми
Якщо вам потрібно створити різні скрипти міграцій та версії в одній схемі, це можливо, але надто ризиковано, і я не рекомендую цього зовсім.
Для цього вам потрібно створити різні "таблиці міграцій", передаючи параметр конструктору.
<?php
$migration = new \ByJG\DbMigration\Migration("db:/uri", "/path", true, "NEW_MIGRATION_TABLE_NAME");
З міркувань безпеки ця функція недоступна з командного рядка, але ви можете використовувати змінну середовища MIGRATION_VERSION, щоб зберегти ім'я.
Ми справді рекомендуємо не використовувати цю функцію. Рекомендація – одна міграція для однієї схеми.
Запуск юніт-тестів
Основні юніт-тести можна запускати за допомогою:
vendor/bin/phpunit
Запуск тестів бази даних
Запуск інтеграційних тестів вимагає, щоб бази даних були запущені. Ми надали базовий docker-compose.yml, який ви можете використовувати для запуску баз даних для тестування.
Запуск баз даних
docker-compose up -d postgres mysql mssql
Запуск тестів
vendor/bin/phpunit
vendor/bin/phpunit tests/SqliteDatabase*
vendor/bin/phpunit tests/MysqlDatabase*
vendor/bin/phpunit tests/PostgresDatabase*
vendor/bin/phpunit tests/SqlServerDblibDatabase*
vendor/bin/phpunit tests/SqlServerSqlsrvDatabase*
Опціонально, ви можете встановити хост і пароль, які використовуються юніт-тестами
export MYSQL_TEST_HOST=localhost # за замовчуванням localhost
export MYSQL_PASSWORD=newpassword # використовуйте '.' якщо хочете мати порожній пароль
export PSQL_TEST_HOST=localhost # за замовчуванням localhost
export PSQL_PASSWORD=newpassword # використовуйте '.' якщо хочете мати порожній пароль
export MSSQL_TEST_HOST=localhost # за замовчуванням localhost
export MSSQL_PASSWORD=Pa55word
export SQLITE_TEST_HOST=/tmp/test.db # за замовчуванням /tmp/test.dbAwesome-plugins/flightmail
FlightMail
Сторонній плагін - підтримується Ryan Stubbs (ryanstubbs/flightmail, ліцензія MIT). Не є частиною ядра Flight - будь ласка, повідомляйте про проблеми в його репозиторії на GitHub.
ryanstubbs/flightmail дозволяє надсилати електронну пошту з вашого додатка Flight без головного болю. Він обгортає Symfony Mailer - найбільш перевірену в бою поштову бібліотеку в PHP - і робить так, ніби це частина Flight. Один рядок для встановлення, один fluent-ланцюжок для надсилання:
Flight::mail()->compose()
->to('someone@example.com')
->subject('У вас вийшло!')
->text('Ваш перший лист уже в дорозі.')
->send();
Можливості
- Будь-який провайдер — по одному рядку. SMTP, Postmark, Sendgrid, Mailgun, Amazon SES, Brevo і компанія працюють через прості DSN-рядки.
- Кілька провайдерів одразу. Транзакційні листи через Postmark, розсилки через власний SMTP — обирайте для кожного повідомлення.
- Шаблони, якщо захочете. Рендеріть тіла листів через Twig або Latte. Не хочете шаблони? Просто передайте рядки й не встановлюйте нічого зайвого.
- Глянець у момент надсилання. Необов'язкове вбудовування CSS і автоматичні текстові частини з вашого HTML — на бібліотеках, які ставляться лише якщо ви ними користуєтесь.
- Нудний у найкращому сенсі. Ліниві з'єднання, зрозумілі помилки замість тихо проковтнутих листів, і все можна підмінити, якщо потрібно щось своє.
Вимоги
| Що | Версія |
|---|---|
| PHP | 8.2 або новіша |
| Flight PHP | core ^3.15 |
| Symfony Mailer | ^7.2 або ^8.0 (встановлюється автоматично) |
Встановлення
composer require ryanstubbs/flightmail
Цього достатньо для надсилання листів у простому тексті та HTML. Рендеринг шаблонів підключається за бажанням — додайте шаблонізатор, лише якщо ним користуватиметесь:
composer require twig/twig # для шаблонів .twig
composer require latte/latte # для шаблонів .latte
Ще дві необов'язкові бібліотеки забезпечують покращення в момент надсилання, описані нижче:
composer require pelago/emogrifier # для вбудовування CSS ("inline_css")
composer require league/html-to-markdown # для текстових частин у Markdown ("text_from_html")
Усі їх можна ставити поряд; FlightMail обере потрібну, спираючись на вашу конфігурацію.
Ваш перший лист
Додайте це до bootstrap (туди ж, де ви визначаєте маршрути):
<?php
require 'vendor/autoload.php';
use ryanstubbs\FlightMail\MailPlugin;
// Скажіть FlightMail, звідки і через що надсилати пошту.
MailPlugin::install([
'dsns' => [
'default' => 'smtp://user:pass@localhost:1025',
],
'from' => 'no-reply@example.com',
]);
Flight::route('/signup', function () {
Flight::mail()->compose()
->to('new-user@example.com')
->subject('Ласкаво просимо на борт!')
->html('<h1>Ласкаво просимо!</h1><p>Ми раді, що ви тут.</p>')
->send();
});
Flight::start();
Користуєтесь скелетом Flight PHP? Зареєструйте в app/config/services.php у стилі екземпляра:
use ryanstubbs\FlightMail\MailPlugin;
MailPlugin::register($app, [
'dsns' => ['default' => 'smtp://user:pass@localhost:1025'],
'from' => 'no-reply@example.com',
]);
Обидва стилі дають той самий мейлер: Flight::mail() і $app->mail() взаємозамінні.
Тестуєте локально? Якщо проєкт крутиться в DDEV, спрямуйте DSN на
smtp://127.0.0.1:1025і читайте кожен перехоплений лист у Mailpit за адресоюhttp://<project>.ddev.site:8025. Нічого не покидає вашу машину.
Надсилання електронної пошти
Прості рядки (шаблонізатор не потрібен)
->text() і ->html() приймають звичайні рядки, і більше нічого встановлювати не потрібно:
Flight::mail()->compose()
->to('ops@example.com')
->subject('Резервне копіювання завершено')
->text('Нічне резервне копіювання завершено за 42 хвилини.')
->send();
Flight::mail()->compose()
->to('billing@example.com')
->subject('Рахунок №123')
->html('<h1>Рахунок №123</h1><p>До сплати: $42.00</p>')
->send();
Шаблони Twig
// welcome.html.twig містить: Привіт, {{ name }}, дякуємо за реєстрацію!
Flight::mail()->compose()
->to('someone@example.com')
->subject('Ласкаво просимо!')
->template('welcome.html.twig', ['name' => 'Ryan'])
->send();
Шаблони Latte
Та сама ідея, розширення .latte:
// welcome.latte містить: Привіт, {$name}, дякуємо за реєстрацію!
Flight::mail()->compose()
->to('someone@example.com')
->subject('Ласкаво просимо!')
->template('welcome.latte', ['name' => 'Ryan'])
->send();
HTML + простий текст разом
Найкраща практика для доставлюваності — дайте поштовим клієнтам обидві версії:
Flight::mail()->compose()
->to('someone@example.com')
->subject('Ласкаво просимо!')
->template('welcome.html.twig', ['name' => 'Ryan']) // багата версія
->textTemplate('welcome.txt.twig', ['name' => 'Ryan']) // запасна версія
->send();
Кілька речей, які варто знати про шаблони:
- Вони рендеряться ліниво, у момент надсилання — складайте зараз, рендеріть потім.
- Шаблонізатор обирається за розширенням:
.twig→ Twig,.latte→ Latte, усе інше → ваш налаштований за замовчуванням (опціяrenderer). - Явне тіло
->html()або->text()завжди перемагає шаблон, тож можна задати шаблон за замовчуванням і перевизначити його для конкретного повідомлення.
Стилізація HTML і генерація текстових частин
Два необов'язкові покращення в момент надсилання, обидва вимкнені за замовчуванням і обидва працюють на бібліотеках, які ви ставите лише якщо хочете:
| Функція | Встановлення | Ключ конфігурації |
|---|---|---|
| Вбудовування CSS | pelago/emogrifier |
inline_css |
| Текстова частина з HTML | league/html-to-markdown |
text_from_html |
Вбудовування CSS у HTML-лист
Gmail і більшість клієнтів вебпошти вирізають блоки <style> — атрибути style="" усередині тегів — єдина стилізація, яку вони надійно шанують. Писати їх вручну — мука; нехай Emogrifier зробить це в момент надсилання:
composer require pelago/emogrifier
MailPlugin::install([
'dsns' => ['default' => 'smtp://user:pass@localhost:1025'],
'inline_css' => true,
]);
Коли це увімкнено, кожне HTML-тіло отримує вбудований CSS безпосередньо перед надсиланням — чи то з шаблону, чи з ->html(). Повідомлення на кшталт <style>p { color: red; }</style><p>Привіт</p> йде як <p style="color: red;">Привіт</p>.
Щоб упровадити спільні стилі в кожен лист (кольори бренду, скидання стилів) без повторення їх у кожному шаблоні, передайте правила напряму або вкажіть файл таблиці стилів:
'inline_css' => ['css_file' => __DIR__ . '/mail-styles/base.css'],
// або
'inline_css' => ['css' => '.button { background: #0a84ff; color: #fff; }'],
Керування на рівні повідомлення:
$message->inlineCss(); // примусово вбудувати CSS для цього повідомлення
$message->withoutInlineCss(); // пропустити, навіть якщо увімкнено глобально
Генерація текстової частини з HTML
Найкраща практика — надсилати HTML і просту текстову версію разом, але писати обидві стомлює. FlightMail може вивести текстову частину з підсумкового HTML автоматично — базова конвертація не потребує зайвої залежності, конвертер постачається разом із Symfony Mime:
MailPlugin::install([
'dsns' => ['default' => 'smtp://user:pass@localhost:1025'],
'text_from_html' => true, // Markdown коли можливо, інакше простий текст
]);
Режими:
trueабо'auto'— вивід Markdown, якщо встановленоleague/html-to-markdown, інакше просте зрізання тегів.'markdown'— примусово Markdown (composer require league/html-to-markdown; заголовки стають==, посилання[text](url), жирний**bold**).'plain'— завжди зрізати теги; працює без додаткових пакетів.
Генерація запускається після рендерингу та вбудовування CSS і лише коли в повідомлення є HTML-тіло, але немає текстового — явне ->text() або ->textTemplate() завжди перемагає. Перевизначення на рівні повідомлення дзеркалять вбудовування:
$message->textFromHtml('plain'); // примусово зрізати теги для цього листа
$message->withoutTextFromHtml(); // лист лише з HTML
Увімкніть режим, чия бібліотека не встановлена — отримаєте зрозумілу помилку з точною командою composer require, яку потрібно виконати, без тихої деградації.
Вибір провайдера
Провайдери підключаються через DSN-рядки. Встановіть пакет-міст, вставте DSN у dsns, готово.
| Провайдер | Встановлення | Приклад DSN |
|---|---|---|
| SMTP | вбудований | smtp://user:pass@host:587 |
| Sendmail | вбудований | sendmail://default |
| Dev/null (відкидати листи) | вбудований | null://null |
| Postmark | composer require symfony/postmark-mailer |
postmark+api://KEY@api.postmarkapp.com |
| Sendgrid | composer require symfony/sendgrid-mailer |
sendgrid+api://KEY@default |
| Mailgun | composer require symfony/mailgun-mailer |
mailgun+https://KEY:DOMAIN@api.mailgun.net |
| Amazon SES | composer require symfony/amazon-mailer |
ses+https://KEY:SECRET@default |
| Brevo | composer require symfony/brevo-mailer |
brevo+api://KEY@default |
| MailerSend | composer require symfony/mailersend-mailer |
mailersend+api://KEY@default |
Повний список живе в документації Symfony Mailer — усе, що там описано, працює тут без змін.
Кілька провайдерів одночасно
Дайте ім'я кожному транспорту, потім обирайте для повідомлення:
MailPlugin::install([
'dsns' => [
'transactional' => 'postmark+api://KEY@api.postmarkapp.com',
'bulk' => 'smtp://user:pass@bulk.example.com:587',
],
'from' => 'no-reply@example.com',
]);
// Немає виклику ->transport() = перший ключ у "dsns" (тут "transactional").
Flight::mail()->compose()->to('...')->text('квитанція')->send();
// Явно обрати інший маршрут.
Flight::mail()->compose()->to('...')->text('розсилка')->transport('bulk')->send();
Довідник конфігурації
Усе необов'язкове, крім dsns.
MailPlugin::install([
// ОБОВ'ЯЗКОВО - ім'я транспорту => Symfony DSN.
// Перший запис використовується, коли повідомлення не вказує транспорт.
'dsns' => [
'default' => 'smtp://user:pass@localhost:1025',
],
// Транспорт, коли в повідомлення немає явного ->transport() і
// ви не хочете перший ключ. Має існувати в "dsns".
'default_transport' => 'default',
// Глобальний відправник. Рядок, Symfony Address або ['email' => 'Name'].
// Застосовується лише коли повідомлення не задає свій ->from().
'from' => ['no-reply@example.com' => 'Мій додаток'],
// Шаблонізатор за замовчуванням: 'twig', 'latte' або власне ім'я.
// Використовується лише для шаблонів, чиє розширення не є зареєстрованим рендерером.
'renderer' => 'twig',
// Де живуть шаблони, пошук за порядком; плюс необов'язковий каталог кешу.
'templates' => [
'paths' => [__DIR__ . '/mail-templates'],
'cache' => __DIR__ . '/cache/mail',
],
// Додаткові опції, які передаються напряму в Twig\Environment.
'twig' => ['options' => ['strict_variables' => true]],
// Налаштування рушія Latte під час завантаження: fn(Latte\Engine $engine): void.
'latte' => ['setup' => static fn (Latte\Engine $e) => $e->addExtension(new MyExtension())],
// Покращення тіла листа під час надсилання (див. «Стилізація HTML і генерація текстових частин»).
'inline_css' => true, // або ['css' => '...', 'css_file' => '...']
'text_from_html' => true, // або 'plain' / 'markdown'
// Власні схеми DSN, власні рендерери, хуки перед надсиланням (див. нижче).
'transport_factories' => [],
'renderers' => [],
'hooks' => [],
// Необов'язкова обв'язка, що передається кожному транспорту.
'event_dispatcher' => $dispatcher, // Symfony MessageEvents
'logger' => $psr3Logger,
]);
Йдемо далі
Усе нижче необов'язкове. Значення за замовчуванням покривають більшість додатків.
Додати власну схему DSN
Реалізуйте TransportFactoryInterface із Symfony і зареєструйте його — тоді ваша схема працюватиме точно як вбудована:
use ryanstubbs\FlightMail\MailPlugin;
use Symfony\Component\Mailer\Transport\Dsn;
use Symfony\Component\Mailer\Transport\TransportFactoryInterface;
use Symfony\Component\Mailer\Transport\TransportInterface;
class MyCarrierFactory implements TransportFactoryInterface
{
public function supports(Dsn $dsn): bool
{
return $dsn->getScheme() === 'mycarrier';
}
public function create(Dsn $dsn): TransportInterface
{
// ... зберіть транспорт, який спілкується з вашим оператором
}
}
$plugin = MailPlugin::install(['dsns' => ['carrier' => 'mycarrier://key']]);
$plugin->addTransportFactory(new MyCarrierFactory());
Додати власний рендерер шаблонів
Підійде все, що перетворює ім'я шаблону плюс параметри на рядок:
use ryanstubbs\FlightMail\MailPlugin;
use ryanstubbs\FlightMail\Render\RendererInterface;
$plugin = MailPlugin::install($config);
$plugin->addRenderer('markdown', fn (array $config): RendererInterface =>
new MarkdownMailRenderer($config['templates']['paths'] ?? [])
);
// Шаблони із закінченням .markdown тепер використовують його автоматично:
Flight::mail()->compose()->to('...')->template('welcome.markdown', ['name' => 'Ryan'])->send();
Виконати щось безпосередньо перед надсиланням
Хуки отримують готове повідомлення — після рендерингу, після значень за замовчуванням, безпосередньо перед відправкою в мережу:
$plugin->addHook(function (ryanstubbs\FlightMail\Message $message): void {
$message->getHeaders()->addTextHeader('X-Mailer', 'MyApp/1.0');
});
Події та логування
Передайте диспетчер подій Symfony та/або PSR-3 логер — кожен транспорт їх використовуватиме:
$plugin->eventDispatcher($dispatcher); // отримує MessageEvent перед кожним надсиланням
$plugin->logger($logger); // логи на рівні транспорту
Шпаргалка з API
// Налаштування
MailPlugin::install($config) // реєстрація в глобальному додатку Flight
MailPlugin::register($app, $config) // реєстрація в конкретному Engine
$mailer = Flight::mail(); // спільний екземпляр Mailer
// Збирання повідомлень
$mailer->compose(): Message
$message->to(...)->from(...)->subject(...) // стандартні методи Symfony Mime
$message->text(string) // тіло зі звичайного рядка
$message->html(string) // тіло з HTML-рядка
$message->template($name, $params) // HTML-тіло з шаблону
$message->htmlTemplate($name, $params) // псевдонім template()
$message->textTemplate($name, $params) // текстове тіло з шаблону
$message->inlineCss() / ->withoutInlineCss() // вбудовування CSS для повідомлення
$message->textFromHtml($mode) // авто текстова частина: true/'auto'/'plain'/'markdown'/false
$message->withoutTextFromHtml() // лист лише з HTML
$message->transport($name) // маршрут через іменований DSN
$message->send(): ?SentMessage // рендер + надсилання
// На самому мейлері
$mailer->send($message): ?SentMessage // явна альтернатива $message->send()
$mailer->render($template, $params): string // рендер без надсилання
$mailer->addHook(callable): static // fn(Message $message): void
$mailer->transports(): TransportManager // get() / has() / names()
$mailer->renderers(): RendererFactory // create() / has() / add()
Оскільки Message розширює Symfony\Component\Mime\Email, кожен метод Symfony, який ви вже знаєте — attach(), embed(), priority(), replyTo() — працює з коробки.
Вирішення проблем
"No mail DSNs configured"
Ви викликали Flight::mail() до реєстрації плагіна, або масив конфігурації не містив dsns. Ця помилка навмисна — FlightMail відмовляється вгадувати, куди має йти пошта, замість того щоб тихо її відкидати.
"Unknown mail template renderer ..."
Ви використали шаблон, шаблонізатор якого не встановлено. Виправте за допомогою composer require twig/twig або composer require latte/latte, або зареєструйте власний рендерер з іменем розширення.
"Unknown mail transport ..."
Виклик ->transport('name') (або default_transport) не збігається з жодним ключем у dsns. Перевірте написання — помилка перелічує налаштовані імена.
Листи не доходять
Спрямуйте dsns на null://null, щоб переконатися, що решта коду працює, потім поверніться до справжнього DSN. У DDEV використовуйте smtp://127.0.0.1:1025 і переглядайте повідомлення в Mailpit на порту 8025.
Звіти про помилки, pull request'и та повний вихідний код — у репозиторії на GitHub.
Awesome-plugins/comment_template
CommentTemplate
CommentTemplate — це потужний шаблонний двигун PHP з компіляцією активів, успадкуванням шаблонів та обробкою змінних. Він надає простий, але гнучкий спосіб керування шаблонами з вбудованою мініфікацією CSS/JS та кешуванням.
Особливості
- Успадкування шаблонів: Використовуйте макети та включайте інші шаблони
- Компіляція активів: Автоматична мініфікація та кешування CSS/JS
- Обробка змінних: Змінні шаблонів з фільтрами та командами
- Кодування Base64: Вбудовані активи як data URI
- Інтеграція з Flight Framework: Опціональна інтеграція з PHP-фреймворком Flight
Встановлення
Встановіть за допомогою composer.
composer require knifelemon/comment-template
Базова конфігурація
Є деякі базові опції конфігурації для початку роботи. Ви можете прочитати більше про них у CommentTemplate Repo.
Метод 1: Використання функції зворотного виклику
<?php
require_once 'vendor/autoload.php';
use KnifeLemon\CommentTemplate\Engine;
$app = Flight::app();
$app->register('view', Engine::class, [], function (Engine $engine) use ($app) {
// Кореневий каталог (де знаходиться index.php) — корінь документа вашого веб-додатка
$engine->setPublicPath(__DIR__);
// Каталог файлів шаблонів — підтримує як відносні, так і абсолютні шляхи
$engine->setSkinPath('views'); // Відносно до public path
// Де зберігатимуться скомпільовані активи — підтримує як відносні, так і абсолютні шляхи
$engine->setAssetPath('assets'); // Відносно до public path
// Розширення файлу шаблону
$engine->setFileExtension('.php');
});
$app->map('render', function(string $template, array $data) use ($app): void {
echo $app->view()->render($template, $data);
});
Метод 2: Використання параметрів конструктора
<?php
require_once 'vendor/autoload.php';
use KnifeLemon\CommentTemplate\Engine;
$app = Flight::app();
// __construct(string $publicPath = "", string $skinPath = "", string $assetPath = "", string $fileExtension = "")
$app->register('view', Engine::class, [
__DIR__, // publicPath — кореневий каталог (де index.php)
'views', // skinPath — шлях до шаблонів (підтримує відносні/абсолютні)
'assets', // assetPath — шлях до скомпільованих активів (підтримує відносні/абсолютні)
'.php' // fileExtension — розширення файлу шаблону
]);
$app->map('render', function(string $template, array $data) use ($app): void {
echo $app->view()->render($template, $data);
});
Конфігурація шляхів
CommentTemplate надає інтелектуальне керування шляхами як для відносних, так і для абсолютних шляхів:
Public Path
Public Path — це кореневий каталог вашого веб-додатка, зазвичай де розташовано index.php. Це корінь документа, з якого веб-сервери обслуговують файли.
// Приклад: якщо ваш index.php знаходиться в /var/www/html/myapp/index.php
$template->setPublicPath('/var/www/html/myapp'); // Кореневий каталог
// Приклад для Windows: якщо ваш index.php знаходиться в C:\xampp\htdocs\myapp\index.php
$template->setPublicPath('C:\\xampp\\htdocs\\myapp');
Конфігурація шляху до шаблонів
Шлях до шаблонів підтримує як відносні, так і абсолютні шляхи:
$template = new Engine();
$template->setPublicPath('/var/www/html/myapp'); // Кореневий каталог (де index.php)
// Відносні шляхи — автоматично об'єднуються з public path
$template->setSkinPath('views'); // → /var/www/html/myapp/views/
$template->setSkinPath('templates/pages'); // → /var/www/html/myapp/templates/pages/
// Абсолютні шляхи — використовуються як є (Unix/Linux)
$template->setSkinPath('/var/www/templates'); // → /var/www/templates/
$template->setSkinPath('/full/path/to/templates'); // → /full/path/to/templates/
// Абсолютні шляхи для Windows
$template->setSkinPath('C:\\www\\templates'); // → C:\www\templates\
$template->setSkinPath('D:/projects/templates'); // → D:/projects/templates/
// UNC шляхи (мережеві ресурси Windows)
$template->setSkinPath('\\\\server\\share\\templates'); // → \\server\share\templates\
Конфігурація шляху до активів
Шлях до активів також підтримує як відносні, так і абсолютні шляхи:
// Відносні шляхи — автоматично об'єднуються з public path
$template->setAssetPath('assets'); // → /var/www/html/myapp/assets/
$template->setAssetPath('static/files'); // → /var/www/html/myapp/static/files/
// Абсолютні шляхи — використовуються як є (Unix/Linux)
$template->setAssetPath('/var/www/cdn'); // → /var/www/cdn/
$template->setAssetPath('/full/path/to/assets'); // → /full/path/to/assets/
// Абсолютні шляхи для Windows
$template->setAssetPath('C:\\www\\static'); // → C:\www\static\
$template->setAssetPath('D:/projects/assets'); // → D:/projects/assets/
// UNC шляхи (мережеві ресурси Windows)
$template->setAssetPath('\\\\server\\share\\assets'); // → \\server\share\assets\
Інтелектуальне виявлення шляхів:
- Відносні шляхи: Без початкових розділювачів (
/,\) або букв дисків - Абсолютні Unix: Починаються з
/(наприклад,/var/www/assets) - Абсолютні Windows: Починаються з літери диска (наприклад,
C:\www,D:/assets) - UNC шляхи: Починаються з
\\(наприклад,\\server\share)
Як це працює:
- Усі шляхи автоматично розв'язуються на основі типу (відносний проти абсолютного)
- Відносні шляхи об'єднуються з public path
@cssта@jsстворюють мініфіковані файли в:{resolvedAssetPath}/css/або{resolvedAssetPath}/js/@assetкопіює окремі файли до:{resolvedAssetPath}/{relativePath}@assetDirкопіює каталоги до:{resolvedAssetPath}/{relativePath}- Інтелектуальне кешування: файли копіюються тільки коли джерело новіше за призначення
Інтеграція з Tracy Debugger
CommentTemplate включає інтеграцію з Tracy Debugger для логування та налагодження під час розробки.

Встановлення
composer require tracy/tracy
Використання
<?php
use KnifeLemon\CommentTemplate\Engine;
use Tracy\Debugger;
// Увімкнути Tracy (має бути викликано перед будь-яким виводом)
Debugger::enable(Debugger::DEVELOPMENT);
Flight::set('flight.content_length', false);
// Перевизначення шаблону
$app->register('view', Engine::class, [], function (Engine $builder) use ($app) {
$builder->setPublicPath($app->get('flight.views.topPath'));
$builder->setAssetPath($app->get('flight.views.assetPath'));
$builder->setSkinPath($app->get('flight.views.path'));
$builder->setFileExtension($app->get('flight.views.extension'));
});
$app->map('render', function(string $template, array $data) use ($app): void {
echo $app->view()->render($template, $data);
});
$app->start();
Функції панелі налагодження
CommentTemplate додає користувацьку панель до панелі налагодження Tracy з чотирма вкладками:
- Overview: Конфігурація, метрики продуктивності та лічильники
- Assets: Деталі компіляції CSS/JS з коефіцієнтами стиснення
- Variables: Оригінальні та перетворені значення з застосованими фільтрами
- Timeline: Хронологічний вигляд усіх операцій шаблонів
Що логується
- Рендеринг шаблонів (початок/кінець, тривалість, макети, імпорти)
- Компіляція активів (файли CSS/JS, розміри, коефіцієнти стиснення)
- Обробка змінних (оригінальні/перетворені значення, фільтри)
- Операції з активами (кодування base64, копіювання файлів)
- Метрики продуктивності (тривалість, використання пам'яті)
Примітка: Нульовий вплив на продуктивність, коли Tracy не встановлено або вимкнено.
Див. повний робочий приклад з Flight PHP.
Директиви шаблонів
Успадкування макетів
Використовуйте макети для створення спільної структури:
layout/global_layout.php:
<!DOCTYPE html>
<html>
<head>
<title>{$title}</title>
</head>
<body>
<!--@contents-->
</body>
</html>
view/page.php:
<!--@layout(layout/global_layout)-->
<h1>{$title}</h1>
<p>{$content}</p>
Керування активами
Файли CSS
<!--@css(/css/styles.css)--> <!-- Мініфіковано та кешовано -->
<!--@cssSingle(/css/critical.css)--> <!-- Один файл, не мініфіковано -->
Файли JavaScript
CommentTemplate підтримує різні стратегії завантаження JavaScript:
<!--@js(/js/script.js)--> <!-- Мініфіковано, завантажено внизу -->
<!--@jsAsync(/js/analytics.js)--> <!-- Мініфіковано, завантажено внизу з async -->
<!--@jsDefer(/js/utils.js)--> <!-- Мініфіковано, завантажено внизу з defer -->
<!--@jsTop(/js/critical.js)--> <!-- Мініфіковано, завантажено в head -->
<!--@jsTopAsync(/js/tracking.js)--> <!-- Мініфіковано, завантажено в head з async -->
<!--@jsTopDefer(/js/polyfill.js)--> <!-- Мініфіковано, завантажено в head з defer -->
<!--@jsSingle(/js/widget.js)--> <!-- Один файл, не мініфіковано -->
<!--@jsSingleAsync(/js/ads.js)--> <!-- Один файл, не мініфіковано, async -->
<!--@jsSingleDefer(/js/social.js)--> <!-- Один файл, не мініфіковано, defer -->
Директиви активів у файлах CSS/JS
CommentTemplate також обробляє директиви активів у файлах CSS та JavaScript під час компіляції:
Приклад CSS:
/* У ваших файлах CSS */
@font-face {
font-family: 'CustomFont';
src: url('<!--@asset(fonts/custom.woff2)-->') format('woff2');
}
.background-image {
background: url('<!--@asset(images/bg.jpg)-->');
}
.inline-icon {
background: url('<!--@base64(icons/star.svg)-->');
}
Приклад JavaScript:
/* У ваших файлах JS */
const fontUrl = '<!--@asset(fonts/custom.woff2)-->';
const imageData = '<!--@base64(images/icon.png)-->';
Кодування Base64
<!--@base64(images/logo.png)--> <!-- Вбудовано як data URI -->
Приклад:
<!-- Вбудовуйте малі зображення як data URI для швидшого завантаження -->
<img src="<!--@base64(images/logo.png)-->" alt="Logo">
<div style="background-image: url('<!--@base64(icons/star.svg)-->');">
Маленька іконка як фон
</div>
Копіювання активів
<!--@asset(images/photo.jpg)--> <!-- Копіювати один актив до публічного каталогу -->
<!--@assetDir(assets)--> <!-- Копіювати весь каталог до публічного каталогу -->
Приклад:
<!-- Копіювати та посилатися на статичні активи -->
<img src="<!--@asset(images/hero-banner.jpg)-->" alt="Hero Banner">
<a href="<!--@asset(documents/brochure.pdf)-->" download>Завантажити брошуру</a>
<!-- Копіювати весь каталог (шрифти, іконки тощо) -->
<!--@assetDir(assets/fonts)-->
<!--@assetDir(assets/icons)-->
Включення шаблонів
<!--@import(components/header)--> <!-- Включити інші шаблони -->
Приклад:
<!-- Включити повторно використовувані компоненти -->
<!--@import(components/header)-->
<main>
<h1>Ласкаво просимо на наш веб-сайт</h1>
<!--@import(components/sidebar)-->
<div class="content">
<p>Основний вміст тут...</p>
</div>
</main>
<!--@import(components/footer)-->
Обробка змінних
Базові змінні
<h1>{$title}</h1>
<p>{$description}</p>
Фільтри змінних
{$title|upper} <!-- Перетворити у верхній регістр -->
{$content|lower} <!-- Перетворити у нижній регістр -->
{$html|striptag} <!-- Видалити HTML-теги -->
{$text|escape} <!-- Екранувати HTML -->
{$multiline|nl2br} <!-- Перетворити нові рядки на <br> -->
{$html|br2nl} <!-- Перетворити теги <br> на нові рядки -->
{$description|trim} <!-- Обрізати пробіли -->
{$subject|title} <!-- Перетворити у title case -->
Команди змінних
{$title|default=Default Title} <!-- Встановити значення за замовчуванням -->
{$name|concat= (Admin)} <!-- Об'єднати текст -->
Команди змінних
{$content|striptag|trim|escape} <!-- Ланцюжок кількох фільтрів -->
Коментарі
Коментарі шаблонів повністю видаляються з виводу та не з'являються в остаточному HTML:
{* Це однорядковий коментар шаблону *}
{*
Це багаторядковий
коментар шаблону
що охоплює кілька рядків
*}
<h1>{$title}</h1>
{* Коментар для налагодження: перевірка, чи працює змінна title *}
<p>{$content}</p>
Примітка: Коментарі шаблонів {* ... *} відрізняються від HTML-коментарів <!-- ... -->. Коментарі шаблонів видаляються під час обробки та ніколи не досягають браузера.
Приклад структури проекту
project/
├── source/
│ ├── layouts/
│ │ └── default.php
│ ├── components/
│ │ ├── header.php
│ │ └── footer.php
│ ├── css/
│ │ ├── bootstrap.min.css
│ │ └── custom.css
│ ├── js/
│ │ ├── app.js
│ │ └── bootstrap.min.js
│ └── homepage.php
├── public/
│ └── assets/ # Згенеровані активи
│ ├── css/
│ └── js/
└── vendor/Awesome-plugins/easy_query
EasyQuery
knifelemon/easy-query — це легкий, плавний конструктор SQL-запитів, який генерує SQL та параметри для підготовлених виразів. Працює з SimplePdo.
Особливості
- 🔗 Плавний API - Ланцюжок методів для зрозумілого конструювання запитів
- 🛡️ Захист від SQL-ін'єкцій - Автоматичне прив'язування параметрів з підготовленими виразами
- 🔧 Підтримка сирого SQL - Вставка сирих SQL-виразів з
raw() - 📝 Різні типи запитів - SELECT, INSERT, UPDATE, DELETE, COUNT
- 🔀 Підтримка JOIN - INNER, LEFT, RIGHT з'єднання з псевдонімами
- 🎯 Розширені умови - LIKE, IN, NOT IN, BETWEEN, оператори порівняння
- 🌐 Незалежність від бази даних - Повертає SQL + параметри, використовуйте з будь-яким з'єднанням БД
- 🪶 Легкий - Мінімальний відбиток з нульовими залежностями
Встановлення
composer require knifelemon/easy-query
Швидкий старт
use KnifeLemon\EasyQuery\Builder;
$q = Builder::table('users')
->select(['id', 'name', 'email'])
->where(['status' => 'active'])
->orderBy('created_at DESC')
->limit(10)
->build();
// Використання з SimplePdo Flight
$users = Flight::db()->fetchAll($q['sql'], $q['params']);
Розуміння build()
Метод build() повертає масив з sql та params. Це розділення забезпечує безпеку вашої бази даних за допомогою підготовлених виразів.
$q = Builder::table('users')
->where(['email' => 'user@example.com'])
->build();
// Повертає:
// [
// 'sql' => 'SELECT * FROM users WHERE email = ?',
// 'params' => ['user@example.com']
// ]
Типи запитів
SELECT
// Вибір всіх стовпців
$q = Builder::table('users')->build();
// SELECT * FROM users
// Вибір конкретних стовпців
$q = Builder::table('users')
->select(['id', 'name', 'email'])
->build();
// SELECT id, name, email FROM users
// З псевдонімом таблиці
$q = Builder::table('users')
->alias('u')
->select(['u.id', 'u.name'])
->build();
// SELECT u.id, u.name FROM users AS u
INSERT
$q = Builder::table('users')
->insert([
'name' => 'John Doe',
'email' => 'john@example.com',
'status' => 'active'
])
->build();
// INSERT INTO users SET name = ?, email = ?, status = ?
Flight::db()->runQuery($q['sql'], $q['params']);
$userId = Flight::db()->lastInsertId();
UPDATE
$q = Builder::table('users')
->update(['status' => 'inactive', 'updated_at' => date('Y-m-d H:i:s')])
->where(['id' => 123])
->build();
// UPDATE users SET status = ?, updated_at = ? WHERE id = ?
Flight::db()->runQuery($q['sql'], $q['params']);
DELETE
$q = Builder::table('users')
->delete()
->where(['id' => 123])
->build();
// DELETE FROM users WHERE id = ?
Flight::db()->runQuery($q['sql'], $q['params']);
COUNT
$q = Builder::table('users')
->count()
->where(['status' => 'active'])
->build();
// SELECT COUNT(*) AS cnt FROM users WHERE status = ?
$count = Flight::db()->fetchField($q['sql'], $q['params']);
Умови WHERE
Проста рівність
$q = Builder::table('users')
->where(['id' => 123, 'status' => 'active'])
->build();
// WHERE id = ? AND status = ?
Оператори порівняння
$q = Builder::table('users')
->where([
'age' => ['>=', 18],
'score' => ['<', 100],
'name' => ['!=', 'admin']
])
->build();
// WHERE age >= ? AND score < ? AND name != ?
LIKE
$q = Builder::table('users')
->where(['name' => ['LIKE', '%john%']])
->build();
// WHERE name LIKE ?
IN / NOT IN
// IN
$q = Builder::table('users')
->where(['id' => ['IN', [1, 2, 3, 4, 5]]])
->build();
// WHERE id IN (?, ?, ?, ?, ?)
// NOT IN
$q = Builder::table('users')
->where(['status' => ['NOT IN', ['banned', 'deleted']]])
->build();
// WHERE status NOT IN (?, ?)
BETWEEN
$q = Builder::table('products')
->where(['price' => ['BETWEEN', [100, 500]]])
->build();
// WHERE price BETWEEN ? AND ?
Умови OR
Використовуйте orWhere() для додавання умов OR у групі:
$q = Builder::table('users')
->where(['status' => 'active'])
->orWhere([
'role' => 'admin',
'permissions' => ['LIKE', '%manage%']
])
->build();
// WHERE status = ? AND (role = ? OR permissions LIKE ?)
JOIN
INNER JOIN
$q = Builder::table('users')
->alias('u')
->select(['u.id', 'u.name', 'p.title'])
->innerJoin('posts', 'u.id = p.user_id', 'p')
->build();
// SELECT u.id, u.name, p.title FROM users AS u INNER JOIN posts AS p ON u.id = p.user_id
LEFT JOIN
$q = Builder::table('users')
->alias('u')
->select(['u.name', 'o.total'])
->leftJoin('orders', 'u.id = o.user_id', 'o')
->build();
// ... LEFT JOIN orders AS o ON u.id = o.user_id
Кілька JOIN
$q = Builder::table('orders')
->alias('o')
->select(['o.id', 'u.name AS customer', 'p.title AS product'])
->innerJoin('users', 'o.user_id = u.id', 'u')
->leftJoin('order_items', 'o.id = oi.order_id', 'oi')
->leftJoin('products', 'oi.product_id = p.id', 'p')
->where(['o.status' => 'completed'])
->build();
Сортування, групування та обмеження
ORDER BY
$q = Builder::table('users')
->orderBy('created_at DESC')
->build();
// ORDER BY created_at DESC
GROUP BY
$q = Builder::table('orders')
->select(['user_id', 'COUNT(*) as order_count'])
->groupBy('user_id')
->build();
// SELECT user_id, COUNT(*) as order_count FROM orders GROUP BY user_id
LIMIT та OFFSET
$q = Builder::table('users')
->limit(10)
->build();
// LIMIT 10
$q = Builder::table('users')
->limit(10, 20) // limit, offset
->build();
// LIMIT 10 OFFSET 20
Сирі SQL-вирази
Використовуйте raw() коли потрібні SQL-функції або вирази, які не повинні трактуватися як прив'язані параметри.
Базовий raw
$q = Builder::table('users')
->update([
'login_count' => Builder::raw('login_count + 1'),
'updated_at' => Builder::raw('NOW()')
])
->where(['id' => 123])
->build();
// SET login_count = login_count + 1, updated_at = NOW()
Raw з прив'язаними параметрами
$q = Builder::table('orders')
->update([
'total' => Builder::raw('COALESCE(subtotal, ?) + ?', [0, 10])
])
->where(['id' => 1])
->build();
// SET total = COALESCE(subtotal, ?) + ?
// params: [0, 10, 1]
Raw у WHERE (підзапит)
$q = Builder::table('products')
->where([
'price' => ['>', Builder::raw('(SELECT AVG(price) FROM products)')]
])
->build();
// WHERE price > (SELECT AVG(price) FROM products)
Безпечні ідентифікатори для введення користувача
Коли назви стовпців надходять від користувача, використовуйте safeIdentifier() для запобігання SQL-ін'єкціям:
$sortColumn = $_GET['sort']; // наприклад, 'created_at'
$safeColumn = Builder::safeIdentifier($sortColumn);
$q = Builder::table('users')
->orderBy($safeColumn . ' DESC')
->build();
// Якщо користувач намагається: "name; DROP TABLE users--"
// Кидає InvalidArgumentException
rawSafe для стовпців від користувача
$userColumn = $_GET['aggregate_column'];
$q = Builder::table('orders')
->select([
Builder::rawSafe('SUM({col})', ['col' => $userColumn])->value . ' AS total'
])
->build();
// Валідує назву стовпця, кидає виняток якщо невалідно
Попередження: Ніколи не конкатенуйте введення користувача безпосередньо в
raw(). Завжди використовуйте прив'язані параметри абоsafeIdentifier().
Повторне використання конструктора запитів
Методи очищення
Очищайте конкретні частини для повторного використання конструктора:
$query = Builder::table('users')
->select(['id', 'name'])
->where(['status' => 'active'])
->orderBy('created_at DESC');
// Перший запит
$q1 = $query->limit(10)->build();
// Очистити та повторно використати
$query->clearWhere()->clearLimit();
// Другий запит з іншими умовами
$q2 = $query
->where(['status' => 'pending'])
->limit(5)
->build();
Доступні методи очищення
| Метод | Опис |
|---|---|
clearWhere() |
Очистити умови WHERE та параметри |
clearSelect() |
Скинути стовпці SELECT до за замовчуванням '*' |
clearJoin() |
Очистити всі клаузули JOIN |
clearGroupBy() |
Очистити клаузу GROUP BY |
clearOrderBy() |
Очистити клаузу ORDER BY |
clearLimit() |
Очистити LIMIT та OFFSET |
clearAll() |
Скинути конструктор до початкового стану |
Приклад пагінації
$baseQuery = Builder::table('users')
->select(['id', 'name', 'email'])
->where(['status' => 'active'])
->orderBy('created_at DESC');
// Отримати загальну кількість
$countQuery = clone $baseQuery;
$countResult = $countQuery->clearSelect()->count()->build();
$total = Flight::db()->fetchField($countResult['sql'], $countResult['params']);
// Отримати пагинаційні результати
$page = 1;
$perPage = 20;
$listResult = $baseQuery->limit($perPage, ($page - 1) * $perPage)->build();
$users = Flight::db()->fetchAll($listResult['sql'], $listResult['params']);
Динамічне конструювання запитів
$query = Builder::table('products')->alias('p');
if (!empty($categoryId)) {
$query->where(['p.category_id' => $categoryId]);
}
if (!empty($minPrice)) {
$query->where(['p.price' => ['>=', $minPrice]]);
}
if (!empty($maxPrice)) {
$query->where(['p.price' => ['<=', $maxPrice]]);
}
if (!empty($searchTerm)) {
$query->where(['p.name' => ['LIKE', "%{$searchTerm}%"]]);
}
$result = $query->orderBy('p.created_at DESC')->limit(20)->build();
$products = Flight::db()->fetchAll($result['sql'], $result['params']);
Повний приклад FlightPHP
use KnifeLemon\EasyQuery\Builder;
// Список користувачів з пагінацією
Flight::route('GET /users', function() {
$page = (int) (Flight::request()->query['page'] ?? 1);
$perPage = 20;
$q = Builder::table('users')
->select(['id', 'name', 'email', 'created_at'])
->where(['status' => 'active'])
->orderBy('created_at DESC')
->limit($perPage, ($page - 1) * $perPage)
->build();
$users = Flight::db()->fetchAll($q['sql'], $q['params']);
Flight::json(['users' => $users, 'page' => $page]);
});
// Створення користувача
Flight::route('POST /users', function() {
$data = Flight::request()->data;
$q = Builder::table('users')
->insert([
'name' => $data->name,
'email' => $data->email,
'created_at' => Builder::raw('NOW()')
])
->build();
Flight::db()->runQuery($q['sql'], $q['params']);
Flight::json(['id' => Flight::db()->lastInsertId()]);
});
// Оновлення користувача
Flight::route('PUT /users/@id', function($id) {
$data = Flight::request()->data;
$q = Builder::table('users')
->update([
'name' => $data->name,
'email' => $data->email,
'updated_at' => Builder::raw('NOW()')
])
->where(['id' => $id])
->build();
Flight::db()->runQuery($q['sql'], $q['params']);
Flight::json(['success' => true]);
});
// Видалення користувача
Flight::route('DELETE /users/@id', function($id) {
$q = Builder::table('users')
->delete()
->where(['id' => $id])
->build();
Flight::db()->runQuery($q['sql'], $q['params']);
Flight::json(['success' => true]);
});
Довідник API
Статичні методи
| Метод | Опис |
|---|---|
Builder::table(string $table) |
Створити новий екземпляр конструктора для таблиці |
Builder::raw(string $sql, array $bindings = []) |
Створити сирий SQL-вираз |
Builder::rawSafe(string $expr, array $identifiers, array $bindings = []) |
Сирий вираз з безпечною заміною ідентифікаторів |
Builder::safeIdentifier(string $identifier) |
Валідувати та повернути безпечну назву стовпця/таблиці |
Методи екземпляра
| Метод | Опис |
|---|---|
alias(string $alias) |
Встановити псевдонім таблиці |
select(string\|array $columns) |
Встановити стовпці для вибору (за замовчуванням: '*') |
where(array $conditions) |
Додати умови WHERE (AND) |
orWhere(array $conditions) |
Додати умови OR WHERE |
join(string $table, string $condition, string $alias, string $type) |
Додати клаузу JOIN |
innerJoin(string $table, string $condition, string $alias) |
Додати INNER JOIN |
leftJoin(string $table, string $condition, string $alias) |
Додати LEFT JOIN |
groupBy(string $groupBy) |
Додати клаузу GROUP BY |
orderBy(string $orderBy) |
Додати клаузу ORDER BY |
limit(int $limit, int $offset = 0) |
Додати LIMIT та OFFSET |
count(string $column = '*') |
Встановити запит на COUNT |
insert(array $data) |
Встановити запит на INSERT |
update(array $data) |
Встановити запит на UPDATE |
delete() |
Встановити запит на DELETE |
build() |
Збудувати та повернути ['sql' => ..., 'params' => ...] |
get() |
Псевдонім для build() |
Інтеграція з Tracy Debugger
EasyQuery автоматично інтегрується з Tracy Debugger, якщо встановлено. Ніякого налаштування не потрібно!
composer require tracy/tracy
use Tracy\Debugger;
Debugger::enable();
// Всі запити автоматично логуються в панель Tracy
$q = Builder::table('users')->where(['status' => 'active'])->build();
Панель Tracy показує:
- Загальну кількість запитів та розбивку за типами
- Згенерований SQL (з підсвічуванням синтаксису)
- Масив параметрів
- Деталі запиту (таблиця, where, joins тощо)
Для повної документації відвідайте репозиторій GitHub.
Awesome-plugins/twig
Twig
Twig — це гнучкий, швидкий і безпечний шаблонізатор для PHP. Це мова шаблонів, яку використовує Symfony та багато інших проєктів, а це означає, що інструменти ШІ для кодування та більшість PHP-розробників вже добре знайомі з її синтаксисом. Twig компілює шаблони в оптимізований PHP, автоматично екранує вивід за замовчуванням (відмінно для захисту від XSS) і легко розширюється за допомогою фільтрів, функцій та розширень.
Встановлення
Встановіть за допомогою composer.
composer require twig/twig
Базове налаштування
Є кілька базових опцій налаштування для початку роботи. Ви можете дізнатися більше про них у Документації Twig.
require 'vendor/autoload.php';
$app = Flight::app();
$app->map('render', function(string $template, array $data): void {
$loader = new \Twig\Loader\FilesystemLoader(Flight::get('flight.views.path'));
$twig = new \Twig\Environment($loader, [
// Де Twig зберігає скомпільовані шаблони
'cache' => __DIR__ . '/../cache/twig',
// Перекомпілювати шаблони при зміні вихідного коду (зручно під час розробки)
'auto_reload' => true,
]);
echo $twig->render($template, $data);
});
Реєстрація Twig як класу View
Якщо ви хочете повторно використовувати одне середовище Twig (рекомендовано для production), зареєструйте його і направте render на нього:
require 'vendor/autoload.php';
$app = Flight::app();
$app->register('view', \Twig\Environment::class, [
new \Twig\Loader\FilesystemLoader($app->get('flight.views.path')),
[
'cache' => __DIR__ . '/../cache/twig',
'auto_reload' => true,
],
]);
$app->map('render', function(string $template, array $data): void {
echo Flight::view()->render($template, $data);
});
Приклад простого макета
Ось простий приклад файлу макета. Це файл, який буде використовуватися для обгортання всіх ваших інших представлень.
{# app/views/layout.twig #}
<!doctype html>
<html lang="en">
<head>
<title>{% if title %}{{ title }} - {% endif %}My App</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<nav>
{# ваші елементи навігації тут #}
</nav>
</header>
<div id="content">
{# Це і є та сама магія #}
{% block content %}{% endblock %}
</div>
<div id="footer">
© Copyright
</div>
</body>
</html>
А тепер у нас є ваш файл, який буде рендеритися всередині блоку content:
{# app/views/home.twig #}
{# Це повідомляє Twig, що цей файл "всередині" файлу layout.twig #}
{% extends 'layout.twig' %}
{# Це вміст, який буде рендеритися всередині макета в блоці content #}
{% block content %}
<h1>Home Page</h1>
<p>Welcome to my app!</p>
{% endblock %}
Потім, коли ви будете рендерити це у вашій функції чи контролері, ви зробите щось на кшталт цього:
// простий маршрут
Flight::route('/', function () {
Flight::render('home.twig', [
'title' => 'Home Page'
]);
});
// або якщо ви використовуєте контролер
Flight::route('/', [HomeController::class, 'index']);
// HomeController.php
class HomeController
{
public function index()
{
Flight::render('home.twig', [
'title' => 'Home Page'
]);
}
}
Перегляньте Документацію Twig для отримання додаткової інформації про те, як використовувати Twig на повну потужність!
Налагодження
Twig поставляється з Розширенням для налагодження, яке додає функцію dump(), яку ви можете використовувати всередині шаблонів. Увімкніть його лише під час розробки:
$app->register('view', \Twig\Environment::class, [
new \Twig\Loader\FilesystemLoader($app->get('flight.views.path')),
[
'cache' => __DIR__ . '/../cache/twig',
'debug' => true, // обов'язково для функції dump()
'auto_reload' => true,
],
], function (\Twig\Environment $twig): void {
$twig->addExtension(new \Twig\Extension\DebugExtension());
});
Потім у шаблоні:
{{ dump(user) }}
Ви також можете поєднувати Twig з Tracy для налагодження на рівні PHP. Для метрик на рівні шаблону (час рендерингу, пам'ять, які шаблони/блоки виконувалися), використовуйте опціональну панель Twig у flightphp/tracy-extensions: передайте Twig\Profiler\Profile як twig_profile до TracyExtensionLoader. Опціональне TwigTracyExtension надає {{ dump() }} / {{ bdump() }} / {{ dumpe() }} у шаблонах, коли Tracy увімкнено.
Застереження щодо безпеки
Twig автоматично екранує вивід за замовчуванням, що допомагає захистити від XSS-атак. Віддавайте перевагу {{ variable }} для тексту. Використовуйте фільтр |raw лише тоді, коли ви свідомо довіряєте HTML-вмісту (наприклад, очищеному markdown, який ви вже обробили на стороні сервера).
Awesome-plugins/session
FlightPHP Сесія - Легкий Обробник Сесій На Основі Файлів
Це легкий, на основі файлів, обробник сесій для Flight PHP Framework. Він надає просте, але потужне рішення для керування сесіями, з функціями, такими як неблокувальне читання сесій, необов'язкове шифрування, функція автофіксації та режим тестування для розробки. Дані сесій зберігаються у файлах, що робить його ідеальним для застосунків, які не потребують бази даних.
Якщо ви хочете використовувати базу даних, перегляньте ghostff/session плагін, який має багато з цих самих функцій, але з бекендом бази даних.
Відвідайте Github repository для повного вихідного коду та деталей.
Встановлення
Встановіть плагін через Composer:
composer require flightphp/session
Основне Використання
Ось простий приклад, як використовувати flightphp/session плагін у вашому застосунку Flight:
require 'vendor/autoload.php';
use flight\Session;
$app = Flight::app();
// Зареєструвати службу сесії
$app->register('session', Session::class);
// Приклад маршруту з використанням сесії
Flight::route('/login', function() {
$session = Flight::session();
$session->set('user_id', 123);
$session->set('username', 'johndoe');
$session->set('is_admin', false);
echo $session->get('username'); // Виводить: johndoe
echo $session->get('preferences', 'default_theme'); // Виводить: default_theme
if ($session->get('user_id')) {
Flight::json(['message' => 'User is logged in!', 'user_id' => $session->get('user_id')]);
}
});
Flight::route('/logout', function() {
$session = Flight::session();
$session->clear(); // Очистити всі дані сесії
Flight::json(['message' => 'Logged out successfully']);
});
Flight::start();
Ключові Пункти
- Неблокувальне: Використовує
read_and_closeдля початку сесії за замовчуванням, запобігаючи проблемам з блокуванням сесій. - Автофіксація: Увімкнено за замовчуванням, тому зміни зберігаються автоматично при завершенні, якщо не вимкнено.
- Зберігання У Файлах: Сесії зберігаються у директорії тимчасових файлів системи під
/flight_sessionsза замовчуванням.
Конфігурація
Ви можете налаштувати обробник сесій, передаючи масив опцій під час реєстрації:
// Так, це подвійний масив :)
$app->register('session', Session::class, [ [
'save_path' => '/custom/path/to/sessions', // Директорія для файлів сесій
'prefix' => 'myapp_', // Префікс для файлів сесій
'encryption_key' => 'a-secure-32-byte-key-here', // Увімкнути шифрування (рекомендовано 32 байти для AES-256-CBC)
'auto_commit' => false, // Вимкнути автофіксацію для ручного керування
'start_session' => true, // Починати сесію автоматично (за замовчуванням: true)
'test_mode' => false, // Увімкнути режим тестування для розробки
'serialization' => 'json', // Метод серіалізації: 'json' (за замовчуванням) або 'php' (спадковий)
] ]);
Опції Конфігурації
| Опція | Опис | Значення За Замовчуванням |
|---|---|---|
save_path |
Директорія, де зберігаються файли сесій | sys_get_temp_dir() . '/flight_sessions' |
prefix |
Префікс для збереженого файлу сесії | sess_ |
encryption_key |
Ключ для шифрування AES-256-CBC (необов'язково) | null (без шифрування) |
auto_commit |
Автоматичне збереження даних сесії при завершенні | true |
start_session |
Починати сесію автоматично | true |
test_mode |
Запуск у режимі тестування без впливу на сесії PHP | false |
test_session_id |
Власний ідентифікатор сесії для режиму тестування (необов'язково) | Випадково згенерований, якщо не встановлено |
serialization |
Метод серіалізації: 'json' (за замовчуванням, безпечно) або 'php' (спадковий, дозволяє об'єкти) | 'json' |
Режими Серіалізації
За замовчуванням, ця бібліотека використовує JSON-серіалізацію для даних сесій, що є безпечним і запобігає вразливостям ін'єкції об'єктів PHP. Якщо вам потрібно зберігати об'єкти PHP у сесії (не рекомендується для більшості застосунків), ви можете обрати спадкову PHP-серіалізацію:
'serialization' => 'json'(за замовчуванням):- Дозволяються лише масиви та примітиви в даних сесії.
- Безпечніше: імунне до ін'єкції об'єктів PHP.
- Файли мають префікс
J(звичайний JSON) абоF(зашифрований JSON).
'serialization' => 'php':- Дозволяє зберігати об'єкти PHP (використовуйте з обережністю).
- Файли мають префікс
P(звичайна PHP-серіалізація) абоE(зашифрована PHP-серіалізація).
Примітка: Якщо ви використовуєте JSON-серіалізацію, спроба зберегти об'єкт викличе виняток.
Додаткове Використання
Ручна Фіксація
Якщо ви вимкнете автофіксацію, ви повинні вручну зафіксувати зміни:
$app->register('session', Session::class, ['auto_commit' => false]);
Flight::route('/update', function() {
$session = Flight::session();
$session->set('key', 'value');
$session->commit(); // Явно зберегти зміни
});
Безпека Сесій З Шифруванням
Увімкніть шифрування для чутливих даних:
$app->register('session', Session::class, [
'encryption_key' => 'your-32-byte-secret-key-here'
]);
Flight::route('/secure', function() {
$session = Flight::session();
$session->set('credit_card', '4111-1111-1111-1111'); // Шифрується автоматично
echo $session->get('credit_card'); // Розшифровується при отриманні
});
Регенерація Сесії
Регенеруйте ідентифікатор сесії для безпеки (наприклад, після входу):
Flight::route('/post-login', function() {
$session = Flight::session();
$session->regenerate(); // Новий ідентифікатор, зберегти дані
// АБО
$session->regenerate(true); // Новий ідентифікатор, видалити старі дані
});
Приклад Middleware
Захистіть маршрути з використанням автентифікації на основі сесії:
Flight::route('/admin', function() {
Flight::json(['message' => 'Welcome to the admin panel']);
})->addMiddleware(function() {
$session = Flight::session();
if (!$session->get('is_admin')) {
Flight::halt(403, 'Access denied');
}
});
Це просто приклад використання в middleware. Для більш детального прикладу, перегляньте middleware документацію.
Методи
Клас Session надає ці методи:
set(string $key, $value): Зберігає значення в сесії.get(string $key, $default = null): Отримує значення, з необов'язковим значення за замовчуванням, якщо ключ не існує.delete(string $key): Видаляє конкретний ключ з сесії.clear(): Видаляє всі дані сесії, але зберігає ту саму назву файлу для сесії.commit(): Зберігає поточні дані сесії у файлову систему.id(): Повертає поточний ідентифікатор сесії.regenerate(bool $deleteOldFile = false): Регенерує ідентифікатор сесії, включаючи створення нового файлу сесії, зберігаючи всі старі дані, а старий файл залишається в системі. Якщо$deleteOldFileдорівнюєtrue, старий файл сесії видаляється.destroy(string $id): Руйнує сесію за ідентифікатором і видаляє файл сесії з системи. Це частинаSessionHandlerInterfaceі$idє обов'язковим. Типове використання буде$session->destroy($session->id()).getAll(): Повертає всі дані з поточної сесії.
Усі методи, крім get() і id(), повертають екземпляр Session для ланцюжка.
Чому Використовувати Цей Плагін?
- Легкий: Без зовнішніх залежностей — лише файли.
- Неблокувальне: Уникає блокування сесій з
read_and_closeза замовчуванням. - Безпечний: Підтримує шифрування AES-256-CBC для чутливих даних.
- Гнучкий: Опції автофіксації, режим тестування та ручного керування.
- Flight-Native: Створено спеціально для фреймворку Flight.
Технічні Деталі
- Формат Зберігання: Файли сесій мають префікс
sess_і зберігаються у налаштованійsave_path. Префікси вмісту файлів:J: Звичайний JSON (за замовчуванням, без шифрування)F: Зашифрований JSON (за замовчуванням з шифруванням)P: Звичайна PHP-серіалізація (спадкова, без шифрування)E: Зашифрована PHP-серіалізація (спадкова з шифруванням)
- Шифрування: Використовує AES-256-CBC з випадковим IV для кожного запису сесії, коли надано
encryption_key. Шифрування працює для обох режимів серіалізації JSON і PHP. - Серіалізація: JSON є за замовчуванням і найбезпечнішим методом. PHP-серіалізація доступна для спадкового/просунутого використання, але менш безпечна.
- Збір Сміття: Реалізує
SessionHandlerInterface::gc()для очищення прострочених сесій.
Співпраця
Внески вітаються! Форкуйте repository, внесіть зміни та надішліть pull request. Повідомте про помилки або запропонуйте функції через трекер проблем Github.
Ліцензія
Цей плагін ліцензований під MIT License. Перегляньте Github repository для деталей.
Awesome-plugins/runway
Runway
Runway — це CLI-додаток, який допомагає керувати вашими Flight додатками. Він може генерувати контролери, відображати всі маршрути, запускати AI-помічники налаштування, міграції (у скелеті) та інше. Він базується на чудовій бібліотеці adhocore/php-cli.
Натисніть тут, щоб переглянути код.
Команди скафолдингу навмисно узгоджені з офіційним скелетом, щоб AI-інструменти кодування та люди отримували однакові шляхи, простори імен та стиль конструктор-ін'єкції кожного разу.
Встановлення
Встановіть за допомогою composer.
composer require flightphp/runway
Скелет вже залежить від Runway; використовуйте php runway з кореня проекту.
Базова конфігурація
При першому запуску Runway спробує знайти конфігурацію runway у app/config/config.php через ключ 'runway'.
<?php
// app/config/config.php
return [
'runway' => [
'app_root' => 'app/',
'public_root' => 'public/',
// optional; skeleton also uses index_root for the public entry
'index_root' => 'public/index.php',
],
];
ПРИМІТКА - Станом на v1.2.0,
.runway-config.jsonзастарів на користьapp/config/config.php. Міграціюйте за допомогоюphp runway config:migrateпри оновленні старих проектів. Скелет все ще може записувати невеликий.runway-config.jsonпри create-project для сумісності; віддавайте перевагу ключуrunwayуconfig.phpнадалі.
Визначення кореня проекту
Runway достатньо розумний, щоб визначити корінь вашого проекту, навіть якщо ви запускаєте його з підкаталогу. Він шукає індикатори типу composer.json, .git або app/config/config.php, щоб визначити, де знаходиться корінь проекту. Це означає, що ви можете запускати команди Runway з будь-якого місця у вашому проекті!
Використання
Runway має ряд команд, які ви можете використовувати для керування вашим Flight додатком. Є два простих способи використання Runway.
- Якщо ви використовуєте проект-скелет, ви можете запустити
php runway [команда]з кореня вашого проекту. - Якщо ви використовуєте Runway як пакет, встановлений через composer, ви можете запустити
vendor/bin/runway [команда]з кореня вашого проекту.
Список команд
Ви можете переглянути список усіх доступних команд, виконавши команду php runway.
php runway
Покладайтеся лише на команди, які дійсно з'являються у цьому списку для вашої установки (основні команди Runway проти специфічних для проекту, таких як migrate скелета).
Довідка по команді
Для будь-якої команди ви можете передати прапорець --help, щоб отримати більше інформації про те, як використовувати команду.
php runway routes --help
php runway make:controller --help
Ось кілька прикладів:
Генерація контролера
make:controller створює скафолдинг контролера, який відповідає макету офіційного скелета:
| Шлях | app/Controller/{Name}.php |
| Простір імен | App\Controller |
| Стиль | Ін'єкція конструктора flight\Engine (без Flight:: у тілі класу) |
php runway make:controller MyController
# → app/Controller/MyController.php
# namespace App\Controller;
Приклад форми, яку ви повинні очікувати (спрощено):
<?php
declare(strict_types=1);
namespace App\Controller;
use flight\Engine;
class MyController
{
protected Engine $app;
public function __construct(Engine $app)
{
$this->app = $app;
}
public function index(): void
{
// e.g. $this->app->render('…', […]);
}
}
Зареєструйте його з class callable, щоб Dice міг побудувати контролер:
// app/config/routes.php
use App\Controller\MyController;
$router->get('/mine', [MyController::class, 'index']);
Чому саме такий макет? Регістр папки повинен відповідати простору імен (Controller не controllers) для Composer PSR-4 на Linux—див. Автозавантаження. Той самий шлях — це те, що кореневі та scoped файли AGENTS.md вказують AI-інструментам використовувати, тому згенеровані та написані вручну контролери залишаються ідентичними.
Старіша документація та спільнотні проекти іноді використовували
app/controllers/таapp\controllers. Це залишається дійсним, якщо ваше дерево все ще використовує папки з малими літерами. Нові проекти скелета та поточний вивідmake:controllerвикористовуютьapp/Controller/+App\Controller.
Генерація моделі Active Record
Спочатку переконайтеся, що ви встановили плагін Active Record.
php runway make:record users
В офіційному скелеті моделі живуть під app/Model/ з простором імен App\Model, а з'єднання з БД — це SimplePdo (ін'єктуйте його або передайте у конструктор ActiveRecord). Назви файлів/просторів імен генеруються відповідно до поточних налаштувань Runway та вашої конфігурації runway—віддавайте перевагу узгодженню нових моделей з App\Model, щоб вони відповідали автозавантаженню та AGENTS.md.
Приклад моделі, узгодженої з демо постів скелета:
<?php
declare(strict_types=1);
namespace App\Model;
use flight\ActiveRecord;
/**
* @property int $id
* @property string $title
* // …
*/
class Post extends ActiveRecord
{
protected array $relations = [];
public function __construct($databaseConnection)
{
parent::__construct($databaseConnection, 'posts');
}
}
Якщо старіший генератор все ще видає app/records / app\records, ви можете зберегти цю конвенцію у застарілих додатках або перемістити файли у app/Model/ та оновити простір імен відповідно до регістру папки.
Міграції (скелет)
Офіційний скелет постачає проектну команду (виявлену з app/commands/), таку як:
php runway migrate
Міграції — це SQL-файли під migrations/ (наприклад YYYYMMDDHHMMSS_description.sql для SQLite та …_description.mysql.sql для MySQL), вибрані з конфігурації драйвера бази даних / env. Точні прапорці та поведінка визначаються цією проектною командою—запустіть php runway migrate --help у вашому додатку.
AI-помічники
Runway надає AI-орієнтовані команди, що використовуються з AI та досвідом розробника:
php runway ai:init
php runway ai:generate-instructions
Вони зберігають облікові дані LLM та генерують інструкції проекту (в основному AGENTS.md). На скелеті, розглядайте AGENTS.md (та scoped копії під app/) плюс SECURITY.md як джерело правди для агентів.
Відображення всіх маршрутів
Це відобразить усі маршрути, які зараз зареєстровані у Flight.
php runway routes
Якщо ви хочете переглядати лише специфічні маршрути, ви можете передати прапорець для фільтрації маршрутів.
# Відобразити лише GET маршрути
php runway routes --get
# Відобразити лише POST маршрути
php runway routes --post
# тощо.
Додавання власних команд до Runway
Якщо ви створюєте пакет для Flight або хочете додати власні команди у ваш проект, ви можете зробити це, створивши директорію src/commands/, flight/commands/, app/commands/ або commands/ для вашого проекту/пакету. Якщо вам потрібна подальша кастомізація, дивіться розділ нижче про Конфігурацію.
У скелеті проектні команди живуть у app/commands/ з простором імен App\Command. Runway виявляє їх за шляхом; тримайте цю папку синхронізованою з Composer classmap/PSR-4, як це вже робить ваш проект.
Щоб створити команду, просто розширьте клас AbstractBaseCommand та реалізуйте щонайменше метод __construct та метод execute.
<?php
declare(strict_types=1);
namespace App\Command;
use flight\commands\AbstractBaseCommand;
class ExampleCommand extends AbstractBaseCommand
{
/**
* Конструктор
*
* @param array<string,mixed> $config Конфігурація з app/config/config.php
*/
public function __construct(array $config)
{
parent::__construct('make:example', 'Створити приклад для документації', $config);
$this->argument('<funny-gif>', 'Назва кумедного gif');
}
/**
* Виконує функцію
*
* @return void
*/
public function execute()
{
$io = $this->app()->io();
$io->info('Створення прикладу...');
// Зробіть щось тут
$io->ok('Приклад створено!');
}
}
Дивіться Документацію adhocore/php-cli для отримання додаткової інформації про те, як побудувати власні команди у ваш Flight додаток!
Управління конфігурацією
Оскільки конфігурація перемістилася до app/config/config.php станом на v1.2.0, є кілька допоміжних команд для управління конфігурацією.
Порада скелета: Тримайте
config.phpяк літеральні PHP-значення. Секрети належать у.env. Уникайте виразів$_ENV[...]всерединіconfig.php—config:setперезаписує цей файл як статичні дані і може вбудувати секрети у файл. Див. Конфігурація.
Міграція старої конфігурації
Якщо у вас є старий файл .runway-config.json, ви можете легко мігрувати його до app/config/config.php за допомогою наступної команды:
php runway config:migrate
Встановлення значення конфігурації
Ви можете встановити значення конфігурації за допомогою команди config:set. Це корисно, якщо ви хочете оновити значення конфігурації без відкриття файлу.
php runway config:set app_root "app/"
Отримання значення конфігурації
Ви можете отримати значення конфігурації за допомогою команди config:get.
php runway config:get app_root
Усі конфігурації Runway
Якщо вам потрібно кастомізувати конфігурацію для Runway, ви можете встановити ці значення у app/config/config.php. Нижче наведено деякі додаткові конфігурації, які ви можете встановити:
<?php
// app/config/config.php
return [
// ... інші значення конфігурації ...
'runway' => [
// Це місце, де знаходиться директорія вашого додатку
'app_root' => 'app/',
// Це директорія, де знаходиться ваш кореневий index файл
'index_root' => 'public/',
// Це шляхи до коренів інших проектів
'root_paths' => [
'/home/user/different-project',
'/var/www/another-project'
],
// Базові шляхи, ймовірно, не потребують конфігурації, але вони тут, якщо ви цього хочете
'base_paths' => [
'/includes/libs/vendor', // якщо у вас дійсно унікальний шлях до вашої vendor директорії чи щось подібне
],
// Фінальні шляхи — це локації в проекті для пошуку файлів команд
'final_paths' => [
'src/diff-path/commands',
'app/module/admin/commands',
],
// Якщо ви хочете просто додати повний шлях, вперед (абсолютний або відносний до кореня проекту)
'paths' => [
'/home/user/different-project/src/diff-path/commands',
'/var/www/another-project/app/module/admin/commands',
'app/my-unique-commands'
]
]
];
Доступ до конфігурації
Якщо вам потрібно ефективно отримати доступ до значень конфігурації, ви можете отримати їх через метод __construct або метод app(). Також важливо зазначити, що якщо у вас є файл app/config/services.php, ці сервіси також будуть доступні для вашої команди.
public function execute()
{
$io = $this->app()->io();
// Доступ до конфігурації
$app_root = $this->config['runway']['app_root'];
// Доступ до сервісів, таких як, можливо, з'єднання з базою даних
$database = $this->config['database']
// ...
}
AI-помічники-обгортки
Runway має деякі допоміжні обгортки, які полегшують AI генерувати команди. Ви можете використовувати addOption та addArgument у спосіб, схожий на Symfony Console. Це корисно, якщо ви використовуєте AI-інструменти для генерації ваших команд.
public function __construct(array $config)
{
parent::__construct('make:example', 'Створити приклад для документації', $config);
// Аргумент mode є nullable і за замовчуванням повністю опціональний
$this->addOption('name', 'Назва прикладу', null);
}
Дивіться також
- Встановлення - Дерево скелета та налаштування create-project
- Автозавантаження -
App\та регістр папки - Впровадження залежностей - Dice + ін'єкція Engine для згенерованих контролерів
- AI та досвід розробника -
ai:init,ai:generate-instructions,AGENTS.md - Active Record - Моделі, що використовуються з
make:record/ скелетApp\Model - SimplePdo - З'єднання з БД, що використовується міграціями та моделями скелета
Awesome-plugins/tracy_extensions
Розширення панелі Tracy для Flight
Це набір розширень, які роблять роботу з Flight ще багатшою.
- Flight - Аналіз усіх змінних Flight.
- Database - Аналіз усіх запитів, які виконувалися на сторінці (якщо ви правильно ініціювали з'єднання з базою даних)
- Request - Аналіз усіх змінних
$_SERVERта перевірка всіх глобальних даних ($_GET,$_POST,$_FILES) - Session - Аналіз усіх змінних
$_SESSION, якщо сесії активні. - Twig (необов'язково) - Аналіз часу рендерингу шаблонів Twig, пам'яті та того, які шаблони/блоки/макроси виконувалися (вимагає
twig/twigта конфігураціюtwig_profile)
Це особливо зручно з офіційним скелетом, який за замовчуванням використовує Twig: той самий макет інструменти AI також чітко відображається на панелі Tracy.
Це панель

І кожна панель відображає дуже корисну інформацію про вашу програму!

Натисніть тут, щоб переглянути код.
Встановлення
Виконайте composer require flightphp/tracy-extensions --dev і вперед!
Twig не є жорсткою залежністю пакета. Встановіть twig/twig тільки якщо вам потрібна панель Twig (скелет вже робить це для представлень).
Конфігурація
Для початку роботи вам потрібно зробити дуже мало конфігурацій. Вам потрібно буде ініціювати налагоджувач Tracy перед використанням цього https://tracy.nette.org/en/guide:
<?php
use Tracy\Debugger;
use flight\debug\tracy\TracyExtensionLoader;
// bootstrap code
require __DIR__ . '/vendor/autoload.php';
Debugger::enable();
// Вам може знадобитися вказати ваше середовище за допомогою Debugger::enable(Debugger::DEVELOPMENT)
// якщо ви використовуєте з'єднання з базою даних у вашій програмі, існує
// необхідна обгортка PDO для використання ТІЛЬКИ В РОЗРОБЦІ (не у продакшені будь ласка!)
// Вона має ті самі параметри, що й звичайне з'єднання PDO
$pdo = new PdoQueryCapture('sqlite:test.db', 'user', 'pass');
// або якщо ви приєднуєте це до фреймворку Flight
Flight::register('db', PdoQueryCapture::class, ['sqlite:test.db', 'user', 'pass']);
// тепер кожного разу, коли ви робите запит, буде фіксуватися час, запит та параметри
// Це з'єднує точки
if(Debugger::$showBar === true) {
// Це повинно бути false, інакше Tracy не зможе рендерити :(
Flight::set('flight.content_length', false);
new TracyExtensionLoader(Flight::app());
}
// більше коду
Flight::start();
Додаткова конфігурація
Дані сесії
Якщо у вас є власний обробник сесій (наприклад, ghostff/session), ви можете передати будь-який масив даних сесії Tracy, і він автоматично виведе його для вас. Ви передаєте його за допомогою ключа session_data у другому параметрі конструктора TracyExtensionLoader.
use Ghostff\Session\Session;
// або використовуйте flight\Session;
require 'vendor/autoload.php';
$app = Flight::app();
$app->register('session', Session::class);
if(Debugger::$showBar === true) {
// Це повинно бути false, інакше Tracy не зможе рендерити :(
Flight::set('flight.content_length', false);
new TracyExtensionLoader(Flight::app(), [ 'session_data' => Flight::session()->getAll() ]);
}
// маршрути та інші речі...
Flight::start();
Панель Twig (необов'язково)
Якщо ваша програма використовує Twig (включаючи офіційний скелет), ви можете показати метрики шаблонів на панелі Tracy. Створіть Twig Profile, приєднайте ProfilerExtension до вашого середовища, потім передайте цей профіль у завантажувач під ключем twig_profile. Приєднуйте профілювання тільки у розробці.
<?php
use flight\debug\tracy\TracyExtensionLoader;
use flight\debug\tracy\TwigTracyExtension;
use Tracy\Debugger;
use Twig\Environment;
use Twig\Extension\ProfilerExtension;
use Twig\Loader\FilesystemLoader;
use Twig\Profiler\Profile;
$loader = new FilesystemLoader(__DIR__ . '/views');
$twig = new Environment($loader, [
'debug' => true,
'cache' => false,
]);
// Необов'язково: експонуйте хелпери дампу Tracy у шаблонах
// {{ dump(var) }}, {{ bdump(var) }}, {{ dumpe(var) }}
$twig->addExtension(new TwigTracyExtension());
$tracyConfig = [];
if (Debugger::$showBar === true) {
$profile = new Profile();
$twig->addExtension(new ProfilerExtension($profile));
$tracyConfig['twig_profile'] = $profile;
}
if (Debugger::$showBar === true) {
Flight::set('flight.content_length', false);
new TracyExtensionLoader(Flight::app(), $tracyConfig);
}
// Зіставлення Flight::render() з Twig (приклад)
Flight::map('render', function (string $template, array $data = []) use ($twig) {
if (substr($template, -5) !== '.twig') {
$template .= '.twig';
}
echo $twig->render($template, $data);
});
Що показує панель
- Загальний час рендерингу Twig та пам'ять
- Кількість викликів шаблонів / блоків / макросів
- Кожен шаблон, який рендерився, з його власним часом та пам'яттю
Вкладка Twig прихована, коли для запиту не рендерилися шаблони, або коли ви опускаєте twig_profile (або не маєте Twig встановленого) — інші панелі Flight продовжують працювати.
У services.php у стилі скелету, створюйте той самий $profile / ProfilerExtension, коли налагодження увімкнено, передайте twig_profile у TracyExtensionLoader, і продовжуйте використовувати ваше спільне середовище Twig для $app->render().
Latte
Для цього розділу потрібен PHP 8.1+.
Якщо у вашому проекті встановлено Latte, Tracy має нативну інтеграцію з Latte для аналізу ваших шаблонів. Ви просто реєструєте розширення з вашим екземпляром Latte (це власний міст Tracy для Latte, а не панель Twig, описана вище).
require 'vendor/autoload.php';
$app = Flight::app();
$app->map('render', function($template, $data, $block = null) {
$latte = new Latte\Engine;
// інші конфігурації...
// додаємо розширення тільки якщо Tracy Debug Bar увімкнено
if(Debugger::$showBar === true) {
// це місце, де ви додаєте панель Latte до Tracy
$latte->addExtension(new Latte\Bridges\Tracy\TracyExtension);
}
$latte->render($template, $data, $block);
});
Дивіться також
- Tracy - Базове налаштування Tracy для Flight
- Twig - Шаблонізація, яку використовує скелет та панель Twig
- Templates - Як Flight зіставляє
renderз Twig/Latte - Installation - Скелет включає tracy-extensions у dev
Awesome-plugins/apm
FlightPHP APM Документація
Ласкаво просимо до FlightPHP APM — вашого особистого тренера з продуктивності для застосунку! Цей посібник — ваш маршрут до налаштування, використання та опанування моніторингу продуктивності застосунків (APM) з FlightPHP. Незалежно від того, чи ви полюєте за повільними запитами, чи просто хочете розібратися в графіках затримок, ми вас покриємо. Давайте зробимо ваш застосунок швидшим, користувачів щасливішими, а сеанси налагодження — легшими!
Перегляньте демо дашборду для сайту Flight Docs.

Чому APM Важливий
Уявіть, що ваш застосунок — це ресторан з великим потоком. Без способу відстежувати, скільки часу займають замовлення чи де кухня гальмує, ви лише вгадуєте, чому клієнти йдуть незадоволеними. APM — ваш помічник кухаря — він стежить за кожним кроком, від вхідних запитів до запитів до бази даних, і позначає все, що вас сповільнює. Повільні сторінки втрачають користувачів (дослідження показують, що 53% відмовляються, якщо сайт завантажується понад 3 секунди!), і APM допомагає вам ловити ці проблеми до того, як вони завдадуть шкоди. Це проактивний спокій — менше моментів «чому це зламалося?», більше перемог «погляньте, як це круто працює!».
Встановлення
Почніть з Composer:
composer require flightphp/apm
Вам знадобиться:
- PHP 7.4+: Забезпечує сумісність з LTS-дистрибутивами Linux, підтримуючи сучасний PHP.
- FlightPHP Core v3.15+: Легкий фреймворк, який ми покращуємо.
Підтримувані Бази Даних
FlightPHP APM наразі підтримує наступні бази даних для зберігання метрик:
- SQLite3: Проста, файлова, чудова для локальної розробки або невеликих застосунків. Варіант за замовчуванням у більшості налаштувань.
- MySQL/MariaDB: Ідеальна для більших проектів або продакшн-середовищ, де потрібне надійне, масштабоване зберігання.
Ви можете вибрати тип бази даних під час кроку налаштування (див. нижче). Переконайтеся, що ваше середовище PHP має необхідні розширення (наприклад, pdo_sqlite або pdo_mysql).
Початок Роботи
Ось ваш покроковий шлях до крутості APM:
1. Зареєструйте APM
Додайте це у ваш index.php або файл services.php, щоб почати відстеження:
use flight\apm\logger\LoggerFactory;
use flight\database\SimplePdo;
use flight\Apm;
$ApmLogger = LoggerFactory::create(__DIR__ . '/../../.runway-config.json');
$Apm = new Apm($ApmLogger);
$Apm->bindEventsToFlightInstance($app);
// Якщо ви додаєте з'єднання з базою даних
// Віддавайте перевагу SimplePdo (або PdoQueryCapture з Tracy Extensions у dev).
// Увімкніть відстеження запитів APM через масив опцій (5-й аргумент).
$pdo = new SimplePdo('mysql:host=localhost;dbname=example', 'user', 'pass', null, [
'trackApmQueries' => true, // required to capture queries for the APM
]);
$Apm->addPdoConnection($pdo);
Що тут відбувається?
LoggerFactory::create()захоплює вашу конфігурацію (докладніше про це незабаром) і налаштовує логгер — SQLite за замовчуванням.Apm— це зірка — він слухає події Flight (запити, маршрути, помилки тощо) і збирає метрики.bindEventsToFlightInstance($app)прив'язує все до вашого Flight-застосунку.
Порада: Вибірка Якщо ваш застосунок завантажений, логування кожного запиту може перевантажити систему. Використовуйте частоту вибірки (від 0.0 до 1.0):
$Apm = new Apm($ApmLogger, 0.1); // Logs 10% of requests
Це зберігає продуктивність на високому рівні, водночас надаючи вам надійні дані.
2. Налаштуйте Його
Виконайте це, щоб створити ваш .runway-config.json:
php vendor/bin/runway apm:init
Що це робить?
- Запускає майстер, який запитує, звідки надходять необроблені метрики (джерело) і куди йдуть оброблені дані (призначення).
- За замовчуванням використовується SQLite — наприклад,
sqlite:/tmp/apm_metrics.sqliteдля джерела, інший для призначення. - Ви отримаєте конфігурацію на кшталт:
{ "apm": { "source_type": "sqlite", "source_db_dsn": "sqlite:/tmp/apm_metrics.sqlite", "storage_type": "sqlite", "dest_db_dsn": "sqlite:/tmp/apm_metrics_processed.sqlite" } }
Цей процес також запитає, чи хочете ви запустити міграції для цього налаштування. Якщо ви налаштовуєте це вперше, відповідь — так.
Чому два місця? Необроблені метрики накопичуються швидко (подумайте про нефільтровані логи). Воркер обробляє їх у структуроване призначення для дашборду. Підтримує порядок!
3. Обробка Метрик за Допомогою Воркера
Воркер перетворює необроблені метрики на дані, готові для дашборду. Запустіть його один раз:
php vendor/bin/runway apm:worker
Що він робить?
- Читає з вашого джерела (наприклад,
apm_metrics.sqlite). - Обробляє до 100 метрик (розмір пакету за замовчуванням) у ваше призначення.
- Зупиняється, коли закінчено або якщо метрик не залишилося.
Підтримуйте Його Роботу Для живих застосунків вам потрібна безперервна обробка. Ось ваші варіанти:
-
Режим Демона:
php vendor/bin/runway apm:worker --daemonПрацює постійно, обробляючи метрики в міру надходження. Чудово для dev або невеликих налаштувань.
-
Crontab: Додайте це до вашого crontab (
crontab -e):* * * * * php /path/to/project/vendor/bin/runway apm:workerЗапускається кожну хвилину — ідеально для продакшену.
-
Tmux/Screen: Запустіть сесію, яку можна від'єднати:
tmux new -s apm-worker php vendor/bin/runway apm:worker --daemon # Ctrl+B, then D to detach; `tmux attach -t apm-worker` to reconnectПідтримує роботу навіть після виходу з системи.
-
Користувацькі Налаштування:
php vendor/bin/runway apm:worker --batch_size 50 --max_messages 1000 --timeout 300--batch_size 50: Обробляти 50 метрик за раз.--max_messages 1000: Зупинитися після 1000 метрик.--timeout 300: Вийти через 5 хвилин.
Чому варто турбуватися? Без воркера ваш дашборд порожній. Це місток між необробленими логами та практичними висновками.
4. Запустіть Дашборд
Перегляньте показники вашого застосунку:
php vendor/bin/runway apm:dashboard
Що це?
- Запускає PHP-сервер за адресою
http://localhost:8001/apm/dashboard. - Показує логи запитів, повільні маршрути, частоту помилок та інше.
Налаштуйте Його:
php vendor/bin/runway apm:dashboard --host 0.0.0.0 --port 8080 --php-path=/usr/local/bin/php
--host 0.0.0.0: Доступний з будь-якої IP-адреси (зручно для віддаленого перегляду).--port 8080: Використовуйте інший порт, якщо 8001 зайнятий.--php-path: Вкажіть шлях до PHP, якщо його немає у вашому PATH.
Відкрийте URL у браузері та досліджуйте!
Продакшн Режим
Для продакшену вам, можливо, доведеться спробувати кілька технік, щоб запустити дашборд, оскільки, ймовірно, є фаєрволи та інші заходи безпеки. Ось кілька варіантів:
- Використовуйте Зворотний Проксі: Налаштуйте Nginx або Apache для переадресації запитів до дашборду.
- SSH Тунель: Якщо ви можете SSH на сервер, використовуйте
ssh -L 8080:localhost:8001 youruser@yourserverдля тунелювання дашборду на вашу локальну машину. - VPN: Якщо ваш сервер за VPN, підключіться до нього та отримайте прямий доступ до дашборду.
- Налаштуйте Фаєрвол: Відкрийте порт 8001 для вашої IP-адреси або мережі сервера. (або будь-який порт, який ви встановили).
- Налаштуйте Apache/Nginx: Якщо у вас є веб-сервер перед вашим застосунком, ви можете налаштувати його на домен або піддомен. Якщо ви це зробите, встановіть document root на
/path/to/your/project/vendor/flightphp/apm/dashboard
Хочете інший дашборд?
Ви можете створити власний дашборд, якщо хочете! Подивіться на директорію vendor/flightphp/apm/src/apm/presenter для ідей щодо представлення даних для вашого власного дашборду!
Функції Дашборду
Дашборд — це ваша штаб-квартира APM — ось що ви побачите:
- Лог Запитів: Кожен запит з міткою часу, URL, кодом відповіді та загальним часом. Натисніть «Деталі» для проміжного ПЗ, запитів та помилок.
- Найповільніші Запити: Топ-5 запитів, що займають час (наприклад, «/api/heavy» за 2.5с).
- Найповільніші Маршрути: Топ-5 маршрутів за середнім часом — чудово для виявлення шаблонів.
- Частота Помилок: Відсоток запитів, що не вдалися (наприклад, 2.3% 500s).
- Перцентилі Затримки: 95-й (p95) і 99-й (p99) часи відповіді — знайте ваші найгірші сценарії.
- Графік Кодів Відповідей: Візуалізуйте 200, 404, 500 з часом.
- Довгі Запити/Проміжне ПЗ: Топ-5 повільних викликів бази даних та шарів проміжного ПЗ.
- Попадання/Промах Кешу: Як часто ваш кеш рятує ситуацію.
Додатково:
- Фільтруйте за «Остання Година», «Останній День» або «Останній Тиждень».
- Перемикайте темний режим для нічних сеансів.
Приклад:
Запит до /users може показати:
- Загальний Час: 150ms
- Проміжне ПЗ:
AuthMiddleware->handle(50ms) - Запит:
SELECT * FROM users(80ms) - Кеш: Попадання на
user_list(5ms)
Додавання Користувацьких Подій
Відстежуйте будь-що — наприклад, API-виклик або процес оплати:
use flight\apm\CustomEvent;
$app->eventDispatcher()->trigger('apm.custom', new CustomEvent('api_call', [
'endpoint' => 'https://api.example.com/users',
'response_time' => 0.25,
'status' => 200
]));
Де це з'явиться? У деталях запиту дашборду під «Користувацькі Події» — розгортається з красивим JSON-форматуванням.
Випадок Використання:
$start = microtime(true);
$apiResponse = file_get_contents('https://api.example.com/data');
$app->eventDispatcher()->trigger('apm.custom', new CustomEvent('external_api', [
'url' => 'https://api.example.com/data',
'time' => microtime(true) - $start,
'success' => $apiResponse !== false
]));
Тепер ви побачите, чи тягне цей API ваш застосунок вниз!
Моніторинг Бази Даних
Відстежуйте PDO-запити так:
use flight\database\SimplePdo;
$pdo = new SimplePdo('sqlite:/path/to/db.sqlite', null, null, null, [
'trackApmQueries' => true, // required to capture queries for the APM
]);
$Apm->addPdoConnection($pdo);
Що Ви Отримаєте:
- Текст запиту (наприклад,
SELECT * FROM users WHERE id = ?) - Час виконання (наприклад, 0.015s)
- Кількість рядків (наприклад, 42)
Увага:
- Опціонально: Пропустіть це, якщо вам не потрібне відстеження БД.
- SimplePdo (переважний): Використовуйте
SimplePdoзtrackApmQueries => true. ЗастарілийPdoWrapperвсе ще працює (5-й аргумент конструктораtrue). Сирий core PDO ще не підключений — стежте за оновленнями! - Попередження про Продуктивність: Логування кожного запиту на сайті з великим навантаженням на БД може сповільнити роботу. Використовуйте вибірку (
$Apm = new Apm($ApmLogger, 0.1)) для зменшення навантаження.
Приклад Виводу:
- Запит:
SELECT name FROM products WHERE price > 100 - Час: 0.023s
- Рядки: 15
Опції Воркера
Налаштуйте воркер на свій смак:
--timeout 300: Зупиняється через 5 хвилин — добре для тестування.--max_messages 500: Обмежує 500 метриками — тримає це кінцевим.--batch_size 200: Обробляє 200 за раз — балансує швидкість і пам'ять.--daemon: Працює безперервно — ідеально для живого моніторингу.
Приклад:
php vendor/bin/runway apm:worker --daemon --batch_size 100 --timeout 3600
Працює годину, обробляючи по 100 метрик за раз.
Request ID у Застосунку
Кожен запит має унікальний request ID для відстеження. Ви можете використовувати цей ID у вашому застосунку для кореляції логів та метрик. Наприклад, ви можете додати request ID до сторінки помилки:
Flight::map('error', function($message) {
// Get the request ID from the response header X-Flight-Request-Id
$requestId = Flight::response()->getHeader('X-Flight-Request-Id');
// Additionally you could fetch it from the Flight variable
// This method won't work well in swoole or other async platforms.
// $requestId = Flight::get('apm.request_id');
echo "Error: $message (Request ID: $requestId)";
});
Оновлення
Якщо ви оновлюєтеся до новішої версії APM, є шанс, що потрібні міграції бази даних. Ви можете зробити це, виконавши наступну команду:
php vendor/bin/runway apm:migrate
Це запустить будь-які міграції, необхідні для оновлення схеми бази даних до останньої версії.
Примітка: Якщо ваша APM база даних велика за розміром, ці міграції можуть зайняти деякий час. Ви можете запустити цю команду в години з меншим навантаженням.
Оновлення з 0.4.3 -> 0.5.0
Якщо ви оновлюєтеся з 0.4.3 до 0.5.0, вам потрібно буде виконати наступну команду:
php vendor/bin/runway apm:config-migrate
Це мігрує вашу конфігурацію зі старого формату, використовуючи файл .runway-config.json, до нового формату, який зберігає ключі/значення у файлі config.php.
Очищення Старих Даних
Щоб підтримувати вашу базу даних у порядку, ви можете очищати старі дані. Це особливо корисно, якщо ви запускаєте завантажений застосунок і хочете підтримувати розмір бази даних керованим. Ви можете зробити це, виконавши наступну команду:
php vendor/bin/runway apm:purge
Це видалить усі дані старше 30 днів з бази даних. Ви можете налаштувати кількість днів, передавши інше значення опції --days:
php vendor/bin/runway apm:purge --days 7
Це видалить усі дані старше 7 днів з бази даних.
Усунення Неполадок
Застрягли? Спробуйте це:
-
Немає Даних на Дашборді?
- Чи працює воркер? Перевірте
ps aux | grep apm:worker. - Шляхи конфігурації збігаються? Перевірте, чи DSN у
.runway-config.jsonвказують на реальні файли. - Запустіть
php vendor/bin/runway apm:workerвручну для обробки очікуюючих метрик.
- Чи працює воркер? Перевірте
-
Помилки Воркера?
- Подивіться на ваші SQLite файли (наприклад,
sqlite3 /tmp/apm_metrics.sqlite "SELECT * FROM apm_metrics_log LIMIT 5"). - Перевірте логи PHP на предмет трасування стека.
- Подивіться на ваші SQLite файли (наприклад,
-
Дашборд Не Запускається?
- Порт 8001 зайнятий? Використовуйте
--port 8080. - PHP не знайдено? Використовуйте
--php-path /usr/bin/php. - Фаєрвол блокує? Відкрийте порт або використовуйте
--host localhost.
- Порт 8001 зайнятий? Використовуйте
-
Занадто Повільно?
- Зменшіть частоту вибірки:
$Apm = new Apm($ApmLogger, 0.05)(5%). - Зменшіть розмір пакету:
--batch_size 20.
- Зменшіть частоту вибірки:
-
Не Відстежуються Винятки/Помилки?
- Якщо у вас увімкнено Tracy для вашого проекту, він перевизначить обробку помилок Flight. Вам потрібно буде вимкнути Tracy і переконатися, що встановлено
Flight::set('flight.handle_errors', true);.
- Якщо у вас увімкнено Tracy для вашого проекту, він перевизначить обробку помилок Flight. Вам потрібно буде вимкнути Tracy і переконатися, що встановлено
-
Не Відстежуються Запити до Бази Даних?
- Віддавайте перевагу
SimplePdoз['trackApmQueries' => true]як 5-й аргумент конструктора (масив опцій). - Якщо ви все ще використовуєте застарілий
PdoWrapper, передайтеtrueяк 5-й аргумент. - Викличте
$Apm->addPdoConnection($pdo)після створення з'єднання.
- Віддавайте перевагу
Awesome-plugins/tracy
Tracy
Tracy — це чудовий обробник помилок, який можна використовувати з Flight. Він має ряд панелей, які допоможуть вам налагодити ваш додаток. Його також дуже легко розширити та додати власні панелі. Команда Flight створила кілька панелей спеціально для проектів Flight з плагіном flightphp/tracy-extensions (Flight vars, DB queries, request, session та необов'язкова панель Twig, коли ви передаєте профіль профайлера — див. Tracy Extensions).
Встановлення
Встановіть за допомогою composer. І ви дійсно захочете встановити це без dev-версії, оскільки Tracy постачається з компонентом обробки помилок для продакшену.
composer require tracy/tracy
Базова конфігурація
Є деякі базові опції конфігурації для початку роботи. Ви можете дізнатися більше про них у Документації Tracy.
require 'vendor/autoload.php';
use Tracy\Debugger;
// Увімкнути Tracy
Debugger::enable();
// Debugger::enable(Debugger::DEVELOPMENT) // іноді вам потрібно бути явним (також Debugger::PRODUCTION)
// Debugger::enable('23.75.345.200'); // ви також можете надати масив IP-адрес
// Тут будуть записуватися помилки та винятки. Переконайтеся, що ця директорія існує та доступна для запису.
Debugger::$logDirectory = __DIR__ . '/../log/';
Debugger::$strictMode = true; // відображати всі помилки
// Debugger::$strictMode = E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED; // всі помилки, крім застарілих сповіщень
if (Debugger::$showBar) {
$app->set('flight.content_length', false); // якщо панель Debugger видима, то content-length не може бути встановлений Flight
// Це специфічно для розширення Tracy для Flight, якщо ви його включили
// в іншому випадку закоментуйте це.
new TracyExtensionLoader($app);
}
Корисні поради
Коли ви налагоджуєте свій код, є деякі дуже корисні функції для виведення даних для вас.
bdump($var)- Це виведе змінну в панель Tracy Bar в окремій панелі.dumpe($var)- Це виведе змінну і потім негайно завершиться.
Awesome-plugins/active_record
Flight Active Record
Активний запис — це відображення сутності бази даних на об'єкт PHP. Простіше кажучи, якщо у вас є таблиця users у базі даних, ви можете "перекласти" рядок у цій таблиці на клас User та об'єкт $user у вашому коді. Див. основний приклад.
Натисніть тут для репозиторію на GitHub.
Основний приклад
Припустимо, у вас є така таблиця:
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT,
password TEXT
);
Тепер ви можете налаштувати новий клас для представлення цієї таблиці:
/**
* Клас ActiveRecord зазвичай є одниною
*
* Дуже рекомендується додавати властивості таблиці як коментарі тут
*
* @property int $id
* @property string $name
* @property string $password
*/
class User extends flight\ActiveRecord {
public function __construct($database_connection)
{
// ви можете налаштувати це таким чином
parent::__construct($database_connection, 'users');
// або таким чином
parent::__construct($database_connection, null, [ 'table' => 'users']);
}
}
Тепер спостерігайте за магією!
// для sqlite
$database_connection = new PDO('sqlite:test.db'); // це просто для прикладу, ви ймовірно використовуватимете реальне з'єднання з базою даних
// для mysql
$database_connection = new PDO('mysql:host=localhost;dbname=test_db&charset=utf8bm4', 'username', 'password');
// або mysqli
$database_connection = new mysqli('localhost', 'username', 'password', 'test_db');
// або mysqli з створенням не на основі об'єкта
$database_connection = mysqli_connect('localhost', 'username', 'password', 'test_db');
$user = new User($database_connection);
$user->name = 'Bobby Tables';
$user->password = password_hash('some cool password');
$user->insert();
// або $user->save();
echo $user->id; // 1
$user->name = 'Joseph Mamma';
$user->password = password_hash('some cool password again!!!');
$user->insert();
// не можна використовувати $user->save() тут, інакше це вважатиме оновленням!
echo $user->id; // 2
І було так просто додати нового користувача! Тепер, коли в базі даних є рядок користувача, як ви його витягнете?
$user->find(1); // знаходить id = 1 у базі даних і повертає його.
echo $user->name; // 'Bobby Tables'
А що, якщо ви хочете знайти всіх користувачів?
$users = $user->findAll();
А з певною умовою?
$users = $user->like('name', '%mamma%')->findAll();
Бачите, наскільки це весело? Давайте встановимо це та почнемо!
Встановлення
Просто встановіть за допомогою Composer
composer require flightphp/active-record
Використання
Це можна використовувати як самостійну бібліотеку або з PHP Framework Flight. Повністю залежить від вас.
Самостійно
Просто переконайтеся, що ви передаєте з'єднання PDO до конструктора.
$pdo_connection = new PDO('sqlite:test.db'); // це просто для прикладу, ви ймовірно використовуватимете реальне з'єднання з базою даних
$User = new User($pdo_connection);
Не хочете завжди встановлювати з'єднання з базою даних у конструкторі? Див. Керування з'єднанням з базою даних для інших ідей!
Реєстрація як методу у Flight
Якщо ви використовуєте PHP Framework Flight, ви можете зареєструвати клас ActiveRecord як сервіс, але чесно кажучи, це не обов'язково.
Flight::register('user', 'User', [ $pdo_connection ]);
// потім ви можете використовувати це так у контролері, функції тощо.
Flight::user()->find(1);
Методи runway
runway — це CLI-інструмент для Flight, який має спеціальну команду для цієї бібліотеки.
# Використання
php runway make:record database_table_name [class_name]
# Приклад
php runway make:record users
Це створить новий клас у директорії app/records/ як UserRecord.php з таким вмістом:
<?php
declare(strict_types=1);
namespace app\records;
/**
* Клас ActiveRecord для таблиці users.
* @link https://docs.flightphp.com/awesome-plugins/active-record
*
* @property int $id
* @property string $username
* @property string $email
* @property string $password_hash
* @property string $created_dt
*/
class UserRecord extends \flight\ActiveRecord
{
/**
* @var array $relations Встановлює відносини для моделі
* https://docs.flightphp.com/awesome-plugins/active-record#relationships
*/
protected array $relations = [
// 'relation_name' => [ self::HAS_MANY, 'RelatedClass', 'foreign_key' ],
];
/**
* Конструктор
* @param mixed $databaseConnection З'єднання з базою даних
*/
public function __construct($databaseConnection)
{
parent::__construct($databaseConnection, 'users');
}
}
Функції CRUD
find($id = null) : boolean|ActiveRecord
Знаходить один запис і призначає його поточному об'єкту. Якщо ви передаєте $id якогось роду, це виконає пошук за первинним ключем з цим значенням. Якщо нічого не передано, це просто знайде перший запис у таблиці.
Додатково ви можете передати інші допоміжні методи для запиту таблиці.
// знаходить запис з деякими умовами заздалегідь
$user->notNull('password')->orderBy('id DESC')->find();
// знаходить запис за конкретним id
$id = 123;
$user->find($id);
findAll(): array<int,ActiveRecord>
Знаходить всі записи в таблиці, яку ви вказуєте.
$user->findAll();
isHydrated(): boolean (v0.4.0)
Повертає true, якщо поточний запис був гідратований (отриманий з бази даних).
$user->find(1);
// якщо запис знайдено з даними...
$user->isHydrated(); // true
insert(): boolean|ActiveRecord
Вставляє поточний запис у базу даних.
$user = new User($pdo_connection);
$user->name = 'demo';
$user->password = md5('demo');
$user->insert();
Первинні ключі на основі тексту
Якщо у вас є первинний ключ на основі тексту (наприклад, UUID), ви можете встановити значення первинного ключа перед вставкою одним з двох способів.
$user = new User($pdo_connection, [ 'primaryKey' => 'uuid' ]);
$user->uuid = 'some-uuid';
$user->name = 'demo';
$user->password = md5('demo');
$user->insert(); // або $user->save();
або ви можете мати автоматично згенерований первинний ключ для вас через події.
class User extends flight\ActiveRecord {
public function __construct($database_connection)
{
parent::__construct($database_connection, 'users', [ 'primaryKey' => 'uuid' ]);
// ви також можете встановити primaryKey таким чином замість масиву вище.
$this->primaryKey = 'uuid';
}
protected function beforeInsert(self $self) {
$self->uuid = uniqid(); // або як вам потрібно генерувати унікальні id
}
}
Якщо ви не встановите первинний ключ перед вставкою, він буде встановлено на rowid, і база даних згенерує його для вас, але він не збережеться, оскільки це поле може не існувати в вашій таблиці. Тому рекомендується використовувати подію для автоматичного керування цим.
update(): boolean|ActiveRecord
Оновлює поточний запис у базі даних.
$user->greaterThan('id', 0)->orderBy('id desc')->find();
$user->email = 'test@example.com';
$user->update();
save(): boolean|ActiveRecord
Вставляє або оновлює поточний запис у базі даних. Якщо запис має id, це оновить, інакше вставить.
$user = new User($pdo_connection);
$user->name = 'demo';
$user->password = md5('demo');
$user->save();
Примітка: Якщо у вас визначені відносини в класі, це рекурсивно збереже ці відносини, якщо вони визначені, створені та мають брудні дані для оновлення. (v0.4.0 та вище)
delete(): boolean
Видаляє поточний запис з бази даних.
$user->gt('id', 0)->orderBy('id desc')->find();
$user->delete();
Ви також можете видалити кілька записів, виконавши пошук заздалегідь.
$user->like('name', 'Bob%')->delete();
dirty(array $dirty = []): ActiveRecord
Брудні дані стосуються даних, які були змінені в записі.
$user->greaterThan('id', 0)->orderBy('id desc')->find();
// на цьому етапі нічого не є "брудним".
$user->email = 'test@example.com'; // тепер email вважається "брудним", оскільки він змінений.
$user->update();
// тепер немає даних, які є брудними, оскільки вони оновлені та збережені в базі даних
$user->password = password_hash()'newpassword'); // тепер це брудне
$user->dirty(); // передача нічого очистить всі брудні записи.
$user->update(); // нічого не оновиться, оскільки нічого не було захоплено як брудне.
$user->dirty([ 'name' => 'something', 'password' => password_hash('a different password') ]);
$user->update(); // обидва name та password оновлені.
copyFrom(array $data): ActiveRecord (v0.4.0)
Це псевдонім для методу dirty(). Трохи зрозуміліше, що ви робите.
$user->copyFrom([ 'name' => 'something', 'password' => password_hash('a different password') ]);
$user->update(); // обидва name та password оновлені.
isDirty(): boolean (v0.4.0)
Повертає true, якщо поточний запис був змінений.
$user->greaterThan('id', 0)->orderBy('id desc')->find();
$user->email = 'test@email.com';
$user->isDirty(); // true
reset(bool $include_query_data = true): ActiveRecord
Скидає поточний запис до його початкового стану. Це дуже корисно для поведінки типу циклу.
Якщо ви передасте true, це також скине дані запиту, які використовувалися для пошуку поточного об'єкта (поведінка за замовчуванням).
$users = $user->greaterThan('id', 0)->orderBy('id desc')->find();
$user_company = new UserCompany($pdo_connection);
foreach($users as $user) {
$user_company->reset(); // почніть з чистого аркуша
$user_company->user_id = $user->id;
$user_company->company_id = $some_company_id;
$user_company->insert();
}
getBuiltSql(): string (v0.4.1)
Після виконання методу find(), findAll(), insert(), update() або save() ви можете отримати SQL, який був побудований, і використовувати його для цілей налагодження.
Методи SQL-запиту
select(string $field1 [, string $field2 ... ])
Ви можете вибрати лише кілька стовпців у таблиці, якщо бажаєте (це ефективніше для дуже широких таблиць з багатьма стовпцями)
$user->select('id', 'name')->find();
from(string $table)
Ви технічно можете вибрати іншу таблицю також! Чому б і ні?!
$user->select('id', 'name')->from('user')->find();
join(string $table_name, string $join_condition)
Ви навіть можете приєднатися до іншої таблиці в базі даних.
$user->join('contacts', 'contacts.user_id = users.id')->find();
where(string $where_conditions)
Ви можете встановити деякі власні аргументи where (ви не можете встановити параметри в цій інструкції where)
$user->where('id=1 AND name="demo"')->find();
Примітка щодо безпеки - Ви можете бути спокушені зробити щось на кшталт $user->where("id = '{$id}' AND name = '{$name}'")->find();. Будь ласка, НЕ РОБІТЬ ЦЬОГО!!! Це вразливе до того, що відомо як атаки SQL-ін'єкцій. Є багато статей онлайн, будь ласка, погугліть "sql injection attacks php" і ви знайдете багато статей на цю тему. Правильний спосіб обробити це з цією бібліотекою — замість цього методу where(), ви б зробили щось на кшталт $user->eq('id', $id)->eq('name', $name)->find(); Якщо вам абсолютно необхідно це зробити, бібліотека PDO має $pdo->quote($var) для екранування. Тільки після використання quote() ви можете використовувати це в інструкції where().
group(string $group_by_statement)/groupBy(string $group_by_statement)
Групуйте ваші результати за певною умовою.
$user->select('COUNT(*) as count')->groupBy('name')->findAll();
order(string $order_by_statement)/orderBy(string $order_by_statement)
Сортуйте повернутий запит певним чином.
$user->orderBy('name DESC')->find();
limit(string $limit)/limit(int $offset, int $limit)
Обмежте кількість повернених записів. Якщо надано другий int, це буде зсув, обмеження, як у SQL.
$user->orderby('name DESC')->limit(0, 10)->findAll();
Умови WHERE
equal(string $field, mixed $value) / eq(string $field, mixed $value)
Де field = $value
$user->eq('id', 1)->find();
notEqual(string $field, mixed $value) / ne(string $field, mixed $value)
Де field <> $value
$user->ne('id', 1)->find();
isNull(string $field)
Де field IS NULL
$user->isNull('id')->find();
isNotNull(string $field) / notNull(string $field)
Де field IS NOT NULL
$user->isNotNull('id')->find();
greaterThan(string $field, mixed $value) / gt(string $field, mixed $value)
Де field > $value
$user->gt('id', 1)->find();
lessThan(string $field, mixed $value) / lt(string $field, mixed $value)
Де field < $value
$user->lt('id', 1)->find();
greaterThanOrEqual(string $field, mixed $value) / ge(string $field, mixed $value) / gte(string $field, mixed $value)
Де field >= $value
$user->ge('id', 1)->find();
lessThanOrEqual(string $field, mixed $value) / le(string $field, mixed $value) / lte(string $field, mixed $value)
Де field <= $value
$user->le('id', 1)->find();
like(string $field, mixed $value) / notLike(string $field, mixed $value)
Де field LIKE $value або field NOT LIKE $value
$user->like('name', 'de')->find();
in(string $field, array $values) / notIn(string $field, array $values)
Де field IN($value) або field NOT IN($value)
$user->in('id', [1, 2])->find();
between(string $field, array $values)
Де field BETWEEN $value AND $value1
$user->between('id', [1, 2])->find();
Умови OR
Можна обгорнути ваші умови в інструкцію OR. Це робиться за допомогою методу startWrap() та endWrap() або заповненням 3-го параметра умови після поля та значення.
// Метод 1
$user->eq('id', 1)->startWrap()->eq('name', 'demo')->or()->eq('name', 'test')->endWrap('OR')->find();
// Це оцінюється як `id = 1 AND (name = 'demo' OR name = 'test')`
// Метод 2
$user->eq('id', 1)->eq('name', 'demo', 'OR')->find();
// Це оцінюється як `id = 1 OR name = 'demo'`
Відносини
Ви можете встановити кілька видів відносин за допомогою цієї бібліотеки. Ви можете встановити відносини один-до-багатьох та один-до-одного між таблицями. Це вимагає трохи додаткового налаштування в класі заздалегідь.
Встановлення масиву $relations не важко, але вгадування правильного синтаксису може бути заплутаним.
protected array $relations = [
// ви можете назвати ключ будь-як. Назва ActiveRecord, ймовірно, хороша. Наприклад: user, contact, client
'user' => [
// обов'язково
// self::HAS_MANY, self::HAS_ONE, self::BELONGS_TO
self::HAS_ONE, // це тип відносини
// обов'язково
'Some_Class', // це "інший" клас ActiveRecord, на який буде посилання
// обов'язково
// залежно від типу відносини
// self::HAS_ONE = зовнішній ключ, що посилається на з'єднання
// self::HAS_MANY = зовнішній ключ, що посилається на з'єднання
// self::BELONGS_TO = локальний ключ, що посилається на з'єднання
'local_or_foreign_key',
// просто для інформації, це також приєднує лише до первинного ключа "іншої" моделі
// необов'язково
[ 'eq' => [ 'client_id', 5 ], 'select' => 'COUNT(*) as count', 'limit' 5 ], // додаткові умови, які ви хочете при приєднанні відносини
// $record->eq('client_id', 5)->select('COUNT(*) as count')->limit(5))
// необов'язково
'back_reference_name' // це якщо ви хочете посилатися назад на цю відносини Ex: $user->contact->user;
];
]
class User extends ActiveRecord{
protected array $relations = [
'contacts' => [ self::HAS_MANY, Contact::class, 'user_id' ],
'contact' => [ self::HAS_ONE, Contact::class, 'user_id' ],
];
public function __construct($database_connection)
{
parent::__construct($database_connection, 'users');
}
}
class Contact extends ActiveRecord{
protected array $relations = [
'user' => [ self::BELONGS_TO, User::class, 'user_id' ],
'user_with_backref' => [ self::BELONGS_TO, User::class, 'user_id', [], 'contact' ],
];
public function __construct($database_connection)
{
parent::__construct($database_connection, 'contacts');
}
}
Тепер у нас налаштовані посилання, тому ми можемо використовувати їх дуже легко!
$user = new User($pdo_connection);
// знаходимо найновішого користувача.
$user->notNull('id')->orderBy('id desc')->find();
// отримуємо контакти за допомогою відносини:
foreach($user->contacts as $contact) {
echo $contact->id;
}
// або ми можемо піти іншим шляхом.
$contact = new Contact();
// знаходимо один контакт
$contact->find();
// отримуємо користувача за допомогою відносини:
echo $contact->user->name; // це ім'я користувача
Досить круто, еге?
Eager Loading
Огляд
Eager loading розв'язує проблему N+1 запитів, завантажуючи відносини заздалегідь. Замість виконання окремого запиту для відносин кожного запису, eager loading отримує всі пов'язані дані лише в одному додатковому запиті на відносини.
Примітка: Eager loading доступний лише для v0.7.0 та вище.
Основне використання
Використовуйте метод with() для вказівки, які відносини завантажити заздалегідь:
// Завантажуємо користувачів з їх контактами в 2 запити замість N+1
$users = $user->with('contacts')->findAll();
foreach ($users as $u) {
foreach ($u->contacts as $contact) {
echo $contact->email; // Без додаткового запиту!
}
}
Кілька відносин
Завантажуйте кілька відносин одразу:
$users = $user->with(['contacts', 'profile', 'settings'])->findAll();
Типи відносин
HAS_MANY
// Eager завантажуємо всі контакти для кожного користувача
$users = $user->with('contacts')->findAll();
foreach ($users as $u) {
// $u->contacts вже завантажено як масив
foreach ($u->contacts as $contact) {
echo $contact->email;
}
}
HAS_ONE
// Eager завантажуємо один контакт для кожного користувача
$users = $user->with('contact')->findAll();
foreach ($users as $u) {
// $u->contact вже завантажено як об'єкт
echo $u->contact->email;
}
BELONGS_TO
// Eager завантажуємо батьківських користувачів для всіх контактів
$contacts = $contact->with('user')->findAll();
foreach ($contacts as $c) {
// $c->user вже завантажено
echo $c->user->name;
}
З find()
Eager loading працює як з findAll() , так і з find() :
$user = $user->with('contacts')->find(1);
// Користувач і всі їхні контакти завантажені в 2 запити
Переваги продуктивності
Без eager loading (проблема N+1):
$users = $user->findAll(); // 1 запит
foreach ($users as $u) {
$contacts = $u->contacts; // N запитів (один на користувача!)
}
// Всього: 1 + N запитів
З eager loading:
$users = $user->with('contacts')->findAll(); // 2 запити всього
foreach ($users as $u) {
$contacts = $u->contacts; // 0 додаткових запитів!
}
// Всього: 2 запити (1 для користувачів + 1 для всіх контактів)
Для 10 користувачів це зменшує запити з 11 до 2 — зменшення на 82%!
Важливі примітки
- Eager loading повністю необов'язковий — lazy loading все ще працює як раніше
- Вже завантажені відносини автоматично пропускаються
- Зворотні посилання працюють з eager loading
- Колбеки відносин поважаються під час eager loading
Обмеження
- Вкладене eager loading (наприклад, with(['contacts.addresses']) ) наразі не підтримується
- Обмеження eager завантаження через замикання не підтримуються в цій версії
Встановлення власних даних
Іноді вам може знадобитися прикріпити щось унікальне до вашого ActiveRecord, наприклад, власний розрахунок, який може бути простішим прикріпити до об'єкта, який потім передається, скажімо, шаблону.
setCustomData(string $field, mixed $value)
Ви прикріплюєте власні дані за допомогою методу setCustomData().
$user->setCustomData('page_view_count', $page_view_count);
А потім ви просто посилаєтеся на це як на звичайну властивість об'єкта.
echo $user->page_view_count;
Події
Ще одна супер крута функція цієї бібліотеки — це події. Події запускаються в певні моменти на основі певних методів, які ви викликаєте. Вони дуже корисні для автоматичного налаштування даних для вас.
onConstruct(ActiveRecord $ActiveRecord, array &config)
Це дуже корисно, якщо вам потрібно встановити з'єднання за замовчуванням або щось подібне.
// index.php або bootstrap.php
Flight::register('db', 'PDO', [ 'sqlite:test.db' ]);
//
//
//
// User.php
class User extends flight\ActiveRecord {
protected function onConstruct(self $self, array &$config) { // не забудьте посилання &
// ви могли б зробити це для автоматичного встановлення з'єднання
$config['connection'] = Flight::db();
// або це
$self->transformAndPersistConnection(Flight::db());
// Ви також можете встановити назву таблиці таким чином.
$config['table'] = 'users';
}
}
beforeFind(ActiveRecord $ActiveRecord)
Це, ймовірно, корисно лише якщо вам потрібно маніпулювати запитом кожного разу.
class User extends flight\ActiveRecord {
public function __construct($database_connection)
{
parent::__construct($database_connection, 'users');
}
protected function beforeFind(self $self) {
// завжди запускайте id >= 0, якщо це ваш стиль
$self->gte('id', 0);
}
}
afterFind(ActiveRecord $ActiveRecord)
Цей, ймовірно, корисніший, якщо вам завжди потрібно запускати деяку логіку кожного разу, коли цей запис отримується. Вам потрібно дешифрувати щось? Вам потрібно запускати власний запит підрахунку кожного разу (не ефективно, але ну)?
class User extends flight\ActiveRecord {
public function __construct($database_connection)
{
parent::__construct($database_connection, 'users');
}
protected function afterFind(self $self) {
// дешифрування чогось
$self->secret = yourDecryptFunction($self->secret, $some_key);
// можливо, зберігання чогось власного, як запит???
$self->setCustomData('view_count', $self->select('COUNT(*) count')->from('user_views')->eq('user_id', $self->id)['count'];
}
}
beforeFindAll(ActiveRecord $ActiveRecord)
Це, ймовірно, корисно лише якщо вам потрібно маніпулювати запитом кожного разу.
class User extends flight\ActiveRecord {
public function __construct($database_connection)
{
parent::__construct($database_connection, 'users');
}
protected function beforeFindAll(self $self) {
// завжди запускайте id >= 0, якщо це ваш стиль
$self->gte('id', 0);
}
}
afterFindAll(array<int,ActiveRecord> $results)
Подібно до afterFind(), але ви можете зробити це для всіх записів!
class User extends flight\ActiveRecord {
public function __construct($database_connection)
{
parent::__construct($database_connection, 'users');
}
protected function afterFindAll(array $results) {
foreach($results as $self) {
// робіть щось круте, як afterFind()
}
}
}
beforeInsert(ActiveRecord $ActiveRecord)
Дуже корисно, якщо вам потрібно встановити деякі значення за замовчуванням кожного разу.
class User extends flight\ActiveRecord {
public function __construct($database_connection)
{
parent::__construct($database_connection, 'users');
}
protected function beforeInsert(self $self) {
// встановіть деякі розумні значення за замовчуванням
if(!$self->created_date) {
$self->created_date = gmdate('Y-m-d');
}
if(!$self->password) {
$self->password = password_hash((string) microtime(true));
}
}
}
afterInsert(ActiveRecord $ActiveRecord)
Можливо, у вас є випадок використання для зміни даних після вставки?
class User extends flight\ActiveRecord {
public function __construct($database_connection)
{
parent::__construct($database_connection, 'users');
}
protected function afterInsert(self $self) {
// робіть, що хочете
Flight::cache()->set('most_recent_insert_id', $self->id);
// або що завгодно....
}
}
beforeUpdate(ActiveRecord $ActiveRecord)
Дуже корисно, якщо вам потрібно встановити деякі значення за замовчуванням кожного разу під час оновлення.
class User extends flight\ActiveRecord {
public function __construct($database_connection)
{
parent::__construct($database_connection, 'users');
}
protected function beforeInsert(self $self) {
// встановіть деякі розумні значення за замовчуванням
if(!$self->updated_date) {
$self->updated_date = gmdate('Y-m-d');
}
}
}
afterUpdate(ActiveRecord $ActiveRecord)
Можливо, у вас є випадок використання для зміни даних після оновлення?
class User extends flight\ActiveRecord {
public function __construct($database_connection)
{
parent::__construct($database_connection, 'users');
}
protected function afterInsert(self $self) {
// робіть, що хочете
Flight::cache()->set('most_recently_updated_user_id', $self->id);
// або що завгодно....
}
}
beforeSave(ActiveRecord $ActiveRecord)/afterSave(ActiveRecord $ActiveRecord)
Це корисно, якщо ви хочете, щоб події відбувалися як під час вставок, так і під час оновлень. Я пощаджу вас довгим поясненням, але я впевнений, що ви можете здогадатися, що це таке.
class User extends flight\ActiveRecord {
public function __construct($database_connection)
{
parent::__construct($database_connection, 'users');
}
protected function beforeSave(self $self) {
$self->last_updated = gmdate('Y-m-d H:i:s');
}
}
beforeDelete(ActiveRecord $ActiveRecord)/afterDelete(ActiveRecord $ActiveRecord)
Не впевнений, що ви хотіли б зробити тут, але ніяких суджень! Рухайтеся!
class User extends flight\ActiveRecord {
public function __construct($database_connection)
{
parent::__construct($database_connection, 'users');
}
protected function beforeDelete(self $self) {
echo 'He was a brave soldier... :cry-face:';
}
}
Керування з'єднанням з базою даних
Коли ви використовуєте цю бібліотеку, ви можете встановити з'єднання з базою даних кількома різними способами. Ви можете встановити з'єднання в конструкторі, ви можете встановити його через змінну конфігурації $config['connection'] або ви можете встановити його через setDatabaseConnection() (v0.4.1).
$pdo_connection = new PDO('sqlite:test.db'); // для прикладу
$user = new User($pdo_connection);
// або
$user = new User(null, [ 'connection' => $pdo_connection ]);
// або
$user = new User();
$user->setDatabaseConnection($pdo_connection);
Якщо ви хочете уникнути завжди встановлювати $database_connection кожного разу, коли викликаєте active record, є способи обійти це!
// index.php або bootstrap.php
// Встановіть це як зареєстрований клас у Flight
Flight::register('db', 'PDO', [ 'sqlite:test.db' ]);
// User.php
class User extends flight\ActiveRecord {
public function __construct(array $config = [])
{
$database_connection = $config['connection'] ?? Flight::db();
parent::__construct($database_connection, 'users', $config);
}
}
// І тепер не потрібні аргументи!
$user = new User();
Примітка: Якщо ви плануєте unit-тестування, робити це таким чином може додати деякі виклики до unit-тестування, але загалом, оскільки ви можете інжектувати ваше з'єднання з
setDatabaseConnection()або$config['connection'], це не так погано.
Якщо вам потрібно оновити з'єднання з базою даних, наприклад, якщо ви запускаєте довготривалий CLI-скрипт і потрібно оновлювати з'єднання час від часу, ви можете переустановити з'єднання з $your_record->setDatabaseConnection($pdo_connection).
Співпраця
Будь ласка, робіть. :D
Налаштування
Коли ви сприяєте, переконайтеся, що ви запускаєте composer test-coverage, щоб підтримувати 100% покриття тестами (це не справжнє покриття unit-тестів, більше як інтеграційне тестування).
Також переконайтеся, що ви запускаєте composer beautify та composer phpcs, щоб виправити будь-які помилки лінтингу.
Ліцензія
MIT
Awesome-plugins/latte
Latte
Latte — це потужний шаблонізатор, який дуже простий у використанні та ближчий до синтаксису PHP, ніж Twig чи Smarty. Його також легко розширювати та додавати власні фільтри й функції.
Встановлення
Встановіть за допомогою composer.
composer require latte/latte
Базова Конфігурація
Є кілька базових опцій конфігурації для початку. Більше про них можна прочитати в Документації Latte.
require 'vendor/autoload.php';
$app = Flight::app();
$app->map('render', function(string $template, array $data, ?string $block): void {
$latte = new Latte\Engine;
// Де Latte зберігає свій кеш
$latte->setTempDirectory(__DIR__ . '/../cache/');
$finalPath = Flight::get('flight.views.path') . $template;
$latte->render($finalPath, $data, $block);
});
Простий Приклад Макету
Ось простий приклад файлу макету. Це файл, який буде використовуватися для обгортання всіх ваших інших представлень.
<!-- app/views/layout.latte -->
<!doctype html>
<html lang="en">
<head>
<title>{$title ? $title . ' - '}My App</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<nav>
<!-- ваші елементи навігації тут -->
</nav>
</header>
<div id="content">
<!-- Ось тут магія -->
{block content}{/block}
</div>
<div id="footer">
© Copyright
</div>
</body>
</html>
А тепер у нас є ваш файл, який буде рендеритися всередині блоку content:
<!-- app/views/home.latte -->
<!-- Це повідомляє Latte, що цей файл "всередині" файлу layout.latte -->
{extends layout.latte}
<!-- Це вміст, який буде рендеритися всередині макету в блоці content -->
{block content}
<h1>Головна Сторінка</h1>
<p>Ласкаво просимо до моєї програми!</p>
{/block}
Потім, коли ви йдете рендерити це у вашій функції чи контролері, ви робите щось на кшталт цього:
// простий маршрут
Flight::route('/', function () {
Flight::render('home.latte', [
'title' => 'Home Page'
]);
});
// або якщо ви використовуєте контролер
Flight::route('/', [HomeController::class, 'index']);
// HomeController.php
class HomeController
{
public function index()
{
Flight::render('home.latte', [
'title' => 'Home Page'
]);
}
}
Дивіться Документацію Latte для отримання додаткової інформації про те, як використовувати Latte на повну потужність!
Налагодження з Tracy
Потрібен PHP 8.1+ для цієї секції.
Ви також можете використовувати Tracy для допомоги в налагодженні ваших файлів шаблонів Latte прямо з коробки! Якщо у вас вже встановлено Tracy, вам потрібно додати розширення Latte до Tracy.
// services.php
use Tracy\Debugger;
$app->map('render', function(string $template, array $data, ?string $block): void {
$latte = new Latte\Engine;
// Де Latte зберігає свій кеш
$latte->setTempDirectory(__DIR__ . '/../cache/');
$finalPath = Flight::get('flight.views.path') . $template;
// Це додасть розширення тільки якщо панель налагодження Tracy увімкнена
if (Debugger::$showBar === true) {
// ось де ви додаєте панель Latte до Tracy
$latte->addExtension(new Latte\Bridges\Tracy\TracyExtension);
}
$latte->render($finalPath, $data, $block);
});Awesome-plugins/awesome_plugins
Чудові Плагіни
Flight надзвичайно розширюваний. Існує низка плагінів, які можна використовувати для додавання функціональності до вашої програми Flight. Деякі з них офіційно підтримуються командою Flight, а інші - мікро/легкі бібліотеки, які допоможуть вам розпочати роботу.
Інструменти ШІ
Flight може стати ще крутішим завдяки плагінам на базі ШІ.
- Flight MCP - Плагін для інтеграції MCP (Model Control Protocol) з Flight, що забезпечує безперебійну функціональність на базі ШІ. В основному зосереджений на документації, він допомагає зменшити витрати на токени, надаючи найактуальнішу інформацію про ваші проекти Flight.
Документація API
Документація API є критично важливою для будь-якого API. Вона допомагає розробникам зрозуміти, як взаємодіяти з вашим API та чого очікувати у відповідь. Існує кілька інструментів, які допоможуть вам генерувати документацію API для ваших проектів Flight.
- FlightPHP OpenAPI Generator - Пост у блозі, написаний Даніелем Шрайбером про те, як використовувати OpenAPI Spec з FlightPHP для побудови вашого API з використанням підходу API first.
- SwaggerUI - Swagger UI - це чудовий інструмент, який допоможе вам генерувати документацію API для ваших проектів Flight. Він дуже простий у використанні і може бути налаштований відповідно до ваших потреб. Це PHP-бібліотека для генерації документації Swagger.
Моніторинг Продуктивності Додатків (APM)
Моніторинг продуктивності додатків (APM) є критично важливим для будь-якого додатку. Він допомагає вам зрозуміти, як працює ваша програма та де знаходяться вузькі місця. Існує ряд інструментів APM, які можна використовувати з Flight.
- official flightphp/apm - Flight APM - це проста бібліотека APM, яку можна використовувати для моніторингу ваших додатків Flight. Її можна використовувати для моніторингу продуктивності вашої програми та допомогти вам виявити вузькі місця.
Асинхронність
Flight вже є швидким фреймворком, але додавання турбо-двигуна робить все ще веселішим (і складнішим)!
- flightphp/async - Офіційна асинхронна бібліотека Flight. Ця бібліотека є простим способом додати асинхронну обробку до вашої програми. Вона використовує Swoole/Openswoole під капотом для надання простого та ефективного способу виконання завдань асинхронно.
Авторизація/Дозволи
Авторизація та дозволи є критично важливими для будь-якої програми, яка потребує контролю за тим, хто може отримати доступ до чого.
- official flightphp/permissions - Офіційна бібліотека дозволів Flight. Ця бібліотека є простим способом додати дозволи на рівні користувача та програми до вашої програми.
Аутентифікація
Аутентифікація є необхідною для програм, які потребують перевірки ідентичності користувача та захисту кінцевих точок API.
- firebase/php-jwt - Бібліотека JSON Web Token (JWT) для PHP. Простий і безпечний спосіб реалізації аутентифікації на основі токенів у ваших додатках Flight. Ідеально підходить для stateless API аутентифікації, захисту маршрутів за допомогою проміжного програмного забезпечення та реалізації потоків авторизації у стилі OAuth.
Кешування
Кешування - це чудовий спосіб прискорити роботу вашої програми. Існує ряд бібліотек кешування, які можна використовувати з Flight.
- official flightphp/cache - Легка, проста і самостійна PHP-бібліотека кешування у файлах
CLI
CLI-додатки - це чудовий спосіб взаємодії з вашою програмою. Ви можете використовувати їх для генерації контролерів, відображення всіх маршрутів тощо.
- official flightphp/runway - Runway - це CLI-додаток, який допомагає вам керувати вашими додатками Flight.
Cookies
Cookies - це чудовий спосіб зберігати невеликі фрагменти даних на стороні клієнта. Їх можна використовувати для зберігання уподобань користувача, налаштувань програми тощо.
- overclokk/cookie - PHP Cookie - це PHP-бібліотека, яка забезпечує простий і ефективний спосіб керування cookies.
Налагодження
Налагодження є критично важливим при розробці у вашому локальному середовищі. Є кілька плагінів, які можуть покращити ваш досвід налагодження.
- tracy/tracy - Це повноцінний обробник помилок, який можна використовувати з Flight. Він має ряд панелей, які допоможуть вам налагодити вашу програму. Його також дуже легко розширювати та додавати власні панелі.
- official flightphp/tracy-extensions - Використовується з обробником помилок Tracy, цей плагін додає кілька додаткових панелей для налагодження конкретно для проектів Flight.
Бази Даних
Бази даних є основою більшості додатків. Це те, як ви зберігаєте та отримуєте дані. Деякі бібліотеки баз даних є просто обгортками для написання запитів, а деякі - повноцінними ORM.
- official flightphp/core SimplePdo - Офіційний помічник PDO Flight, який є частиною ядра. Це сучасна обгортка з зручними методами-помічниками, такими як
insert(),update(),delete()таtransaction()для спрощення операцій з базою даних. Всі результати повертаються як Collections для гнучкого доступу у вигляді масиву/об'єкта. Не ORM, а просто кращий спосіб роботи з PDO. - deprecated flightphp/core PdoWrapper - Офіційна обгортка PDO Flight, яка є частиною ядра (застаріла з версії v3.18.0). Використовуйте SimplePdo замість неї.
- official flightphp/active-record - Офіційний Flight ActiveRecord ORM/Mapper. Чудова маленька бібліотека для легкого отримання та збереження даних у вашій базі даних.
- byjg/php-migration - Плагін для відстеження всіх змін бази даних у вашому проекті.
- knifelemon/easy-query - Легкий, плавний конструктор SQL-запитів, який генерує SQL та параметри для підготовлених операторів. Добре працює з SimplePdo.
Шифрування
Шифрування є критично важливим для будь-якої програми, яка зберігає конфіденційні дані. Шифрування та розшифрування даних не є дуже складним, але правильне зберігання ключа шифрування може бути складним. Найважливіше - ніколи не зберігати ваш ключ шифрування у публічному каталозі або додавати його до вашого репозиторію коду.
- defuse/php-encryption - Це бібліотека, яку можна використовувати для шифрування та розшифрування даних. Почати роботу досить просто, щоб почати шифрувати та розшифровувати дані.
Електронна Пошта
Надсилання електронної пошти — базова потреба більшості веб-додатків: вітальні листи, скидання пароля, сповіщення. Ці бібліотеки роблять це безболісно і водночас зберігають надійну доставлюваність.
- ryanstubbs/flightmail - FlightMail обгортає Symfony Mailer зручним fluent API у стилі Flight. Надсилайте через SMTP або будь-якого великого провайдера за допомогою простих DSN-рядків, спрямовуйте різні провайдери для кожного повідомлення та формуйте тіла листів шаблонами Twig або Latte. Це неофіційний плагін для Flight, який не підтримується командою Flight.
Черга Завдань
Черги завдань справді корисні для асинхронної обробки завдань. Це може бути надсилання електронних листів, обробка зображень або будь-що, що не потрібно робити в реальному часі.
- n0nag0n/simple-job-queue - Simple Job Queue - це бібліотека, яку можна використовувати для асинхронної обробки завдань. Її можна використовувати з beanstalkd, MySQL/MariaDB, SQLite та PostgreSQL.
Сесії
Сесії не дуже корисні для API, але для побудови веб-додатку сесії можуть бути критично важливими для підтримки стану та інформації про вхід.
- official flightphp/session - Офіційна бібліотека сесій Flight. Це проста бібліотека сесій, яку можна використовувати для зберігання та отримання даних сесії. Вона використовує вбудовану обробку сесій PHP.
- Ghostff/Session - Менеджер сесій PHP (non-blocking, flash, segment, шифрування сесії). Використовує PHP open_ssl для опціонального шифрування/розшифрування даних сесії.
Шаблонізація
Шаблонізація є основою будь-якого веб-додатку з UI. Існує ряд шаблонізаторів, які можна використовувати з Flight.
- deprecated flightphp/core View - Це дуже базовий шаблонізатор, який є частиною ядра. Не рекомендується використовувати, якщо у вас більше кількох сторінок у вашому проекті.
- latte/latte - Latte - це повноцінний шаблонізатор, який дуже простий у використанні та відчувається ближче до синтаксису PHP, ніж Twig чи Smarty. Його також дуже легко розширювати та додавати власні фільтри та функції.
- twig/twig - Twig - це гнучкий, швидкий і безпечний шаблонізатор (той самий, що використовується Symfony). Інструменти ШІ та багато PHP-розробників добре його знають, він автоматично екранує вивід за замовчуванням і має величезну екосистему розширень.
- knifelemon/comment-template - CommentTemplate - це потужний PHP-шаблонізатор з компіляцією активів, успадкуванням шаблонів та обробкою змінних. Має автоматичну мініфікацію CSS/JS, кешування, кодування Base64 та опціональну інтеграцію з фреймворком Flight PHP.
Інтеграція з WordPress
Хочете використовувати Flight у вашому проекті WordPress? Для цього є зручний плагін!
- n0nag0n/wordpress-integration-for-flight-framework - Цей WordPress-плагін дозволяє запускати Flight поряд з WordPress. Ідеально підходить для додавання кастомних API, мікросервісів або навіть повних додатків до вашого сайту WordPress з використанням фреймворку Flight. Дуже корисний, якщо ви хочете найкращого з обох світів!
Внесок
Є плагін, яким ви хотіли б поділитися? Надішліть pull request, щоб додати його до списку!
Media
Медіа
Ми намагалися відстежити те, що можемо, з різних типів медіа в інтернеті щодо Flight. Дивіться нижче різні ресурси, які ви можете використовувати, щоб дізнатися більше про Flight.
Статті та огляд
- Unit Testing and SOLID Principles by Brian Fenton (2015?)
- PHP Web Framework Flight by ojambo (2025)
- Define, Generate, and Implement: An API-First Approach with OpenAPI Generator and FlightPHP by Daniel Schreiber (2025)
- Best PHP Micro Frameworks for 2024 by n0nag0n (2024)
- Creating a RESTful API with Flight Framework by n0nag0n (2024)
- Building a Simple Blog with Flight Part 2 by n0nag0n (2024)
- Building a Simple Blog with Flight Part 1 by n0nag0n (2024)
- 🚀 Build a Simple CRUD API in PHP with the Flight Framework by soheil-khaledabadi (2024)
- Building a PHP Web Application with the Flight Micro-framework by Arthur C. Codex (2023)
- Best PHP Frameworks for Web Development in 2024 by Ravikiran A S (2023)
- Top 12 PHP Frameworks: A Comprehensive Guide for 2023 by marketing kbk (2023)
- 5 PHP Frameworks You've (Probably) Never Heard of by n0nag0n (2022)
- 12 top PHP frameworks for web developers to consider in 2023 by Anna Monus (2022)
- The Best PHP Microframeworks on a Cloud Server by Shahzeb Ahmed (2021)
- PHP framework: Top 15 powerful ones for your web development by AHT Tech (2020)
- Easy PHP Routing with FlightPHP by Lucas Conceição (2019)
- Trying Out New PHP Framework (Flight) by Leon (2017)
- Setting up FlightPHP to work with Backbonejs by Timothy Tocci (2015)
Відео та посібники
- Build a Flight PHP App with MVC & MariaDB in 10 Minutes! (Beginner Friendly) by ojamboshop (2025)
- Create a REST API for IoT Devices Using PHP & FlightPHP - ESP32 API by IoT Craft Hub (2024)
- PHP Flight Framework Simple Introductory Video by n0nag0n (2024)
- Set header HTTP code in Flightphp (3 Solutions!!) by Roel Van de Paar (2024)
- PHP Flight Framework Tutorial. Super easy API Project! by n0nag0n (2022)
- Aplicación web CRUD con php y mysql y bootstrap usando flight by Devlopteca - Oscar Uh (2021)
- DevOps & SysAdmins: Lighttpd rewrite rule for Flight PHP microframework by Roel Van de Paar (2021)
- Tutorial REST API Flight PHP #PART2 INSERT TABLE Info #Code (Tagalog) by Info Singkat Official (2020)
- Tutorial REST API Flight PHP #PART1 Info #Code (Tagalog) by Info Singkat Official (2020)
- How To Create JSON REST API IN PHP - Part 2 by Codewife (2018)
- How To Create JSON REST API IN PHP - Part 1 by Codewife (2018)
- Teste Micro Frameworks PHP - Flight PHP, Lumen, Slim 3 e Laravel by Codemarket (2016)
- Tutorial 1 Flight PHP - Instalación by absagg (2014)
- Tutorial 2 Flight PHP - Route parte 1 by absagg (2014)
Чи чогось бракує?
Чи бракує нам чогось, що ви написали чи записали? Дайте нам знати за допомогою issue або pull request!
Examples
Потрібен швидкий старт?
У вас є два варіанти для початку роботи з новим проектом Flight:
- Full Skeleton Boilerplate: Більш повноцінний приклад з контролерами та views.
- Single File Skeleton Boilerplate: Один файл, який містить усе необхідне для запуску вашого додатка в одному простому файлі.
Приклади, надані спільнотою:
- flightravel: FlightPHP з директоріями Laravel, з інструментами PHP + GH Actions
- fleact - Стартер-кіт FlightPHP з інтеграцією ReactJS.
- flastro - Стартер-кіт FlightPHP з інтеграцією Astro.
- velt - Velt — це швидкий і простий шаблон стартера Svelte з бекендом FlightPHP.
- vite-flightphp - FlightPHP та сучасний фронтенд (Vite + Tailwind CSS) з підтримкою hot reload.
Потрібне натхнення?
Хоча ці приклади не є офіційно спонсорованими командою Flight, вони можуть дати вам ідеї щодо того, як структурувати ваші власні проекти, побудовані на Flight!
- ASC REST API Spell Checker - Легкий REST API для перевірки орфографії арабської мови, побудований на FlightPHP та бібліотеці ArPHP. Цей API надає можливості перевірки орфографії арабського тексту, включаючи виявлення помилково написаних слів та пропозиції виправлень.
- Eventify - Eventify — це односторінковий додаток, що з'єднує організаторів подій з учасниками. Побудований на PHP (FlightPHP), JavaScript та MySQL, він включає JWT автентифікацію, керування подіями та документацію RESTful API за допомогою OpenAPI.
- Ivox Car Rental - Ivox Car Rental — це односторінковий, мобільно-дружній веб-додаток для оренди автомобілів, побудований на PHP (FlightPHP), JavaScript та MySQL. Він підтримує реєстрацію користувачів, перегляд та бронювання автомобілів, тоді як адміністратори можуть керувати автомобілями, користувачами та бронюваннями. Додаток має REST API, JWT автентифікацію та адаптивний дизайн для сучасного досвіду оренди.
- Decay - Flight v3 з HTMX та SleekDB, все про зомбі! (Demo)
- Flight Example Blog - Flight v3 з Middleware, Controllers, Active Record та Latte.
- Flight CRUD RESTful API - Простий проект CRUD API з використанням фреймворку Flight, який надає базову структуру для нових користувачів, щоб швидко налаштувати PHP-додаток з операціями CRUD та підключенням до бази даних. Проект демонструє, як використовувати Flight для розробки RESTful API, роблячи його ідеальним інструментом навчання для початківців та корисним стартовим набором для більш досвідчених розробників.
- Flight School Management System - Flight v3
- Paste Bin with Comments - Flight v3
- Basic Skeleton App
- Example Wiki
- The IT-Innovator PHP Framework Application
- LittleEducationalCMS (Spanish)
- Italian Yellow Pages API
- Generic Content Management System (with....very little documentation)
- A tiny php framework based on Flight and medoo.
- Example MVC Application
- Production ready Flight Boilerplate - Готовий до виробництва фреймворк автентифікації, який заощаджує тижні розробки. Функції корпоративного рівня безпеки: 2FA/TOTP, інтеграція LDAP, Azure SSO, інтелектуальне обмеження швидкості, відбитки сесій, захист від brute-force, панель аналітики безпеки, всебічний аудит логування та гранульований контроль доступу на основі ролей.
Хочете поділитися своїм прикладом?
Якщо у вас є проект, яким ви хочете поділитися, будь ласка, надішліть pull request, щоб додати його до цього списку!
Install/install
Інструкції зі встановлення
Перш ніж встановити Flight, потрібні деякі базові передумови. Зокрема вам знадобиться:
- Встановити PHP на вашій системі
- Встановити Composer для найкращого досвіду розробника.
Базове встановлення
Якщо ви користуєтеся Composer, ви можете виконати наступну команду:
composer require flightphp/core
Це розмістить на вашій системі лише файли ядра Flight. Вам потрібно буде визначити структуру проєкту, макети, залежності, конфігурації, автозавантаження тощо. Цей метод гарантує, що жодні інші залежності, окрім Flight, не будуть встановлені.
Ви також можете завантажити файли безпосередньо та розпакувати їх у вашу вебдиректорію.
Базове встановлення чудово підходить для навчання, мікро API та експериментів із копіюванням і вставкою. Для повного макету застосунку, якого люди і AI інструменти для кодування можуть дотримуватися однаково, використовуйте рекомендований скелет нижче.
Рекомендоване встановлення
Настійно рекомендується починати з застосунку flightphp/skeleton для будь-яких нових проєктів. Встановлення дуже просте.
composer create-project flightphp/skeleton my-project/
cd my-project/
composer start
# необов'язкова демонстрація бази даних + пости
php runway migrate
Цей крок налаштовує структуру проєкту, автозавантаження Composer PSR-4, конфігурацію та інструменти, як-от Tracy, Tracy Extensions та Runway. Він також постачає кореневий файл AGENTS.md (та копії в межах app/), щоб AI-асистенти мали спільний макет із вами — див. AI та досвід розробника.
Що надає скелет
project-root/
├── AGENTS.md # Джерело істини для AI / агентів
├── SECURITY.md # Очікування щодо безпеки
├── .env.example # Секрети / накладення для розгортання (копіюється в .env)
├── public/index.php # Лише вебвхід
├── app/
│ ├── config/ # bootstrap, маршрути, сервіси, config_sample.php
│ ├── Controller/ # App\Controller\* (папка в PascalCase!)
│ ├── Middleware/ # App\Middleware\*
│ ├── Model/ # App\Model\* (ActiveRecord)
│ ├── Utils/ # Config, Env, DatabaseFactory
│ ├── commands/ # CLI-команди Runway
│ ├── views/ # Twig-шаблони (*.twig)
│ ├── cache/
│ └── log/
├── migrations/ # SQL-міграції (.sql / .mysql.sql)
└── tests/ # PHPUnit
Простори імен відповідають регістру папок. Composer зіставляє "App\\": "app/", отже:
| Шлях на диску | Простір імен |
|---|---|
app/Controller/HomeController.php |
App\Controller\HomeController |
app/Middleware/… |
App\Middleware\… |
app/Model/… |
App\Model\… |
app/Utils/… |
App\Utils\… |
На Linux app/controller/ не те саме, що app/Controller/. Автозавантаження чутливе до регістру — відповідайте папкам скелета в PascalCase. Деталі: Автозавантаження.
Типові складові стеку (нові проєкти): Twig-шаблони, SimplePdo + ActiveRecord, Dice з ін'єкцією Engine (надавайте перевагу відсутності Flight:: усередині класів застосунку), опційно SQLite після php runway migrate.
create-project зазвичай копіює app/config/config_sample.php → config.php та .env.example → .env, якщо вони присутні. Маршрути розташовані в app/config/routes.php; сервіси та DI — у app/config/services.php.
Документація ↔ скелет: Ця документація навчає API Flight (часто з короткими прикладами
Flight::). Скелет визначає форму застосунку. Коли додаєте код уapp/, дотримуйтеся дерева скелета; використовуйте документацію для назв методів, параметрів і плагінів.
Налаштування вебсервера
Вбудований PHP-сервер розробки
Це найпростіший спосіб почати роботу. Ви можете використовувати вбудований сервер для запуску застосунку і навіть використовувати SQLite як базу даних (за умови, що sqlite3 встановлено у вашій системі) без особливих додаткових вимог! Просто виконайте наступну команду після встановлення PHP:
php -S localhost:8000
# або зі скелетним застосунком
composer start
Потім відкрийте браузер і перейдіть на http://localhost:8000.
Якщо ви хочете зробити коренем документів вашого проєкту іншу директорію (Наприклад: ваш проєкт знаходиться в ~/myproject, але ваш корінь документів — ~/myproject/public/), ви можете виконати наступну команду, перебуваючи в директорії ~/myproject:
php -S localhost:8000 -t public/
# зі скелетним застосунком це вже налаштовано
composer start
Потім відкрийте браузер і перейдіть на http://localhost:8000.
Apache
Переконайтеся, що Apache вже встановлено у вашій системі. Якщо ні, загугліть, як встановити Apache у вашій системі.
Для Apache відредагуйте ваш файл .htaccess наступним чином:
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [QSA,L]
Примітка: Якщо вам потрібно використовувати flight у піддиректорії, додайте рядок
RewriteBase /subdir/одразу післяRewriteEngine On.
Примітка: Якщо ви хочете захистити всі серверні файли, наприклад файл бази даних або
.env. Помістіть це у ваш файл.htaccess:
RewriteEngine On
RewriteRule ^(.*)$ index.php
Nginx
Переконайтеся, що Nginx вже встановлено у вашій системі. Якщо ні, загугліть, як встановити Nginx у вашій системі.
Для Nginx додайте наступне до оголошення вашого сервера:
server {
location / {
try_files $uri $uri/ /index.php;
}
}
Створіть ваш файл index.php
Якщо ви виконуєте базове встановлення, вам знадобиться трохи коду для початку.
<?php
// Якщо ви використовуєте Composer, підключіть автозавантажувач.
require 'vendor/autoload.php';
// якщо ви не використовуєте Composer, завантажте фреймворк безпосередньо
// require 'flight/Flight.php';
// Потім визначте маршрут і призначте функцію для обробки запиту.
Flight::route('/', function () {
echo 'hello world!';
});
// Нарешті, запустіть фреймворк.
Flight::start();
Зі скелетним застосунком публічна точка входу лише запускає застосунок. Маршрути реєструються в app/config/routes.php (зазвичай [App\Controller\…::class, 'method'], щоб Dice міг впроваджувати залежності). Сервіси, Twig, SimplePdo та контейнер налаштовані в app/config/services.php. Ця структура є навмисною, щоб AI-інструменти та люди редагували одні й ті самі місця щоразу.
Встановлення PHP
Якщо у вашій системі вже встановлено php, можете пропустити ці інструкції та перейти до розділу завантаження
macOS
Встановлення PHP за допомогою Homebrew
-
Встановіть Homebrew (якщо ще не встановлено):
- Відкрийте Terminal і виконайте:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
- Відкрийте Terminal і виконайте:
-
Встановіть PHP:
- Встановіть останню версію:
brew install php - Щоб встановити конкретну версію, наприклад, PHP 8.1:
brew tap shivammathur/php brew install shivammathur/php/php@8.1
- Встановіть останню версію:
-
Перемикання між версіями PHP:
- Видаліть поточну версію зі зв'язку та зв'яжіть бажану версію:
brew unlink php brew link --overwrite --force php@8.1 - Перевірте встановлену версію:
php -v
- Видаліть поточну версію зі зв'язку та зв'яжіть бажану версію:
Windows 10/11
Ручне встановлення PHP
-
Завантажте PHP:
- Відвідайте PHP для Windows та завантажте останню або конкретну версію (наприклад, 7.4, 8.0) як zip-файл без потокової безпеки (non-thread-safe).
-
Розпакуйте PHP:
- Розпакуйте завантажений zip-файл у
C:\php.
- Розпакуйте завантажений zip-файл у
-
Додайте PHP до системного PATH:
- Перейдіть до System Properties > Environment Variables.
- У розділі System variables знайдіть Path і натисніть Edit.
- Додайте шлях
C:\php(або куди ви розпакували PHP). - Натисніть OK, щоб закрити всі вікна.
-
Налаштуйте PHP:
- Скопіюйте
php.ini-developmentуphp.ini. - Відредагуйте
php.ini, щоб налаштувати PHP за потреби (наприклад, встановитиextension_dir, увімкнути розширення).
- Скопіюйте
-
Перевірте встановлення PHP:
- Відкрийте Command Prompt і виконайте:
php -v
- Відкрийте Command Prompt і виконайте:
Встановлення кількох версій PHP
-
Повторіть наведені вище кроки для кожної версії, розміщуючи кожну в окремій директорії (наприклад,
C:\php7,C:\php8). -
Перемикайтеся між версіями, змінюючи системну змінну PATH, щоб вказувати на бажану директорію версії.
Ubuntu (20.04, 22.04 тощо)
Встановлення PHP за допомогою apt
-
Оновіть списки пакетів:
- Відкрийте Terminal і виконайте:
sudo apt update
- Відкрийте Terminal і виконайте:
-
Встановіть PHP:
- Встановіть останню версію PHP:
sudo apt install php - Щоб встановити конкретну версію, наприклад, PHP 8.1:
sudo apt install php8.1
- Встановіть останню версію PHP:
-
Встановіть додаткові модулі (необов'язково):
- Наприклад, щоб встановити підтримку MySQL:
sudo apt install php8.1-mysql
- Наприклад, щоб встановити підтримку MySQL:
-
Перемикання між версіями PHP:
- Використовуйте
update-alternatives:sudo update-alternatives --set php /usr/bin/php8.1
- Використовуйте
-
Перевірте встановлену версію:
- Виконайте:
php -v
- Виконайте:
Rocky Linux
Встановлення PHP за допомогою yum/dnf
-
Увімкніть сховище EPEL:
- Відкрийте Terminal і виконайте:
sudo dnf install epel-release
- Відкрийте Terminal і виконайте:
-
Встановіть сховище Remi:
- Виконайте:
sudo dnf install https://rpms.remirepo.net/enterprise/remi-release-8.rpm sudo dnf module reset php
- Виконайте:
-
Встановіть PHP:
- Щоб встановити версію за замовчуванням:
sudo dnf install php - Щоб встановити конкретну версію, наприклад, PHP 7.4:
sudo dnf module install php:remi-7.4
- Щоб встановити версію за замовчуванням:
-
Перемикання між версіями PHP:
- Використовуйте команду модуля
dnf:sudo dnf module reset php sudo dnf module enable php:remi-8.0 sudo dnf install php
- Використовуйте команду модуля
-
Перевірте встановлену версію:
- Виконайте:
php -v
- Виконайте:
Загальні примітки
- Для середовищ розробки важливо налаштувати параметри PHP відповідно до вимог вашого проєкту.
- Під час перемикання версій PHP переконайтеся, що всі необхідні розширення PHP встановлені для конкретної версії, яку ви збираєтеся використовувати.
- Перезапустіть ваш вебсервер (Apache, Nginx тощо) після перемикання версій PHP або оновлення конфігурацій, щоб застосувати зміни.
Guides
Посібники
Flight PHP створено для того, щоб бути простим, але потужним, і наші посібники допоможуть вам будувати реальні додатки крок за кроком. Ці практичні навчальні матеріали проведуть вас через повні проекти, щоб продемонструвати, як Flight можна використовувати ефективно.
Офіційні посібники
Будування блогу
Дізнайтеся, як створити функціональний блог-додаток за допомогою Flight PHP. Цей посібник проведе вас через:
- Налаштування структури проекту
- Роботу з шаблонами за допомогою Latte
- Реалізацію маршрутів для публікацій
- Зберігання та отримання даних
- Обробку надсилань форм
- Основну обробку помилок
Цей навчальний матеріал ідеальний для початківців, які хочуть побачити, як усі елементи поєднуються в реальному додатку.
Юніт-тестування та принципи SOLID
Цей посібник охоплює основи юніт-тестування в додатках Flight PHP. Він включає:
- Налаштування PHPUnit
- Написання тестуваного коду за допомогою принципів SOLID
- Мокування залежностей
- Поширені помилки, яких слід уникати
- Масштабування ваших тестів під час зростання додатку Цей навчальний матеріал ідеальний для розробників, які хочуть покращити якість коду та його підтримуваність.
Неофіційні посібники
Хоча ці посібники не підтримуються офіційно командою Flight, вони є цінними ресурсами, створеними спільнотою. Вони охоплюють різні теми та випадки використання, надаючи додаткові ідеї щодо використання Flight PHP.
Creating a RESTful API with Flight Framework
Цей посібник проведе вас через створення RESTful API за допомогою фреймворку Flight PHP. Він охоплює основи налаштування API, визначення маршрутів та повернення JSON-відповідей.
Building a Simple Blog
Цей посібник проведе вас через створення базового блогу за допомогою фреймворку Flight PHP. Насправді він має 2 частини: одну для основ та іншу для більш просунутих тем і вдосконалень для блогу, готового до виробництва.
- Building a Simple Blog with Flight - Part 1 - Початок роботи з простим блогом.
- Building a Simple Blog with Flight - Part 2 - Вдосконалення блогу для виробництва.
Building a Pokémon API in PHP: A Beginner's Guide
Цей веселий посібник проведе вас через створення простого API для Pokémon за допомогою Flight PHP. Він охоплює основи налаштування API, визначення маршрутів та повернення JSON-відповідей.
Співпраця
Маєте ідею для посібника? Знайшли помилку? Ми вітаємо внесок! Наші посібники підтримуються в репозиторії документації FlightPHP.
Якщо ви створили щось цікаве за допомогою Flight і хочете поділитися цим як посібником, будь ласка, надішліть запит на злиття. Поділ вашого знання допомагає спільноті Flight розвиватися.
Шукаєте документацію API?
Якщо ви шукаєте конкретну інформацію про основні функції та методи Flight, перегляньте розділ Learn нашої документації.