Learn/flight_vs_laravel

Flight vs Laravel

Що таке Laravel?

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

Існує також так багато модулів Laravel, що розробники часто відчувають, ніби єдиний спосіб вирішити проблеми — це через ці модулі, тоді як насправді ви могли б просто використати іншу бібліотеку або написати власний код.

Переваги порівняно з Flight

Недоліки порівняно з Flight

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

Нижче наведено перелік усіх доступних параметрів конфігурації:

Конфігурація завантажувача

Існує також додатковий параметр конфігурації для завантажувача. Він дозволяє автоматично завантажувати класи з _ у назві класу.

// Увімкнути завантаження класів із підкресленнями
// За замовчуванням true
Loader::$v2ClassLoading = false;

Пам'ятайте, що автозавантаження також залежить від регістру папок, що відповідає вашим просторам імен — особливо зі структурою skeleton'а App\ + app/Controller/.

Конфігурація проєкту та .env (шаблон skeleton)

Ядро Flight не вимагає файлів .env. Багато застосунків використовують лише PHP-масив конфігурації. Офіційний skeleton розшаровує конфігурацію, щоб секрети залишалися поза git, а Runway міг безпечно перезаписувати літеральну конфігурацію:

  1. .env / реальне середовище — секрети та перевизначення для розгортання (ігнорується git).
  2. app/config/config.php — літеральні PHP-масиви за замовчуванням (копіюється з config_sample.php). Бажано не використовувати вирази $_ENV[...] у цьому файлі: такі інструменти, як runway config:set, можуть перезаписати його статичними значеннями та вбудувати секрети у файл.
  3. Об'єднання під час завантаження — 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 () {
  // Обробка випадку, коли не знайдено
});

Дивіться також

Усунення неполадок

Журнал змін

Learn/ai

AI та досвід розробника з Flight

Огляд

Flight створено для роботи з інструментами AI-кодування, а не проти них. Невеликий, передбачуваний API, чітка структура застосунку в офіційному скелеті та інструкційні файли, специфічні для проєкту, означають, що асистенти, як-от GitHub Copilot, Cursor, Windsurf, Claude Code і Gemini, можуть дотримуватися тих самих шаблонів, які ви написали б вручну.

Завдяки вбудованим командам Runway для підключення до LLM-провайдерів і генерації інструкцій проєкту, Flight допомагає вам і вашій команді отримувати послідовну та релевантну допомогу без повторного вставлення одного й того ж контексту в кожний чат.

Розуміння

AI-асистенти для написання коду є найкориснішими, коли вони розуміють контекст вашого проєкту, його угоди та цілі. AI-помічники Flight дозволяють вам:

Ці функції постачаються з ядром 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

Вам буде запропоновано:

Це створює облікові дані, які використовуються для подальших 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-провайдера для генерації інструкцій і записує їх переважно до:

Залежно від версії 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.

Поглиблене використання

Дивіться також

Усунення неполадок

Журнал змін

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 щоразу, коли ми викликаємо функцію, пов'язану з датою/часом. Ось деякі рекомендовані налаштування:

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 Що робить хороший одиничний тест

Хороші одиничні тести мають багато таких характеристик:

Є причини йти проти деяких з цих, але як загальні рекомендації вони послужать вам добре.

5.5 Коли тестування боляче

Одиничне тестування змушує вас відчути біль поганого дизайну спереду - Michael Feathers

Коли ви пишете одиничні тести, ви змушуєте себе фактично використовувати клас для досягнення речей. Якщо ви пишете тести в кінці, або, що гірше, просто кидаєте код через стіну для QA чи когось, щоб написати тести, ви не отримуєте жодного зворотного зв'язку про те, як клас фактично поводиться. Якщо ми пишемо тести, і клас є справжнім болем для використання, ми дізнаємося про це, коли пишемо його, що майже найдешевший час, щоб виправити це.

Якщо клас важко тестувати, це дефект дизайну. Різні дефекти проявляються по-різному, хоча. Якщо вам потрібно робити багато моків, ваш клас, ймовірно, має забагато залежностей, або ваші методи роблять забагато. Чим більше налаштування ви повинні робити для кожного тесту, тим більше ймовірно, що ваші методи роблять забагато. Якщо вам потрібно писати дуже заплутані сценарії тестів, щоб перевірити поведінку, методи класу, ймовірно, роблять забагато. Якщо вам потрібно копати всередині купи приватних методів та стану, щоб тестувати речі, можливо, є інший клас, який намагається вийти. Одиничне тестування дуже добре розкриває "айсбергові класи", де 80% того, що робить клас, приховано в захищеному або приватному коді. Я колись був великим шанувальником робити якомога більше захищеним, але тепер зрозумів, що я просто робив свої індивідуальні класи відповідальними за забагато, і справжнє рішення було розділити клас на менші шматки.

Написано Браяном Фентоном - Браян Фентон є розробником PHP протягом 8 років у Середньому Заході та районі затоки, зараз у Thismoment. Він зосереджується на майстерності коду та принципах дизайну. Блог на www.brianfenton.us, Twitter на @brianfenton. Коли він не зайнятий тим, щоб бути татом, він насолоджується їжею, пивом, іграми та навчанням.

Learn/security

Безпека

Огляд

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

Офіційний скелет також містить спеціальний SECURITY.md і проміжне програмне забезпечення для заголовків безпеки, щоб AI-інструменти для кодування (і люди) мали одне продумане місце для секретів, заголовків і правил XSS/SQL — окремо від загального стилю кодування в AGENTS.md.

Розуміння

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

Шаблони допомагають із 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);
// Це виведе: &lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;

// 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); // це виведе всіх користувачів у базі даних, а не лише одне ім'я користувача

Секрети та конфігурація

Перевірка зворотного виклику 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 секунд
});

Дивіться також

Усунення неполадок

Журнал змін

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-запити

Flight::route('GET /info', function() {
    echo 'This is some info!';
});
// HEAD-запит до /info поверне ті самі заголовки, але без тіла.

OPTIONS-запити

OPTIONS-запити автоматично обробляються Flight для будь-якого визначеного маршруту.

// Для маршруту, визначеного як:
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');
});

Перегляд інформації про маршрут

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

  1. Використати властивість executedRoute на об’єкті Flight::router().
  2. Попросити передати об’єкт маршруту у ваш зворотний виклик, передавши 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
]);

Дивіться також

Усунення неполадок

404 Not Found або неочікувана поведінка маршруту

Якщо ви бачите помилку 404 Not Found (але ви присягаєтеся, що маршрут справді існує, і це не помилка), насправді це може бути проблемою з тим, що ви повертаєте значення у кінцевій точці маршруту, а не просто виводите його. Причина цього навмисна, але може стати несподіванкою для деяких розробників.

Flight::route('/hello', function(){
    // Це може спричинити помилку 404 Not Found
    return 'Hello World';
});

// Ймовірно, ви хочете так
Flight::route('/hello', function(){
    echo 'Hello World';
});

Причина в тому, що в маршрутизатор вбудовано спеціальний механізм, який трактує повернений результат як сигнал «перейти до наступного маршруту». Цю поведінку задокументовано в розділі Маршрутизація.

Журнал змін

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 це означає перевірку того, як ваші маршрути, контролери та логіка реагують на різні вхідні дані — без залежності від глобального стану чи реальних зовнішніх сервісів.

Ключові принципи:

Базове використання

Налаштування PHPUnit

  1. Встановіть PHPUnit за допомогою Composer:
    composer require --dev phpunit/phpunit
  2. Створіть каталог tests у корені вашого проєкту.
  3. Додайте тестовий скрипт до вашого composer.json:
    "scripts": {
        "test": "phpunit --configuration phpunit.xml"
    }
  4. Створіть файл 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']);
    }
}

Поради:

Використання впровадження залежностей для тестованих контролерів

Впроваджуйте залежності (наприклад, базу даних або поштовий сервіс) у ваші контролери, щоб їх було легко імітувати в тестах:

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);
    }
}

Просунуте використання

Дивіться також

Усунення неполадок

Журнал змін

Learn/flight_vs_symfony

Flight vs Symfony

Що таке Symfony?

Symfony — це набір повторно використовуваних компонентів PHP і PHP фреймворк для веб-проектів.

Стандартна основа, на якій побудовані найкращі PHP додатки. Виберіть будь-який з 50 автономних компонентів, доступних для ваших власних додатків.

Прискорте створення та підтримку ваших PHP веб-додатків. Припиніть повторювані завдання кодування і насолоджуйтесь можливістю контролювати свій код.

Плюси в порівнянні з Flight

Мінуси в порівнянні з Flight

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();
});

Дивіться також

Вирішення проблем

Журнал змін

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, але це все одно виконує роботу з тими самими перевагами!

Дивіться також

Усунення неполадок

Журнал змін

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 автентифікації, і ви хочете перенаправити користувача на сторінку входу, якщо він не автентифікований. У вас є кілька варіантів:

  1. Ви можете повернути false з функції middleware, і Flight автоматично поверне помилку 403 Forbidden, але без кастомізації.
  2. Ви можете перенаправити користувача на сторінку входу за допомогою Flight::redirect().
  3. Ви можете створити власну помилку в 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.']);
        }
    }
}

Дивіться також

Вирішення проблем

Changelog

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 для отримання додаткової інформації.

Див. також

Вирішення проблем

Журнал змін

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'], якщо він існує.

Властивості об'єкта запиту

Об'єкт запиту надає такі властивості:

Допоміжні методи

Є кілька допоміжних методів для складання частин 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.

Див. також

Вирішення проблем

Журнал змін

Learn/why_frameworks

Чому фреймворк?

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

Причини використання фреймворка

Ось кілька причин, чому ви можете хотіти розглянути можливість використання фреймворка:

Flight — це мікро-фреймворк. Це означає, що він невеликий і легкий. Він не надає такої ж функціональності, як більші фреймворки, такі як Laravel або Symfony. Однак він забезпечує багато функціональності, яку ви потребуєте для створення веб-додатків. Його також легко навчитися і використовувати. Це робить його хорошим вибором для швидкого і легкого створення веб-додатків. Якщо ви новачок у фреймворках, Flight є чудовим початковим фреймворком для старту. Він допоможе вам дізнатися про переваги використання фреймворків, не перевантажуючи вас занадто великою складністю. Після того, як ви отримаєте деякий досвід з Flight, вам буде легше перейти на більш складні фреймворки, такі як Laravel або Symfony, однак Flight все ще може скласти успішну надійну програму.

Що таке маршрутизація?

Маршрутизація — це основа фреймворка Flight, але що це таке насправді? Маршрутизація — це процес взяття URL-адреси і зіставлення її з конкретною функцією у вашому коді. Це спосіб, яким ви можете змусити ваш веб-сайт виконувати різні дії в залежності від запитуваної URL-адреси. Наприклад, ви можете захотіти показати профіль користувача, коли вони відвідують /user/1234, але показати список всіх користувачів, коли вони відвідують /users. Це все робиться через маршрутизацію.

Це може працювати приблизно так:

І чому це важливо?

Наявність належного централізованого маршрутизатора може значно спростити ваше життя! Це може бути важко зрозуміти на перший погляд. Ось кілька причин, чому:

Я впевнений, що ви знайомі з методом "скрипт за скриптом" для створення веб-сайту. У вас може бути файл під назвою 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');
});

Дивіться також

Вирішення проблем

Журнал змін

Learn/events

Менеджер подій

станом на v3.15.0

Огляд

Події дозволяють реєструвати та активувати власну поведінку у вашому додатку. З додаванням Flight::onEvent() та Flight::triggerEvent(), ви тепер можете підключатися до ключових моментів життєвого циклу вашого додатку або визначати власні події (наприклад, сповіщення та email) для того, щоб зробити ваш код більш модульним та розширюваним. Ці методи є частиною mappable methods у Flight, що означає, що ви можете перевизначити їхню поведінку відповідно до ваших потреб.

Розуміння

Події дозволяють розділяти різні частини вашого додатку, щоб вони не залежали надто сильно одна від одної. Це розділення — часто називається розв’язанням залежностей — робить ваш код легшим для оновлення, розширення або налагодження. Замість того, щоб писати все в одному великому блоці, ви можете розбити вашу логіку на менші, незалежні частини, які реагують на конкретні дії (події).

Уявіть, що ви будуєте додаток для блогу:

Без подій ви б запхали все це в одну функцію. З подіями ви можете розбити це: одна частина зберігає коментар, інша активує подію на кшталт 'comment.posted', а окремі слухачі обробляють email та логування. Це робить ваш код чистішим та дозволяє додавати або видаляти функції (наприклад, сповіщення) без дотику до основної логіки.

Поширені випадки використання

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

Однак уявіть, що у вас є функція "забули пароль". Це повинно бути частиною вашої основної функціональності, а не подією, бо якщо той email не буде надісланий, користувач не зможе скинути пароль та використовувати ваш додаток.

Основне використання

Система подій у Flight побудована навколо двох основних методів: Flight::onEvent() для реєстрації слухачів подій та Flight::triggerEvent() для активації подій. Ось як ви можете їх використовувати:

Реєстрація слухачів подій

Щоб слухати подію, використовуйте Flight::onEvent(). Цей метод дозволяє вам визначити, що повинно відбуватися, коли подія виникає.

Flight::onEvent(string $event, callable $callback): void

Ви "підписуєтеся" на подію, повідомляючи 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

Простий приклад

$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();

Опція 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

Порада: Групуйте за призначенням

У 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 оновлена.";
});

Чому це корисно: Код редагування не турбується про кешування — він просто сигналізує оновлення. Інші частини додатку можуть реагувати за потреби.

Найкращі практики

Система подій у Flight PHP з Flight::onEvent() та Flight::triggerEvent() дає вам простий, але потужний спосіб будувати гнучкі додатки. Дозволяючи різним частинам додатку спілкуватися через події, ви можете тримати код організованим, повторно використовуваним та легким для розширення. Чи то логування дій, надсилання сповіщень, чи керування оновленнями, події допомагають робити це без заплутування логіки. Плюс, з можливістю перевизначення цих методів, у вас є свобода налаштувати систему під ваші потреби. Почніть з однієї події та спостерігайте, як це трансформує структуру вашого додатку!

Вбудовані події

Flight PHP має кілька вбудованих подій, які ви можете використовувати для підключення до життєвого циклу фреймворку. Ці події активуються в конкретних точках циклу запит/відповідь, дозволяючи виконувати власну логіку, коли певні дії відбуваються.

Список вбудованих подій

Дивіться також

Вирішення проблем

Журнал змін

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!

Дивіться також

Усунення проблем

Журнал змін

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 є простою:

  1. Оновіть вашу реєстрацію:

    // Старий
    Flight::register('db', \flight\database\PdoWrapper::class, [ /* ... */ ]);
    
    // Новий
    Flight::register('db', \flight\database\SimplePdo::class, [ /* ... */ ]);
  2. Усі існуючі методи PdoWrapper працюють у SimplePdo - Немає руйнівних змін. Ваш існуючий код продовжить працювати.

  3. Опціонально використовуйте нові методи-помічники - Почніть використовувати insert(), update(), delete() та transaction(), щоб спростити ваш код.

Дивіться також

Вирішення проблем

Журнал змін

Learn/collections

Колекції

Огляд

Клас Collection у Flight — це зручний інструмент для керування наборами даних. Він дозволяє отримувати доступ до даних і маніпулювати ними, використовуючи як синтаксис масивів, так і синтаксис об'єктів, що робить ваш код чистішим і гнучкішим.

Розуміння

Collection — це, по суті, обгортка навколо масиву, але з додатковими можливостями. Ви можете використовувати її як масив, перебирати її, підраховувати елементи та навіть звертатися до елементів як до властивостей об'єкта. Це особливо корисно, коли потрібно передавати структуровані дані у вашому застосунку або коли ви хочете зробити код трохи зрозумілішим.

Колекції реалізують кілька інтерфейсів PHP:

Базове використання

Створення колекції

Ви можете створити колекцію, просто передавши масив у її конструктор:

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']);

Колекції особливо корисні, коли потрібно передавати структуровані дані між компонентами або коли ви хочете забезпечити більш об'єктно-орієнтований інтерфейс до даних масиву.

Дивіться також

Усунення неполадок

Журнал змін

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

Недоліки порівняно з Flight

Learn/extending

Розширення

Огляд

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

Розуміння

Існує 2 способи, якими ви можете розширити функціональність Flight:

  1. Відображення методів - Це використовується для створення простих власних методів, які ви можете викликати з будь-якого місця у вашому додатку. Ці методи зазвичай використовуються для утилітарних функцій, які ви хочете викликати з будь-якого місця у вашому коді.
  2. Реєстрація класів - Це використовується для реєстрації власних класів у 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?

Див. також

Вирішення проблем

Журнал змін

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);

Дивіться також

Вирішення проблем

Журнал змін

Learn/flight_vs_slim

Flight проти Slim

Що таке Slim?

Slim — це PHP мікрофреймворк, який допомагає швидко створювати прості, але потужні веб-додатки та API.

Значна частина натхнення для деяких функцій v3 у Flight насправді прийшла від Slim. Групування маршрутів та виконання middleware у певному порядку — це дві функції, натхненні Slim. Slim v3 вийшов орієнтованим на простоту, але щодо v4 є суперечливі відгуки.

Переваги порівняно з Flight

Недоліки порівняно з Flight

Learn/autoloading

Автозавантаження

Огляд

Автозавантаження — це концепція в PHP, коли ви вказуєте каталог або каталоги для завантаження класів. Це набагато корисніше, ніж використання require або include для завантаження класів. Це також вимога для використання пакетів Composer.

Правильне налаштування автозавантаження важливе і для розробки з підтримкою AI: агенти розміщують файли там, куди вказує простір імен. Якщо регістр папки та простору імен не збігаються, на Linux з'являтимуться помилки "клас не знайдено", навіть якщо все "працювало" на нечутливому до регістру диску Mac.

Розуміння

За замовчуванням будь-який клас Flight автозавантажується автоматично завдяки Composer. Для ваших класів застосунку є два поширені підходи:

  1. Composer PSR-4 (те, що використовує офіційний скелет): зіставте префікс простору імен з каталогом у composer.json, потім виконайте composer dump-autoload.
  2. Flight::path(): вкажіть завантажувачу Flight каталоги (зручно для простих застосунків або коли ви не використовуєте Composer для коду застосунку).

Використання автозавантажувача значно спрощує ваш код. Замість стіни include / require на початку кожного файлу класи завантажуються, коли ви вперше їх використовуєте.

Чутливість до регістру (прочитайте двічі)

Простори імен мають збігатися зі структурою каталогів і регістром літер цих каталогів.

Працює Ламається на Linux
App\Controller\HomeControllerapp/Controller/HomeController.php App\Controller\… з папкою app/controllers/
app\controllers\MyControllerapp/controllers/MyController.php Змішування App\ з нижнім регістром controllers

Простори імен PHP не чутливі до регістру в деяких контекстах, але Composer і файлова система — ні. Офіційний скелет стандартизує так:

У старішій документації та прикладах спільноти іноді використовувався нижній регістр 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() {
        // щось робимо
    }
}

Дивіться також

Виправлення неполадок

Клас не знайдено (автозавантаження не працює)

Це може статися з кількох причин. Нижче наведено кілька прикладів.

Неправильна назва файлу

Найпоширеніша причина — назва класу не збігається з назвою файлу.

Якщо у вас є клас з назвою 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() для контролерів і моделей там.

Журнал змін

Learn/uploaded_file

Обробник Завантаженого Файлу

Огляд

Клас UploadedFile у Flight полегшує та робить безпечним обробку завантаження файлів у вашому додатку. Він обгортає деталі процесу завантаження файлів PHP, надаючи вам простий, об'єктно-орієнтований спосіб доступу до інформації про файл та переміщення завантажених файлів.

Розуміння

Коли користувач завантажує файл через форму, PHP зберігає інформацію про файл у суперглобальній змінній $_FILES. У Flight ви рідко взаємодієте з $_FILES безпосередньо. Натомість об'єкт Request у Flight (доступний через Flight::request()) надає метод getUploadedFiles(), який повертає масив об'єктів UploadedFile, роблячи обробку файлів набагато зручнішою та надійнішою.

Клас UploadedFile надає методи для:

Цей клас допомагає уникнути поширених помилок з завантаженням файлів, таких як обробка помилок чи безпечне переміщення файлів.

Основне Використання

Доступ до Завантажених Файлів з Запиту

Рекомендований спосіб доступу до завантажених файлів — через об'єкт запиту:

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 "Виникла помилка під час завантаження файлу.";
}

Дивіться Також

Вирішення Проблем

Журнал Змін

Guides/unit_testing

Unit-тестування у Flight PHP з PHPUnit

Цей посібник знайомить з unit-тестуванням у Flight PHP за допомогою PHPUnit, і розрахований на початківців, які хочуть зрозуміти чому unit-тестування важливе та як застосовувати його на практиці. Ми зосередимось на тестуванні поведінки — перевірці, що ваш застосунок робить те, що ви очікуєте, наприклад, надсилає електронний лист або зберігає запис, а не на тривіальних обчисленнях. Ми почнемо з простого обробника маршрутів і перейдемо до складнішого контролера, використовуючи впровадження залежностей (DI) та макетування сторонніх сервісів.

Чому unit-тестування?

Unit-тестування гарантує, що ваш код поводиться очікувано, виявляючи помилки до того, як вони потрапляють у продакшн. Воно особливо корисне у Flight, де легка маршрутизація та гнучкість можуть призводити до складних взаємодій. Для соло-розробників або команд unit-тести слугують страхувальною сіткою, документуючи очікувану поведінку та запобігаючи регресіям, коли ви повертаєтесь до коду пізніше. Вони також покращують дизайн: код, який важко тестувати, часто сигналізує про надто складні або тісно пов'язані класи.

На відміну від спрощених прикладів (наприклад, тестування x * y = z), ми зосередимось на реальних поведінках, таких як перевірка вхідних даних, збереження даних або ініціювання дій на кшталт надсилання листів. Наша мета — зробити тестування доступним і значущим.

Загальні керівні принципи

  1. Тестуйте поведінку, а не реалізацію: Зосереджуйтесь на результатах (наприклад, «лист надіслано» або «запис збережено»), а не на внутрішніх деталях. Це робить тести стійкими до рефакторингу.
  2. Перестаньте використовувати Flight::: Статичні методи Flight неймовірно зручні, але ускладнюють тестування. Вам варто звикнути використовувати змінну $app з $app = Flight::app();. $app має всі ті ж методи, що й Flight::. Ви все одно зможете використовувати $app->route() або $this->app->json() у своєму контролері тощо. Також варто використовувати справжній маршрутизатор Flight через $router = $app->router(), і тоді ви зможете використовувати $router->get(), $router->post(), $router->group() тощо. Див. Маршрутизація.
  3. Тримайте тести швидкими: Швидкі тести спонукають до частого запуску. Уникайте повільних операцій, як-от виклики бази даних в unit-тестах. Якщо у вас повільний тест, це ознака того, що ви пишете інтеграційний тест, а не unit-тест. Інтеграційні тести — це коли ви фактично задіюєте реальні бази даних, реальні HTTP-виклики, реальне надсилання листів тощо. Вони мають своє місце, але вони повільні та можуть бути нестабільними, тобто іноді падають з невідомої причини.
  4. Використовуйте описові назви: Назви тестів мають чітко описувати поведінку, яка тестується. Це покращує читабельність і супроводжуваність.
  5. Уникайте глобальних змінних, як чуми: Мінімізуйте використання $app->set() і $app->get(), оскільки вони діють як глобальний стан, вимагаючи макетів у кожному тесті. Віддавайте перевагу DI або контейнеру DI (див. Контейнер впровадження залежностей). Навіть використання методу $app->map() технічно є «глобальним» станом, і його варто уникати на користь DI. Використовуйте бібліотеку сесій, наприклад flightphp/session, щоб мати змогу макетувати об'єкт сесії у ваших тестах. Не викликайте $_SESSION безпосередньо у вашому коді, оскільки це вносить глобальну змінну у ваш код, що ускладнює тестування.
  6. Використовуйте впровадження залежностей: Впроваджуйте залежності (наприклад, PDO, поштові сервіси) у контролери, щоб ізолювати логіку та спростити макетування. Якщо у вас клас із забагато залежностей, розгляньте можливість рефакторингу його на менші класи, кожен з яких має єдину відповідальність згідно з принципами SOLID.
  7. Макетуйте сторонні сервіси: Макетуйте бази даних, HTTP-клієнти (cURL) або поштові сервіси, щоб уникнути зовнішніх викликів. Тестуйте на один-два рівні вглиб, але дайте вашій основній логіці виконуватись. Наприклад, якщо ваш застосунок надсилає SMS, ви НЕ хочете реально надсилати SMS щоразу під час запуску тестів, бо ці витрати накопичуватимуться (і це буде повільніше). Натомість змакетуйте сервіс SMS і просто перевірте, що ваш код викликав сервіс SMS із правильними параметрами.
  8. Прагніть високого покриття, а не досконалості: 100% покриття рядків — це добре, але це не означає, що все у вашому коді протестовано так, як треба (можете дослідити покриття гілок/шляхів у PHPUnit). Пріоритетними є критичні поведінки (наприклад, реєстрація користувача, відповіді API та фіксація невдалих відповідей).
  9. Використовуйте контролери для маршрутів: У визначеннях маршрутів використовуйте контролери, а не замикання. flight\Engine $app впроваджується в кожен контролер через конструктор за замовчуванням. У тестах використовуйте $app = new Flight\Engine(), щоб створити екземпляр Flight у межах тесту, впровадіть його у ваш контролер і викликайте методи безпосередньо (наприклад, $controller->register()). Див. Розширення Flight та Маршрутизація.
  10. Оберіть стиль макетування і дотримуйтесь його: PHPUnit підтримує кілька стилів макетування (наприклад, prophecy, вбудовані макети), або ви можете використовувати анонімні класи, які мають свої переваги, як-от автодоповнення коду, ламання, якщо ви змінюєте визначення методу, тощо. Просто будьте послідовні у своїх тестах. Див. PHPUnit Mock Objects.
  11. Використовуйте видимість protected для методів/властивостей, які ви хочете тестувати в підкласах: Це дозволяє перевизначати їх у тестових підкласах, не роблячи їх публічними; це особливо корисно для анонімних класів-макетів.

Налаштування PHPUnit

Спершу налаштуйте PHPUnit у вашому проєкті Flight PHP за допомогою Composer для зручного тестування. Більше деталей див. у посібнику PHPUnit для початківців.

  1. У каталозі вашого проєкту виконайте:

    composer require --dev phpunit/phpunit

    Це встановить останню версію PHPUnit як залежність для розробки.

  2. Створіть каталог tests у корені проєкту для тестових файлів.

  3. Додайте тестовий скрипт до composer.json для зручності:

    // інший вміст composer.json
    "scripts": {
        "test": "phpunit --configuration phpunit.xml"
    }
  4. Створіть файл 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']);
    }
}

Ключові моменти:

Запустіть 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']);
    }
}

Ключові моменти:

Тестування контролера з макетами

Тепер протестуємо поведінку 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']);
    }
}

Ключові моменти:

Надмірне макетування

Будьте обережні, щоб не макетувати занадто багато вашого коду. Наведу приклад нижче, чому це може бути погано, на прикладі нашого 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-тестами

Unit-тести особливо корисні у більших проєктах або коли ви повертаєтесь до коду через місяці. Вони документують поведінку та виявляють регресії, рятуючи вас від повторного вивчення вашого застосунку. Для соло-розробників тестуйте критичні шляхи (наприклад, реєстрацію користувача, обробку платежів). Для команд тести забезпечують узгоджену поведінку серед усіх внесків. Більше про переваги фреймворків і тестів див. у розділі Чому фреймворки?.

Долучайтеся до репозиторію документації Flight PHP зі своїми порадами щодо тестування!

Автор: n0nag0n, 2025

Guides/blog

Створення простого блогу за допомогою Flight PHP

Цей посібник проведе вас через створення базового блогу з використанням PHP-фреймворку Flight. Ви налаштуєте проєкт, визначите маршрути, керуватимете публікаціями за допомогою JSON та відображатимете їх за допомогою шаблонізатора Latte — все це демонструє простоту та гнучкість Flight. Наприкінці у вас буде функціональний блог із головною сторінкою, сторінками окремих публікацій та формою створення.

Передумови

Крок 1: Налаштування вашого проєкту

Почніть зі створення нового каталогу проєкту та встановлення Flight через Composer.

  1. Створіть каталог:

    mkdir flight-blog
    cd flight-blog
  2. Встановіть Flight:

    composer require flightphp/core
  3. Створіть публічний каталог: Flight використовує єдину точку входу (index.php). Створіть папку public/ для неї:

    mkdir public
  4. Базовий index.php: Створіть public/index.php із простим маршрутом "hello world":

    <?php
    require '../vendor/autoload.php';
    
    Flight::route('/', function () {
        echo 'Hello, Flight!';
    });
    
    Flight::start();
  5. Запустіть вбудований сервер: Перевірте ваше налаштування за допомогою сервера розробки 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

Крок 3: Встановлення та налаштування Latte

Latte — це легкий шаблонізатор, який добре інтегрується з Flight.

  1. Встановіть Latte:

    composer require latte/latte
  2. Налаштуйте 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();
  3. Створіть шаблон макета: У 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>&copy; {date('Y')} Flight Blog</p>
        </footer>
    </body>
    </html>
  4. Створіть головний шаблон: У 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, щоб побачити відрендерену сторінку.

  5. Створіть файл даних:

    Використовуйте файл JSON для імітації бази даних задля простоти.

    У data/posts.json:

    [
        {
            "slug": "first-post",
            "title": "My First Post",
            "content": "This is my very first blog post with Flight PHP!"
        }
    ]

Крок 4: Визначення маршрутів

Виділіть свої маршрути в окремий файл конфігурації для кращої організації.

  1. Створіть 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']);
    });
  2. Оновіть 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: Зберігання та отримання публікацій блогу

Додайте методи для завантаження та збереження публікацій.

  1. Додайте метод для публікацій: У index.php додайте метод для завантаження публікацій:

    Flight::map('posts', function () {
        $file = __DIR__ . '/../data/posts.json';
        return json_decode(file_get_contents($file), true);
    });
  2. Оновіть маршрути: Змініть 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: Створення шаблонів

Оновіть шаблони для відображення публікацій.

  1. Сторінка публікації (app/views/post.latte):

    {extends 'layout.latte'}
    
     {block content}
         <h2>{$post['title']}</h2>
         <div class="post-content">
             <p>{$post['content']}</p>
         </div>
     {/block}

Крок 7: Додавання створення публікацій

Обробка надсилання форми для додавання нових публікацій.

  1. Форма (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}
  2. Додайте 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('/');
    });
  3. Перевірте:

    • Відвідайте 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}

Наступні кроки

Висновок

Ви створили простий блог за допомогою Flight PHP! Цей посібник демонструє основні функції, як-от маршрутизацію, шаблонізацію за допомогою Latte та обробку надсилання форм — усе це залишається легким. Вивчайте документацію Flight, щоб дізнатися про більш розширені функції та розвинути свій блог далі!

License

Ліцензія MIT (MIT)

Авторське право © 2024 @mikecao, @n0nag0n

Цим надається дозвіл, безкоштовно, будь-якій особі, яка отримала копію цього програмного забезпечення та супутньої документації файлів (далі — “Програмне забезпечення”), користуватися Програмним забезпеченням без обмежень, включаючи без обмежень права на використання, копіювання, модифікацію, об’єднання, публікацію, розповсюдження, підліцензування та/або продаж копій Програмного забезпечення, а також дозволяти особам, яким Програмне забезпечення надано, робити це, за умов дотримання наступних умов:

Вищезазначене повідомлення про авторські права та це повідомлення про дозвіл повинні бути включені в усі копії або значні частини Програмного забезпечення.

ПРОГРАМНЕ ЗАБЕЗПЕЧЕННЯ НАДАЄТЬСЯ “ЯК Є”, БЕЗ ГАРАНТІЙ БУДЬ-ЯКОГО РОДУ, ЯВНИХ АБО ПРИХОВАНИХ, ВКЛЮЧАЮЧИ, АЛЕ НЕ ОБМЕЖУЮЧИСЬ ГАРАНТІЯМИ КОМЕРЦІЙНОЇ РІЗНИЧКИ, ПРИДАТНОСТІ ДЛЯ ПЕВНОЇ МЕТИ ТА НЕПORУШЕННЯ. У ЖОДНОМУ ВИПАДКУ АВТОРИ АБО ВЛАСНИКИ АВТОРСЬКИХ ПРАВ НЕ НЕСУТЬ ВІДПОВІДАЛЬНОСТІ ЗА БУДЬ-ЯКІ ПРЕТЕНЗІЇ, ЗБИТКИ АБО ІНШІ ЗОБОВ'ЯЗАННЯ, ЧИ У ПРАВОВІЙ СПРАВІ, ДЕЛІКТІ АБО ІНШОМУ, ЩО ВИНИКЛО, ВИРІСШЕ З, АБО У ЗВ'ЯЗКУ З ПРОГРАМНИМ ЗАБЕЗПЕЧЕННЯМ АБО ВИКОРИСТАННЯМ АБО ІНШИМИ УГОДАМИ У ПРОГРАМНОМУ ЗАБЕЗПЕЧЕННІ.

About

Flight PHP Framework

Flight — це швидкий, простий, розширюваний фреймворк для PHP, створений для розробників, які хочуть швидко виконувати роботу без зайвих проблем. Незалежно від того, чи створюєте ви класичний веб-додаток, блискавично швидкий API, чи працюєте з AI-асистентами програмування, низький розмір і проста конструкція Flight роблять його ідеальним вибором. Flight призначений бути легким, але також може задовольняти вимоги архітектури корпоративного рівня.

Чому обрати Flight?

Відеогляд

Досить просто, правда?
Дізнайтеся більше про 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 та досвід розробки.

Що ви отримуєте (високий рівень):

Встановлення скелета додатка

Досить просто!

# Створити новий проєкт
composer create-project flightphp/skeleton my-project/
# Перейти до директорії нового проєкту
cd my-project/
# Запустити локальний dev-сервер, щоб одразу почати роботу!
composer start

Це створить структуру проєкту, скопіює config_sample.phpconfig.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

Matrix

Та Discord

Внесок

Є два способи внести свій внесок у Flight:

  1. Внесок у основний фреймворк, відвідавши репозиторій ядра.
  2. Допомогти покращити документацію! Цей сайт документації розміщений на 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

Переваги

Цей сайт документації використовує цю бібліотеку для кешування кожної зі сторінок!

Натисніть тут, щоб переглянути код.

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

Встановіть через 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

Основні параметри конфігурації:

Управління робітниками за допомогою 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 — це рядок, який містить три частини:

  1. Заголовок: Метадані про токен (алгоритм, тип)
  2. Навантаження: Ваші дані (ID користувача, ролі, термін дії тощо)
  3. Підпис: Криптографічний підпис для перевірки автентичності

Приклад JWT: eyJ0eXAiOiJKV1QiLCJhbGc... (виглядає як нісенітниця, але це структуровані дані!)

Чому використовувати JWT?

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

Встановіть за допомогою 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)

$jwt = JWT::encode($payload, $secretKey, 'HS256');
$decoded = JWT::decode($jwt, new Key($secretKey, 'HS256'));

Асиметричні алгоритми (RSA/ECDSA)

// Генерація ключів: 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"

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 надає ці основні методи:

Чому використовувати цю бібліотеку?

Дивіться також

Ліцензія

Бібліотека 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.


Що він робить?

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

  1. Завантажте папку flight-integration до вашої директорії /wp-content/plugins/.
  2. Активуйте плагін в адмін-панелі WordPress (меню Плагіни).
  3. Перейдіть до Налаштувань > Flight Framework, щоб налаштувати плагін.
  4. Вкажіть шлях до вашої інсталяції Flight (або використовуйте Composer для встановлення Flight).
  5. Налаштуйте шлях до папки вашого додатку та створіть структуру папок (плагін може допомогти з цим!).
  6. Почніть створювати свій додаток 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-асистент може:

Ключові моменти

Конфігурація 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 (або інший асинхронний драйвер) для продакшену з мінімальними змінами.

Вимоги

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

Встановіть через 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

Цей файл — проста перемикачка, яка змушує додаток працювати в режимі 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();
    }
}

Запуск сервера

Порада: Для продакшену використовуйте реверс-проксі (Nginx) перед Swoole для обробки TLS, статичних файлів та балансування навантаження.

Нотатки щодо конфігурації

Драйвер Swoole надає кілька опцій конфігурації:

Налаштуйте ці параметри відповідно до ресурсів вашого хоста та шаблонів трафіку.

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

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 Скрипти

Скрипти поділені на три набори скриптів:

Директорія зі скриптами виглядає так:

 <root dir>
     |
     +-- base.sql
     |
     +-- /migrations
              |
              +-- /up
                   |
                   +-- 00001.sql
                   +-- 00002.sql
              +-- /down
                   |
                   +-- 00000.sql
                   +-- 00001.sql

Багаторазове середовище розробки

Якщо ви працюєте з кількома розробниками та кількома гілками, важко визначити, яке наступне число.

У цьому випадку ви можете додати суфікс "-dev" після номера версії.

Погляньте на сценарій:

У обох випадках розробники створять файл під назвою 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 та інтеграція його у ваші проєкти

Основне використання:

Дивіться приклад:

<?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.db

Awesome-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();

Можливості

Вимоги

Що Версія
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();

Кілька речей, які варто знати про шаблони:

Стилізація 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 коли можливо, інакше простий текст
]);

Режими:

Генерація запускається після рендерингу та вбудовування 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 та кешуванням.

Особливості

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

Встановіть за допомогою 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\

Інтелектуальне виявлення шляхів:

Як це працює:

Інтеграція з Tracy Debugger

CommentTemplate включає інтеграцію з Tracy Debugger для логування та налагодження під час розробки.

Comment Template Tracy

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

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 з чотирма вкладками:

Що логується

Примітка: Нульовий вплив на продуктивність, коли 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.

Особливості

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

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 показує:

Для повної документації відвідайте репозиторій 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">
            &copy; 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();

Ключові Пункти

Конфігурація

Ви можете налаштувати обробник сесій, передаючи масив опцій під час реєстрації:

// Так, це подвійний масив :)
$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-серіалізацію:

Примітка: Якщо ви використовуєте 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 надає ці методи:

Усі методи, крім get() і id(), повертають екземпляр Session для ланцюжка.

Чому Використовувати Цей Плагін?

Технічні Деталі

Співпраця

Внески вітаються! Форкуйте 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.

  1. Якщо ви використовуєте проект-скелет, ви можете запустити php runway [команда] з кореня вашого проекту.
  2. Якщо ви використовуєте 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.phpconfig: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);
}

Дивіться також

Awesome-plugins/tracy_extensions

Розширення панелі Tracy для Flight

Це набір розширень, які роблять роботу з Flight ще багатшою.

Це особливо зручно з офіційним скелетом, який за замовчуванням використовує Twig: той самий макет інструменти AI також чітко відображається на панелі Tracy.

Це панель

Flight Bar

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

Flight Data Flight Database Flight Request

Натисніть тут, щоб переглянути код.

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

Виконайте 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_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);
});

Дивіться також

Awesome-plugins/apm

FlightPHP APM Документація

Ласкаво просимо до FlightPHP APM — вашого особистого тренера з продуктивності для застосунку! Цей посібник — ваш маршрут до налаштування, використання та опанування моніторингу продуктивності застосунків (APM) з FlightPHP. Незалежно від того, чи ви полюєте за повільними запитами, чи просто хочете розібратися в графіках затримок, ми вас покриємо. Давайте зробимо ваш застосунок швидшим, користувачів щасливішими, а сеанси налагодження — легшими!

Перегляньте демо дашборду для сайту Flight Docs.

FlightPHP APM

Чому APM Важливий

Уявіть, що ваш застосунок — це ресторан з великим потоком. Без способу відстежувати, скільки часу займають замовлення чи де кухня гальмує, ви лише вгадуєте, чому клієнти йдуть незадоволеними. APM — ваш помічник кухаря — він стежить за кожним кроком, від вхідних запитів до запитів до бази даних, і позначає все, що вас сповільнює. Повільні сторінки втрачають користувачів (дослідження показують, що 53% відмовляються, якщо сайт завантажується понад 3 секунди!), і APM допомагає вам ловити ці проблеми до того, як вони завдадуть шкоди. Це проактивний спокій — менше моментів «чому це зламалося?», більше перемог «погляньте, як це круто працює!».

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

Почніть з Composer:

composer require flightphp/apm

Вам знадобиться:

Підтримувані Бази Даних

FlightPHP APM наразі підтримує наступні бази даних для зберігання метрик:

Ви можете вибрати тип бази даних під час кроку налаштування (див. нижче). Переконайтеся, що ваше середовище 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);

Що тут відбувається?

Порада: Вибірка Якщо ваш застосунок завантажений, логування кожного запиту може перевантажити систему. Використовуйте частоту вибірки (від 0.0 до 1.0):

$Apm = new Apm($ApmLogger, 0.1); // Logs 10% of requests

Це зберігає продуктивність на високому рівні, водночас надаючи вам надійні дані.

2. Налаштуйте Його

Виконайте це, щоб створити ваш .runway-config.json:

php vendor/bin/runway apm:init

Що це робить?

Цей процес також запитає, чи хочете ви запустити міграції для цього налаштування. Якщо ви налаштовуєте це вперше, відповідь — так.

Чому два місця? Необроблені метрики накопичуються швидко (подумайте про нефільтровані логи). Воркер обробляє їх у структуроване призначення для дашборду. Підтримує порядок!

3. Обробка Метрик за Допомогою Воркера

Воркер перетворює необроблені метрики на дані, готові для дашборду. Запустіть його один раз:

php vendor/bin/runway apm:worker

Що він робить?

Підтримуйте Його Роботу Для живих застосунків вам потрібна безперервна обробка. Ось ваші варіанти:

Чому варто турбуватися? Без воркера ваш дашборд порожній. Це місток між необробленими логами та практичними висновками.

4. Запустіть Дашборд

Перегляньте показники вашого застосунку:

php vendor/bin/runway apm:dashboard

Що це?

Налаштуйте Його:

php vendor/bin/runway apm:dashboard --host 0.0.0.0 --port 8080 --php-path=/usr/local/bin/php

Відкрийте URL у браузері та досліджуйте!

Продакшн Режим

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

Хочете інший дашборд?

Ви можете створити власний дашборд, якщо хочете! Подивіться на директорію vendor/flightphp/apm/src/apm/presenter для ідей щодо представлення даних для вашого власного дашборду!

Функції Дашборду

Дашборд — це ваша штаб-квартира APM — ось що ви побачите:

Додатково:

Приклад: Запит до /users може показати:

Додавання Користувацьких Подій

Відстежуйте будь-що — наприклад, 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);

Що Ви Отримаєте:

Увага:

Приклад Виводу:

Опції Воркера

Налаштуйте воркер на свій смак:

Приклад:

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 днів з бази даних.

Усунення Неполадок

Застрягли? Спробуйте це:

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);
}

Корисні поради

Коли ви налагоджуєте свій код, є деякі дуже корисні функції для виведення даних для вас.

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%!

Важливі примітки

Обмеження

Встановлення власних даних

Іноді вам може знадобитися прикріпити щось унікальне до вашого 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">
            &copy; 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 може стати ще крутішим завдяки плагінам на базі ШІ.

Документація API

Документація API є критично важливою для будь-якого API. Вона допомагає розробникам зрозуміти, як взаємодіяти з вашим API та чого очікувати у відповідь. Існує кілька інструментів, які допоможуть вам генерувати документацію API для ваших проектів Flight.

Моніторинг Продуктивності Додатків (APM)

Моніторинг продуктивності додатків (APM) є критично важливим для будь-якого додатку. Він допомагає вам зрозуміти, як працює ваша програма та де знаходяться вузькі місця. Існує ряд інструментів APM, які можна використовувати з Flight.

Асинхронність

Flight вже є швидким фреймворком, але додавання турбо-двигуна робить все ще веселішим (і складнішим)!

Авторизація/Дозволи

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

Аутентифікація

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

Кешування

Кешування - це чудовий спосіб прискорити роботу вашої програми. Існує ряд бібліотек кешування, які можна використовувати з Flight.

CLI

CLI-додатки - це чудовий спосіб взаємодії з вашою програмою. Ви можете використовувати їх для генерації контролерів, відображення всіх маршрутів тощо.

Cookies

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

Налагодження

Налагодження є критично важливим при розробці у вашому локальному середовищі. Є кілька плагінів, які можуть покращити ваш досвід налагодження.

Бази Даних

Бази даних є основою більшості додатків. Це те, як ви зберігаєте та отримуєте дані. Деякі бібліотеки баз даних є просто обгортками для написання запитів, а деякі - повноцінними ORM.

Шифрування

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

Електронна Пошта

Надсилання електронної пошти — базова потреба більшості веб-додатків: вітальні листи, скидання пароля, сповіщення. Ці бібліотеки роблять це безболісно і водночас зберігають надійну доставлюваність.

Черга Завдань

Черги завдань справді корисні для асинхронної обробки завдань. Це може бути надсилання електронних листів, обробка зображень або будь-що, що не потрібно робити в реальному часі.

Сесії

Сесії не дуже корисні для API, але для побудови веб-додатку сесії можуть бути критично важливими для підтримки стану та інформації про вхід.

Шаблонізація

Шаблонізація є основою будь-якого веб-додатку з UI. Існує ряд шаблонізаторів, які можна використовувати з Flight.

Інтеграція з WordPress

Хочете використовувати Flight у вашому проекті WordPress? Для цього є зручний плагін!

Внесок

Є плагін, яким ви хотіли б поділитися? Надішліть pull request, щоб додати його до списку!

Media

Медіа

Ми намагалися відстежити те, що можемо, з різних типів медіа в інтернеті щодо Flight. Дивіться нижче різні ресурси, які ви можете використовувати, щоб дізнатися більше про Flight.

Статті та огляд

Відео та посібники

Чи чогось бракує?

Чи бракує нам чогось, що ви написали чи записали? Дайте нам знати за допомогою issue або pull request!

Examples

Потрібен швидкий старт?

У вас є два варіанти для початку роботи з новим проектом Flight:

Приклади, надані спільнотою:

Потрібне натхнення?

Хоча ці приклади не є офіційно спонсорованими командою Flight, вони можуть дати вам ідеї щодо того, як структурувати ваші власні проекти, побудовані на Flight!

Хочете поділитися своїм прикладом?

Якщо у вас є проект, яким ви хочете поділитися, будь ласка, надішліть pull request, щоб додати його до цього списку!

Install/install

Інструкції зі встановлення

Перш ніж встановити Flight, потрібні деякі базові передумови. Зокрема вам знадобиться:

  1. Встановити PHP на вашій системі
  2. Встановити 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.phpconfig.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

  1. Встановіть Homebrew (якщо ще не встановлено):

    • Відкрийте Terminal і виконайте:
      /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
  2. Встановіть PHP:

    • Встановіть останню версію:
      brew install php
    • Щоб встановити конкретну версію, наприклад, PHP 8.1:
      brew tap shivammathur/php
      brew install shivammathur/php/php@8.1
  3. Перемикання між версіями PHP:

    • Видаліть поточну версію зі зв'язку та зв'яжіть бажану версію:
      brew unlink php
      brew link --overwrite --force php@8.1
    • Перевірте встановлену версію:
      php -v

Windows 10/11

Ручне встановлення PHP

  1. Завантажте PHP:

    • Відвідайте PHP для Windows та завантажте останню або конкретну версію (наприклад, 7.4, 8.0) як zip-файл без потокової безпеки (non-thread-safe).
  2. Розпакуйте PHP:

    • Розпакуйте завантажений zip-файл у C:\php.
  3. Додайте PHP до системного PATH:

    • Перейдіть до System Properties > Environment Variables.
    • У розділі System variables знайдіть Path і натисніть Edit.
    • Додайте шлях C:\php (або куди ви розпакували PHP).
    • Натисніть OK, щоб закрити всі вікна.
  4. Налаштуйте PHP:

    • Скопіюйте php.ini-development у php.ini.
    • Відредагуйте php.ini, щоб налаштувати PHP за потреби (наприклад, встановити extension_dir, увімкнути розширення).
  5. Перевірте встановлення PHP:

    • Відкрийте Command Prompt і виконайте:
      php -v

Встановлення кількох версій PHP

  1. Повторіть наведені вище кроки для кожної версії, розміщуючи кожну в окремій директорії (наприклад, C:\php7, C:\php8).

  2. Перемикайтеся між версіями, змінюючи системну змінну PATH, щоб вказувати на бажану директорію версії.

Ubuntu (20.04, 22.04 тощо)

Встановлення PHP за допомогою apt

  1. Оновіть списки пакетів:

    • Відкрийте Terminal і виконайте:
      sudo apt update
  2. Встановіть PHP:

    • Встановіть останню версію PHP:
      sudo apt install php
    • Щоб встановити конкретну версію, наприклад, PHP 8.1:
      sudo apt install php8.1
  3. Встановіть додаткові модулі (необов'язково):

    • Наприклад, щоб встановити підтримку MySQL:
      sudo apt install php8.1-mysql
  4. Перемикання між версіями PHP:

    • Використовуйте update-alternatives:
      sudo update-alternatives --set php /usr/bin/php8.1
  5. Перевірте встановлену версію:

    • Виконайте:
      php -v

Rocky Linux

Встановлення PHP за допомогою yum/dnf

  1. Увімкніть сховище EPEL:

    • Відкрийте Terminal і виконайте:
      sudo dnf install epel-release
  2. Встановіть сховище Remi:

    • Виконайте:
      sudo dnf install https://rpms.remirepo.net/enterprise/remi-release-8.rpm
      sudo dnf module reset php
  3. Встановіть PHP:

    • Щоб встановити версію за замовчуванням:
      sudo dnf install php
    • Щоб встановити конкретну версію, наприклад, PHP 7.4:
      sudo dnf module install php:remi-7.4
  4. Перемикання між версіями PHP:

    • Використовуйте команду модуля dnf:
      sudo dnf module reset php
      sudo dnf module enable php:remi-8.0
      sudo dnf install php
  5. Перевірте встановлену версію:

    • Виконайте:
      php -v

Загальні примітки

Guides

Посібники

Flight PHP створено для того, щоб бути простим, але потужним, і наші посібники допоможуть вам будувати реальні додатки крок за кроком. Ці практичні навчальні матеріали проведуть вас через повні проекти, щоб продемонструвати, як Flight можна використовувати ефективно.

Офіційні посібники

Будування блогу

Дізнайтеся, як створити функціональний блог-додаток за допомогою Flight PHP. Цей посібник проведе вас через:

Цей навчальний матеріал ідеальний для початківців, які хочуть побачити, як усі елементи поєднуються в реальному додатку.

Юніт-тестування та принципи SOLID

Цей посібник охоплює основи юніт-тестування в додатках Flight PHP. Він включає:

Неофіційні посібники

Хоча ці посібники не підтримуються офіційно командою 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 Pokémon API in PHP: A Beginner's Guide

Цей веселий посібник проведе вас через створення простого API для Pokémon за допомогою Flight PHP. Він охоплює основи налаштування API, визначення маршрутів та повернення JSON-відповідей.

Співпраця

Маєте ідею для посібника? Знайшли помилку? Ми вітаємо внесок! Наші посібники підтримуються в репозиторії документації FlightPHP.

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

Шукаєте документацію API?

Якщо ви шукаєте конкретну інформацію про основні функції та методи Flight, перегляньте розділ Learn нашої документації.