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

Розуміння

Ви можете налаштувати певні поведінки Flight, встановлюючи значення конфігурації через метод set.

Flight::set('flight.log_errors', true);

У файлі app/config/config.php ви можете побачити всі доступні змінні конфігурації за замовчуванням.

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

Опції конфігурації Flight

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

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

Крім того, є ще одне налаштування конфігурації для завантажувача. Це дозволить вам автоматично завантажувати класи з _ в назві класу.

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

Змінні

Flight дозволяє зберігати змінні, щоб їх можна було використовувати будь-де у вашому додатку.

// Зберегти вашу змінну
Flight::set('id', 123);

// В іншому місці вашого додатка
$id = Flight::get('id');

Щоб перевірити, чи встановлена змінна, ви можете зробити:

if (Flight::has('id')) {
  // Зробити щось
}

Ви можете очистити змінну так:

// Очищає змінну id
Flight::clear('id');

// Очищає всі змінні
Flight::clear();

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

Помилки та винятки

Усі помилки та винятки перехоплюються 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 Not Found

Коли URL не знайдено, Flight викликає метод notFound. Поведінка за замовчуванням — надіслати відповідь HTTP 404 Not Found з простим повідомленням.

Ви можете перевизначити цю поведінку для власних потреб:

Flight::map('notFound', function () {
  // Обробити не знайдено
});

Див. також

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

Журнал змін

Learn/ai

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

Огляд

Flight полегшує прискорення ваших PHP-проектів за допомогою інструментів на основі ШІ та сучасних робочих процесів розробників. З вбудованими командами для підключення до постачальників LLM (Велика мовна модель) та генерації проектно-специфічних інструкцій для кодування ШІ, Flight допомагає вам і вашій команді отримати максимум від асистентів ШІ, таких як GitHub Copilot, Cursor та Windsurf.

Розуміння

Асистенти для кодування ШІ найбільш корисні, коли вони розуміють контекст вашого проекту, конвенції та цілі. Допоміжники ШІ від Flight дозволяють вам:

Ці функції вбудовані в основний CLI Flight та офіційний стартовий проект flightphp/skeleton.

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

Налаштування облікових даних LLM

Команда ai:init проведе вас через процес підключення вашого проекту до постачальника LLM.

php runway ai:init

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

Це створює файл .runway-creds.json у корені вашого проекту (і забезпечує його додавання до .gitignore).

Приклад:

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:generate-instructions допомагає створити або оновити інструкції для асистентів кодування ШІ, адаптовані до вашого проекту.

php runway ai:generate-instructions

Ви відповість на кілька питань про ваш проект (опис, база даних, шаблонізація, безпека, розмір команди тощо). Flight використовує ваш постачальник LLM для генерації інструкцій, а потім записує їх до:

Приклад:

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)? latte
Is security an important element of this project? (y/n) y
...
AI instructions updated successfully.

Тепер ваші інструменти ШІ надаватимуть розумніші, більш релевантні пропозиції на основі реальних потреб вашого проекту.

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

Див. також

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

Журнал змін

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

Розуміння

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

Templates допомагають з XSS, екрануючи вихідні дані за замовчуванням, тому вам не потрібно пам'ятати про це. Sessions можуть допомогти з CSRF, зберігаючи токен CSRF у сесії користувача, як описано нижче. Використання підготовлених запитів з PDO може допомогти запобігти атакам SQL injection (або використання зручних методів у класі PdoWrapper). CORS можна обробити за допомогою простого хука перед викликом Flight::start().

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

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

Заголовки

HTTP-заголовки є одним з найпростіших способів захисту ваших веб-додатків. Ви можете використовувати заголовки, щоб запобігти clickjacking, XSS та іншим атакам. Існує кілька способів додати ці заголовки до вашого додатку.

Два чудові веб-сайти для перевірки безпеки ваших заголовків — securityheaders.com та observatory.mozilla.org. Після налаштування коду нижче ви можете легко перевірити, чи працюють ваші заголовки, за допомогою цих двох сайтів.

Додавання вручну

Ви можете вручну додати ці заголовки, використовуючи метод header об'єкта Flight\Response.

// Встановлення заголовка X-Frame-Options для запобігання clickjacking
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 sniffing
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=()');
});

Додавання як middleware

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

// app/middlewares/SecurityHeadersMiddleware.php

namespace app\middlewares;

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();
        $response->header('X-Frame-Options', 'SAMEORIGIN');
        $response->header("Content-Security-Policy", "default-src 'self'");
        $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=()');
    }
}

// index.php або де ви маєте свої маршрути
// FYI, ця порожня група діє як глобальний middleware для
// всіх маршрутів. Звичайно, ви можете зробити те саме і просто додати
// це тільки до конкретних маршрутів.
Flight::group('', function(Router $router) {
    $router->get('/users', [ 'UserController', 'getUsers' ]);
    // більше маршрутів
}, [ SecurityHeadersMiddleware::class ]);

Cross Site Request Forgery (CSRF)

Cross Site Request Forgery (CSRF) — це тип атаки, коли шкідливий веб-сайт може змусити браузер користувача надіслати запит на ваш веб-сайт. Це може бути використано для виконання дій на вашому веб-сайті без відома користувача. Flight не надає вбудований механізм захисту від CSRF, але ви можете легко реалізувати свій власний, використовуючи middleware.

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

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

Middleware
// app/middlewares/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');
            }
        }
    }
}

// index.php або де ви маєте свої маршрути
use app\middlewares\CsrfMiddleware;

Flight::group('', function(Router $router) {
    $router->get('/users', [ 'UserController', 'getUsers' ]);
    // більше маршрутів
}, [ CsrfMiddleware::class ]);
Фільтри подій
// Цей middleware перевіряє, чи є запит 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);
        }
    }
});

Cross Site Scripting (XSS)

Cross Site Scripting (XSS) — це тип атаки, коли шкідливий ввід форми може ін'єктувати код у ваш веб-сайт. Більшість таких можливостей виникає з значень форм, які заповнюватимуть ваші кінцеві користувачі. Ви ніколи не повинні довіряти виводу від ваших користувачів! Завжди припускайте, що всі вони є найкращими хакерами у світі. Вони можуть ін'єктувати шкідливий JavaScript або HTML у вашу сторінку. Цей код може бути використаний для крадіжки інформації від ваших користувачів або виконання дій на вашому веб-сайті. Використовуючи клас view Flight або інший шаблонізатор, як Latte, ви можете легко екранувати вихід, щоб запобігти атакам XSS.

// Припустимо, користувач розумний і намагається використати це як своє ім'я
$name = '<script>alert("XSS")</script>';

// Це екранує вихід
Flight::view()->set('name', $name);
// Це виведе: &lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;

// Якщо ви використовуєте щось на кшталт Latte, зареєстроване як ваш клас view, воно також автоматично екранує це.
Flight::view()->render('template', ['name' => $name]);

SQL Injection

SQL Injection — це тип атаки, коли шкідливий користувач може ін'єктувати SQL-код у вашу базу даних. Це може бути використано для крадіжки інформації з вашої бази даних або виконання дій у вашій базі даних. Знову ж таки, ви ніколи не повинні довіряти вводу від ваших користувачів! Завжди припускайте, що вони намагаються нашкодити. Використання підготовлених запитів у ваших об'єктах PDO запобіжить SQL injection.

// Припускаючи, що ви зареєстрували Flight::db() як ваш об'єкт PDO
$statement = Flight::db()->prepare('SELECT * FROM users WHERE username = :username');
$statement->execute([':username' => $username]);
$users = $statement->fetchAll();

// Якщо ви використовуєте клас PdoWrapper, це можна легко зробити в одному рядку
$users = Flight::db()->fetchAll('SELECT * FROM users WHERE username = :username', [ 'username' => $username ]);

// Ви можете зробити те саме з об'єктом PDO з плейсхолдерами ?
$statement = Flight::db()->fetchAll('SELECT * FROM users WHERE username = ?', [ $username ]);

Невпевнений приклад

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

var_dump($users); // це виведе всіх користувачів у базі даних, не тільки того з одним ім'ям користувача

CORS

Cross-Origin Resource Sharing (CORS) — це механізм, який дозволяє багатьом ресурсам (наприклад, шрифтам, JavaScript тощо) на веб-сторінці запитуватися з іншого домену поза доменом, з якого походить ресурс. Flight не має вбудованої функціональності, але це можна легко обробити за допомогою хука, який запускається перед викликом методу Flight::start().

// app/utils/CorsUtil.php

namespace app\utils;

class CorsUtil
{
    public function set(array $params): void
    {
        $request = Flight::request();
        $response = Flight::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 = Flight::request();

        if (in_array($request->getVar('HTTP_ORIGIN'), $allowed, true) === true) {
            $response = Flight::response();
            $response->header("Access-Control-Allow-Origin", $request->getVar('HTTP_ORIGIN'));
        }
    }
}

// index.php або де ви маєте свої маршрути
$CorsUtil = new CorsUtil();

// Це потрібно запустити перед запуском start.
Flight::before('start', [ $CorsUtil, 'setupCors' ]);

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

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


// Припустимо 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)) {
    // Пароль збігається
}

Обмеження швидкості

Захищайтеся від атак brute force або атак відмови в обслуговуванні, обмежуючи швидкість запитів за допомогою кешу.

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

Примітка: Хочете дізнатися більше про маршрутизацію? Перегляньте сторінку "why a framework?" для більш детального пояснення.

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

Визначення простого маршруту

Основна маршрутизація в 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' ]);
// or
Flight::route('/', [ GreetingController::class, 'hello' ]); // preferred method
// or
Flight::route('/', [ 'GreetingController::hello' ]);
// or 
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.';
});

// You cannot use Flight::get() for routes as that is a method 
//    to get variables, not create a route.
Flight::post('/', function() { /* code */ });
Flight::patch('/', function() { /* code */ });
Flight::put('/', function() { /* code */ });
Flight::delete('/', function() { /* code */ });

Ви також можете відображати кілька методів на один зворотний виклик, використовуючи роздільник |:

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!';
});
// A HEAD request to /info will return the same headers, but no body.

Запити OPTIONS

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

// For a route defined as:
Flight::route('GET|POST /users', function() { /* ... */ });

// An OPTIONS request to /users will respond with:
//
// Status: 204 No Content
// Allow: GET, POST, HEAD, OPTIONS

Використання об'єкта Router

Крім того, ви можете отримати об'єкт Router, який має деякі допоміжні методи для використання:


$router = Flight::router();

// maps all methods just like Flight::route()
$router->map('/', function() {
    echo 'hello world!';
});

// GET request
$router->get('/users', function() {
    echo 'users';
});
$router->post('/users',             function() { /* code */});
$router->put('/users/update/@id',   function() { /* code */});
$router->delete('/users/@id',       function() { /* code */});
$router->patch('/users/@id',        function() { /* code */});

Регулярні вирази (Regex)

Ви можете використовувати регулярні вирази у ваших маршрутах:

Flight::route('/user/[0-9]+', function () {
  // This will match /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) {
  // This will match /bob/123
  // But will not match /bob/12345
});

Примітка: Співставлення груп regex () з позиційними параметрами не підтримується. Приклад: :'\(

Важливе застереження

Хоча в прикладі вище здається, що @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) {
    // This will match the following URLS:
    // /blog/2012/12/10
    // /blog/2012/12
    // /blog/2012
    // /blog
  }
);

Будь-які необов'язкові параметри, які не співпадають, будуть передані як NULL.

Дикі карти в маршрутизації

Співставлення виконується тільки для окремих сегментів URL. Якщо ви хочете співставити кілька сегментів, ви можете використовувати символ дикого *.

Flight::route('/blog/*', function () {
  // This will match /blog/2000/02/01
});

Щоб маршрутизувати всі запити на один зворотний виклик, ви можете зробити:

Flight::route('*', function () {
  // Do something
});

Обробник 404 Not Found

За замовчуванням, якщо URL не знайдено, Flight надішле відповідь HTTP 404 Not Found, яка є дуже простою та звичайною. Якщо ви хочете мати більш кастомізовану відповідь 404, ви можете відобразити свій власний метод notFound:

Flight::map('notFound', function() {
    $url = Flight::request()->url;

    // You could also use Flight::render() with a custom template.
    $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);

    // You could also use Flight::render() with a custom template.
    $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 тощо), єдиний тип маршрутів, де це доступно, — це або безпосереднє створення об'єкта самостійно та використання контейнера для створення вашого об'єкта, або ви можете використовувати рядки для визначення класу та методу для виклику. Ви можете перейти на сторінку Dependency Injection для більшої інформації.

Ось швидкий приклад:


use flight\database\PdoWrapper;

// Greeting.php
class Greeting
{
    protected PdoWrapper $pdoWrapper;
    public function __construct(PdoWrapper $pdoWrapper) {
        $this->pdoWrapper = $pdoWrapper;
    }

    public function hello(int $id) {
        // do something with $this->pdoWrapper
        $name = $this->pdoWrapper->fetchField("SELECT name FROM users WHERE id = ?", [ $id ]);
        echo "Hello, world! My name is {$name}!";
    }
}

// index.php

// Setup the container with whatever params you need
// See the Dependency Injection page for more information on PSR-11
$dice = new \Dice\Dice();

// Don't forget to reassign the variable with '$dice = '!!!!!
$dice = $dice->addRule('flight\database\PdoWrapper', [
    'shared' => true,
    'constructParams' => [ 
        'mysql:host=localhost;dbname=test', 
        'root',
        'password'
    ]
]);

// Register the container handler
Flight::registerContainerHandler(function($class, $params) use ($dice) {
    return $dice->create($class, $params);
});

// Routes like normal
Flight::route('/hello/@id', [ 'Greeting', 'hello' ]);
// or
Flight::route('/hello/@id', 'Greeting->hello');
// or
Flight::route('/hello/@id', 'Greeting::hello');

Flight::start();

Передача виконання наступному маршруту

Deprecated Ви можете передати виконання наступному співпадаючому маршруту, повертаючи true з вашої функції зворотного виклику.

Flight::route('/user/@name', function (string $name) {
  // Check some condition
  if ($name !== "Bob") {
    // Continue to next route
    return true;
  }
});

Flight::route('/user/*', function () {
  // This will get called
});

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

Псевдоніми маршрутів

Призначаючи псевдонім маршруту, ви можете пізніше динамічно викликати цей псевдонім у вашому додатку, щоб згенерувати його пізніше в коді (наприклад: посилання в HTML-шаблоні або генерація URL для перенаправлення).

Flight::route('/users/@id', function($id) { echo 'user:'.$id; }, false, 'user_view');
// or 
Flight::route('/users/@id', function($id) { echo 'user:'.$id; })->setAlias('user_view');

// later in code somewhere
class UserController {
    public function update() {

        // code to save user...
        $id = $user['id']; // 5 for example

        $redirectUrl = Flight::getUrl('user_view', [ 'id' => $id ]); // will return '/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');
    // or
    Flight::route('/@id', function($id) { echo 'user:'.$id; })->setAlias('user_view');
});

Перевірка інформації про маршрут

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

  1. Ви можете використовувати властивість executedRoute на об'єкті Flight::router().
  2. Ви можете запросити передачу об'єкта маршруту до вашого зворотного виклику, передавши true як третій параметр у методі маршруту. Об'єкт маршруту завжди буде останнім параметром, переданим до вашої функції зворотного виклику.

executedRoute

Flight::route('/', function() {
  $route = Flight::router()->executedRoute;
  // Do something with $route
  // Array of HTTP methods matched against
  $route->methods;

  // Array of named parameters
  $route->params;

  // Matching regular expression
  $route->regex;

  // Contains the contents of any '*' used in the URL pattern
  $route->splat;

  // Shows the url path....if you really need it
  $route->pattern;

  // Shows what middleware is assigned to this
  $route->middleware;

  // Shows the alias assigned to this route
  $route->alias;
});

Примітка: Властивість executedRoute буде встановлена тільки після виконання маршруту. Якщо ви спробуєте отримати доступ до неї перед виконанням маршруту, вона буде NULL. Ви також можете використовувати executedRoute у middleware!

Передача true до визначення маршруту

Flight::route('/', function(\flight\net\Route $route) {
  // Array of HTTP methods matched against
  $route->methods;

  // Array of named parameters
  $route->params;

  // Matching regular expression
  $route->regex;

  // Contains the contents of any '*' used in the URL pattern
  $route->splat;

  // Shows the url path....if you really need it
  $route->pattern;

  // Shows what middleware is assigned to this
  $route->middleware;

  // Shows the alias assigned to this route
  $route->alias;
}, true);// <-- This true parameter is what makes that happen

Групування маршрутів та Middleware

Можуть бути випадки, коли ви хочете групувати пов'язані маршрути разом (наприклад, /api/v1). Ви можете зробити це, використовуючи метод group:

Flight::group('/api/v1', function () {
  Flight::route('/users', function () {
    // Matches /api/v1/users
  });

  Flight::route('/posts', function () {
    // Matches /api/v1/posts
  });
});

Ви навіть можете вкладати групи в групи:

Flight::group('/api', function () {
  Flight::group('/v1', function () {
    // Flight::get() gets variables, it doesn't set a route! See object context below
    Flight::route('GET /users', function () {
      // Matches GET /api/v1/users
    });

    Flight::post('/posts', function () {
      // Matches POST /api/v1/posts
    });

    Flight::put('/posts/1', function () {
      // Matches PUT /api/v1/posts
    });
  });
  Flight::group('/v2', function () {

    // Flight::get() gets variables, it doesn't set a route! See object context below
    Flight::route('GET /users', function () {
      // Matches GET /api/v2/users
    });
  });
});

Групування з контекстом об'єкта

Ви все ще можете використовувати групування маршрутів з об'єктом Engine наступним чином:

$app = Flight::app();

$app->group('/api/v1', function (Router $router) {

  // user the $router variable
  $router->get('/users', function () {
    // Matches GET /api/v1/users
  });

  $router->post('/posts', function () {
    // Matches POST /api/v1/posts
  });
});

Примітка: Це перевага метод визначення маршрутів і груп з об'єктом $router.

Групування з Middleware

Ви також можете призначати middleware групі маршрутів:

Flight::group('/api/v1', function () {
  Flight::route('/users', function () {
    // Matches /api/v1/users
  });
}, [ MyAuthMiddleware::class ]); // or [ new MyAuthMiddleware() ] if you want to use an instance

Дивіться більше деталей на сторінці group middleware.

Ресурсна маршрутизація

Ви можете створювати набір маршрутів для ресурсу, використовуючи метод 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.

// Whitelist only these methods and blacklist the rest
Flight::resource('/users', UsersController::class, [ 'only' => [ 'index', 'show' ] ]);
// Blacklist only these methods and whitelist the rest
Flight::resource('/users', UsersController::class, [ 'except' => [ 'create', 'store', 'edit', 'update', 'destroy' ] ]);

Це по суті опції білої та чорної списків, щоб ви могли вказати, які маршрути ви хочете створити.

Middleware

Ви також можете вказати middleware, яке буде виконуватися для кожного з маршрутів, створених методом resource.

Flight::resource('/users', UsersController::class, [ 'middleware' => [ MyAuthMiddleware::class ] ]);

Потокові відповіді

Ви тепер можете потоково передавати відповіді клієнту, використовуючи stream() або streamWithHeaders(). Це корисно для відправки великих файлів, довготривалих процесів або генерації великих відповідей. Потокова передача маршруту обробляється трохи інакше, ніж звичайний маршрут.

Примітка: Потокові відповіді доступні тільки якщо у вас встановлено flight.v2.output_buffering на false.

Потік з ручними заголовками

Ви можете потоково передавати відповідь клієнту, використовуючи метод stream() на маршруті. Якщо ви зробите це, ви мусите встановити всі заголовки вручну перед тим, як вивести щось клієнту. Це робиться з функцією header() php або методом Flight::response()->setRealHeader().

Flight::route('/@filename', function($filename) {

    $response = Flight::response();

    // obviously you would sanitize the path and whatnot.
    $fileNameSafe = basename($filename);

    // If you have additional headers to set here after the route has executed
    // you must define them before anything is echoed out.
    // They must all be a raw call to the header() function or 
    // a call to Flight::response()->setRealHeader()
    header('Content-Disposition: attachment; filename="'.$fileNameSafe.'"');
    // or
    $response->setRealHeader('Content-Disposition: attachment; filename="'.$fileNameSafe.'"');

    $filePath = '/some/path/to/files/'.$fileNameSafe;

    if (!is_readable($filePath)) {
        Flight::halt(404, 'File not found');
    }

    // manually set the content length if you'd like
    header('Content-Length: '.filesize($filePath));
    // or
    $response->setRealHeader('Content-Length: '.filesize($filePath));

    // Stream the file to the client as it's read
    readfile($filePath);

// This is the magic line here
})->stream();

Потік з заголовками

Ви також можете використовувати метод streamWithHeaders(), щоб встановити заголовки перед початком потокової передачі.

Flight::route('/stream-users', function() {

    // you can add any additional headers you want here
    // you just must use header() or Flight::response()->setRealHeader()

    // however you pull your data, just as an example...
    $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 ',';
        }

        // This is required to send the data to the client
        ob_flush();
    }
    echo '}';

// This is how you'll set the headers before you start streaming.
})->streamWithHeaders([
    'Content-Type' => 'application/json',
    'Content-Disposition' => 'attachment; filename="users.json"',
    // optional status code, defaults to 200
    'status' => 200
]);

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

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

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

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

Flight::route('/hello', function(){
    // This might cause a 404 Not Found error
    return 'Hello World';
});

// What you probably want
Flight::route('/hello', function(){
    echo 'Hello World';
});

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

Журнал змін

Learn/learn

Дізнайтесь про Flight

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

Примітка: Ви побачите приклади, які використовують Flight:: як статичну змінну, і деякі, що використовують об'єкт двигуна $app->. Обидва працюють взаємозамінно з одним іншим. $app та $this->app у контролері/проміжному ПЗ — це рекомендований підхід від команди Flight.

Основні компоненти

Маршрутизація

Дізнайтесь, як керувати маршрутами для вашого веб-додатка. Це також включає групування маршрутів, параметри маршрутів та проміжне ПЗ.

Проміжне ПЗ

Дізнайтесь, як використовувати проміжне ПЗ для фільтрації запитів та відповідей у вашому додатку.

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

Дізнайтесь, як автозавантажувати власні класи у вашому додатку.

Запити

Дізнайтесь, як обробляти запити та відповіді у вашому додатку.

Відповіді

Дізнайтесь, як надсилати відповіді вашим користувачам.

Шаблони HTML

Дізнайтесь, як використовувати вбудований двигун перегляду для рендерингу ваших шаблонів HTML.

Безпека

Дізнайтесь, як захищати ваш додаток від поширених загроз безпеки.

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

Дізнайтесь, як налаштовувати фреймворк для вашого додатка.

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

Дізнайтесь, як використовувати систему подій для додавання власних подій до вашого додатка.

Розширення Flight

Дізнайтесь, як розширювати фреймворк, додаючи власні методи та класи.

Хуки методів та фільтрація

Дізнайтесь, як додавати хуки подій до ваших методів та внутрішніх методів фреймворка.

Контейнер ін'єкції залежностей (DIC)

Дізнайтесь, як використовувати контейнери ін'єкції залежностей (DIC) для керування залежностями вашого додатка.

Утиліти класів

Колекції

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

Обгортка JSON

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

Обгортка PDO

PDO іноді може додавати більше головного болю, ніж потрібно. Цей простий клас-обгортка може значно спростити взаємодію з вашою базою даних.

Обробник завантажених файлів

Простий клас для допомоги в керуванні завантаженими файлами та переміщенні їх у постійне місце.

Важливі концепції

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

Ось коротка стаття про те, чому ви повинні використовувати фреймворк. Це гарна ідея зрозуміти переваги використання фреймворка, перш ніж почати його використовувати.

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

Flight порівняно з іншими фреймворками

Якщо ви мігруєте з іншого фреймворка, такого як Laravel, Slim, Fat-Free чи Symfony, до Flight, ця сторінка допоможе вам зрозуміти відмінності між ними.

Інші теми

Юніт-тестування

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

ШІ та досвід розробника

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

Міграція 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.

Тестування простого обробника маршруту

Припустимо, у вас є маршрут, який валідує email:

// 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\PdoWrapper;

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\PdoWrapper::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 є зручним помічником для роботи з базами даних за допомогою 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 is an array
}

Ви також можете використовувати його для записів:

$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'];
// or
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'];
    // or
    echo $user->name;
}

Використання заповнювачів IN()

Ви можете використовувати один ? у клаузі IN() і передати масив або рядок, розділений комами:

$ids = [1, 2, 3];
$users = Flight::db()->fetchAll("SELECT * FROM users WHERE id IN (?)", [$ids]);
// or
$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 // last param enables APM
]);

Після виконання запитів ви можете журналізувати їх вручну, але APM журналізуватиме їх автоматично, якщо увімкнено:

Flight::db()->logQueries();

Це викличе подію (flight.db.queries) з метриками з'єднання та запитів, яку ви можете прослуховувати за допомогою системи подій Flight.

Повний приклад

Flight::route('/users', function () {
    // Get all users
    $users = Flight::db()->fetchAll('SELECT * FROM users');

    // Stream all users
    $statement = Flight::db()->runQuery('SELECT * FROM users');
    while ($user = $statement->fetch()) {
        echo $user['name'];
    }

    // Get a single user
    $user = Flight::db()->fetchRow('SELECT * FROM users WHERE id = ?', [123]);

    // Get a single value
    $count = Flight::db()->fetchField('SELECT COUNT(*) FROM users');

    // Special IN() syntax
    $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']);

    // Insert a new user
    Flight::db()->runQuery("INSERT INTO users (name, email) VALUES (?, ?)", ['Bob', 'bob@example.com']);
    $insert_id = Flight::db()->lastInsertId();

    // Update a user
    Flight::db()->runQuery("UPDATE users SET name = ? WHERE id = ?", ['Bob', 123]);

    // Delete a user
    Flight::db()->runQuery("DELETE FROM users WHERE id = ?", [123]);

    // Get the number of affected rows
    $statement = Flight::db()->runQuery("UPDATE users SET name = ? WHERE name = ?", ['Bob', 'Sally']);
    $affected_rows = $statement->rowCount();
});

Див. також

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

Журнал змін

Learn/dependency_injection_container

Контейнер для ін'єкції залежностей

Огляд

Контейнер для ін'єкції залежностей (DIC) — це потужне розширення, яке дозволяє керувати залежностями вашого додатка.

Розуміння

Ін'єкція залежностей (DI) — це ключове поняття в сучасних PHP-фреймворках і використовується для керування створенням та конфігурацією об'єктів. Деякі приклади бібліотек DIC: flightphp/container, Dice, Pimple, PHP-DI, і league/container.

DIC — це вишуканий спосіб дозволити вам створювати та керувати вашими класами в централізованому місці. Це корисно, коли вам потрібно передавати той самий об'єкт до кількох класів (наприклад, до ваших контролерів або middleware).

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

Старий спосіб робити речі може виглядати так:


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 є те, що модульне тестування стає набагато простішим. Ви можете створити мок-об'єкт і передати його до вашого класу. Це величезна користь, коли ви пишете тести для вашого додатка!

Створення централізованого обробника 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 знав використовувати його для контролерів/middleware
Flight::registerContainerHandler(function($class, $params) {
    Flight::make($class, $params);
});

// припустимо, у нас є наступний приклад класу, який приймає об'єкт PDO в конструкторі
class EmailCron {
    protected PDO $pdo;

    public function __construct(PDO $pdo) {
        $this->pdo = $pdo;
    }

    public function send() {
        // код, що надсилає email
    }
}

// І нарешті, ви можете створювати об'єкти з використанням ін'єкції залежностей
$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

Якщо ви використовуєте екземпляр Engine у ваших контролерах/middleware, ось як би ви його налаштували:


// Десь у вашому файлі завантаження
$engine = Flight::app();

$container = new \Dice\Dice;
$container = $container->addRule('*', [
    'substitutions' => [
        // Тут ви передаєте екземпляр
        Engine::class => $engine
    ]
]);

$engine->registerContainerHandler(function($class, $params) use ($container) {
    return $container->create($class, $params);
});

// Тепер ви можете використовувати екземпляр Engine у ваших контролерах/middleware

class MyController {
    public function __construct(Engine $app) {
        $this->app = $app;
    }

    public function index() {
        $this->app->render('index');
    }
}

Додавання інших класів

Якщо у вас є інші класи, які ви хочете додати до контейнера, з 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. Ось приклад з використанням контейнера PSR-11 від League:


require 'vendor/autoload.php';

// той самий клас UserController, як вище

$container = new \League\Container\Container();
$container->add(UserController::class)->addArgument(PdoWrapper::class);
$container->add(PdoWrapper::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 за замовчуванням. Шаблонізація є дуже ефективним способом відокремити логіку вашого додатка від шару представлення.

Розуміння

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

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

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

Latte

рекомендовано

Ось як ви б використовували рушій шаблонів Latte для ваших переглядів.

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

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 з макетами показано в розділі awesome plugins цієї документації.

Ви можете дізнатися більше про повні можливості 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/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.

Розуміння

За замовчуванням будь-який клас Flight автозавантажується для вас автоматично завдяки composer. Однак, якщо ви хочете автозавантажувати власні класи, ви можете використовувати метод Flight::path() для вказівки каталогу для завантаження класів.

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

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

Припустимо, у нас є структура каталогів така:

# Приклад шляху
/home/user/project/my-flight-project/
├── app
│   ├── cache
│   ├── config
│   ├── controllers - містить контролери для цього проекту
│   ├── translations
│   ├── UTILS - містить класи лише для цієї програми (це все великими літерами навмисно для прикладу пізніше)
│   └── views
└── public
    └── css
    └── js
    └── index.php

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

Ви можете вказати кожен каталог для завантаження ось так:


/**
 * 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() для вказівки кореневого каталогу (не кореневого каталогу документів або папки 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) {
        // зробити щось
    }
}

Підкреслення в назвах класів

Починаючи з 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/controllers і він має простір імен
// це не працюватиме.
Flight::route('/hello', 'MyController->hello');

// вам потрібно обрати один з цих варіантів
Flight::route('/hello', 'app\controllers\MyController->hello');
// або якщо у вас є оператор use нагорі

use app\controllers\MyController;

Flight::route('/hello', [ MyController::class, 'hello' ]);
// також можна написати
Flight::route('/hello', MyController::class.'->hello');
// також...
Flight::route('/hello', [ 'app\controllers\MyController', 'hello' ]);

path() не визначено

У скелетному додатку це визначено всередині файлу config.php, але щоб ваші класи були знайдені, вам потрібно переконатися, що метод path() визначено (ймовірно, до кореня вашого каталогу) перед тим, як ви спробуєте його використовувати.

// Додати шлях до автозавантажувача
Flight::path(__DIR__.'/../');

Журнал змін

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

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

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

Чому проводити юніт-тестування?

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

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

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

  1. Тестуйте поведінку, а не реалізацію: Зосередьтеся на результатах (наприклад, «email надіслано» або «запис збережено») замість внутрішніх деталей. Це робить тести стійкими до рефакторингу.
  2. Перестаньте використовувати Flight::: Статичні методи Flight жахливо зручні, але ускладнюють тестування. Ви повинні звикнути використовувати змінну $app з $app = Flight::app();. $app має всі ті самі методи, що й Flight::. Ви все ще зможете використовувати $app->route() або $this->app->json() у вашому контролері тощо. Ви також повинні використовувати реальний роутер Flight з $router = $app->router() і тоді ви зможете використовувати $router->get(), $router->post(), $router->group() тощо. Див. Routing.
  3. Тримайте тести швидкими: Швидкі тести заохочують до частого виконання. Уникайте повільних операцій, як-от виклики бази даних у юніт-тестах. Якщо у вас є повільний тест, це знак, що ви пишете інтеграційний тест, а не юніт-тест. Інтеграційні тести — це коли ви дійсно залучаєте реальні бази даних, реальні HTTP-виклики, реальне надсилання email тощо. Вони мають своє місце, але вони повільні та можуть бути нестабільними, тобто іноді провалюються з невідомої причини.
  4. Використовуйте описові назви: Назви тестів повинні чітко описувати поведінку, яка тестується. Це покращує читабельність і підтримку.
  5. Уникайте глобальних змінних як чуми: Мінімізуйте використання $app->set() і $app->get(), оскільки вони діють як глобальний стан, що вимагає імітацій у кожному тесті. Віддавайте перевагу DI або контейнеру DI (див. Dependency Injection Container). Навіть використання методу $app->map() технічно є «глобальним» і повинно уникатися на користь DI. Використовуйте бібліотеку сесій, таку як flightphp/session, щоб ви могли імітувати об'єкт сесії у ваших тестах. Не викликайте $_SESSION безпосередньо у вашому коді, оскільки це впорскує глобальну змінну у ваш код, ускладнюючи тестування.
  6. Використовуйте вприскування залежностей: Впроваджуйте залежності (наприклад, PDO, поштові клієнти) у контролери, щоб ізолювати логіку та спростити імітацію. Якщо у вас є клас з надто багатьма залежностями, розгляньте рефакторинг його на менші класи, кожен з яких має єдину відповідальність, дотримуючись принципів SOLID.
  7. Імітуйте сторонні сервіси: Імітуйте бази даних, HTTP-клієнти (cURL) або поштові сервіси, щоб уникнути зовнішніх викликів. Тестуйте на один-два рівні глибини, але дозвольте вашій основній логіці виконуватися. Наприклад, якщо ваша програма надсилає текстове повідомлення, ви НЕ хочете дійсно надсилати текстове повідомлення щоразу, коли запускаєте тести, бо ці витрати накопичаться (і це буде повільніше). Натомість імітуйте сервіс текстових повідомлень і просто перевірте, що ваш код викликав сервіс текстових повідомлень з правильними параметрами.
  8. Стреміться до високого покриття, а не до досконалості: 100% покриття рядків — це добре, але це насправді не означає, що все у вашому коді протестовано так, як повинно бути (прочитайте про branch/path coverage в PHPUnit). Пріоритезуйте критичні поведінки (наприклад, реєстрацію користувача, відповіді API та захоплення невдалих відповідей).
  9. Використовуйте контролери для маршрутів: У ваших визначеннях маршрутів використовуйте контролери, а не замикання. flight\Engine $app впроваджується в кожен контролер через конструктор за замовчуванням. У тестах використовуйте $app = new Flight\Engine() для інстанціювання Flight у тесті, впровадіть його у ваш контролер і викликайте методи безпосередньо (наприклад, $controller->register()). Див. Extending Flight і Routing.
  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 Testing and SOLID Principles для деталей про структуру тестів:

// 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'; // Simulate POST data
        $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'; // Simulate POST data
        $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, щоб перевірити, чи маршрут поводиться як очікується. Для деталей про requests і responses у Flight див. відповідну документацію.

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

Для складніших сценаріїв використовуйте вприскування залежностей (DI), щоб зробити контролери тестовими. Уникайте глобальних змінних Flight (наприклад, Flight::set(), Flight::map(), Flight::register()), оскільки вони діють як глобальний стан, що вимагає імітацій для кожного тесту. Натомість використовуйте контейнер DI Flight, DICE, PHP-DI або ручне DI.

Використаємо flight\database\PdoWrapper замість сирого PDO. Цей обгортка набагато легша для імітації та юніт-тестування!

Ось контролер, який зберігає користувача в базу даних і надсилає email привітання:

use flight\database\PdoWrapper;

class UserController {
    protected $app;
    protected $db;
    protected $mailer;

    public function __construct(Engine $app, PdoWrapper $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)) {
            // adding the return here helps unit testing to stop execution
            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, збереження в базу даних та надсилання email. Ми імітуємо базу даних і поштовий сервіс, щоб ізолювати контролер.

// tests/UserControllerDICTest.php
use PHPUnit\Framework\TestCase;

class UserControllerDICTest extends TestCase {
    public function testValidEmailSavesAndSendsEmail() {

        // Sometimes mixing mocking styles is necessary
        // Here we use PHPUnit's built-in mock for PDOStatement
        $statementMock = $this->createMock(PDOStatement::class);
        $statementMock->method('execute')->willReturn(true);
        // Using an anonymous class to mock PdoWrapper
        $mockDb = new class($statementMock) extends PdoWrapper {
            protected $statementMock;
            public function __construct($statementMock) {
                $this->statementMock = $statementMock;
            }

            // When we mock it this way, we are not really making a database call.
            // We can further setup this to alter the PDOStatement mock to simulate failures, etc.
            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 PdoWrapper {
            // An empty constructor bypasses the parent constructor
            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';

        // Need to map jsonHalt to avoid exiting
        $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\PdoWrapper;
use flight\Engine;

// UserControllerDICV2.php
class UserControllerDICV2 {
    protected $app;
    protected $db;
    protected $mailer;

    public function __construct(Engine $app, PdoWrapper $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)) {
            // adding the return here helps unit testing to stop execution
            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);
    }
}

І тепер надмірно імітований юніт-тест, який насправді нічого не тестує:

use PHPUnit\Framework\TestCase;

class UserControllerTest extends TestCase {
    public function testValidEmailSavesAndSendsEmail() {
        $app = new Engine();
        $app->request()->data->email = 'test@example.com';
        // we are skipping the extra dependency injection here cause it's "easy"
        $controller = new class($app) extends UserControllerDICV2 {
            protected $app;
            // Bypass the deps in the construct
            public function __construct($app) {
                $this->app = $app;
            }

            // We'll just force this to be valid.
            protected function isEmailValid($email) {
                return true; // Always return true, bypassing real validation
            }

            // Bypass the actual DB and mailer calls
            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']);
    }
}

Ура, у нас є юніт-тести, і вони проходять! Але чекайте, що якщо я насправді зміню внутрішні механізми isEmailValid або registerUser? Мої тести все ще пройдуть, бо я імітував всю функціональність. Дозвольте мені показати, що я маю на увазі.

// UserControllerDICV2.php
class UserControllerDICV2 {

    // ... other methods ...

    protected function isEmailValid($email) {
        // Changed logic
        $validEmail = filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
        // Now it should only have a specific domain
        $validDomain = strpos($email, '@example.com') !== false; 
        return $validEmail && $validDomain;
    }
}

Якщо я запустив би мої юніт-тести вище, вони все ще пройшли б! Але бо я не тестував поведінку (насправді дозволяючи деякому коду виконуватися), я потенційно закодував помилку, яка чекає на прояв у продакшні. Тест повинен бути модифікований, щоб врахувати нову поведінку, а також протилежність, коли поведінка не така, як ми очікуємо.

Повний приклад

Ви можете знайти повний приклад проекту Flight PHP з юніт-тестами на GitHub: n0nag0n/flight-unit-tests-guide. Для глибшого розуміння див. Unit Testing and SOLID Principles.

Поширені помилки

Масштабування з юніт-тестами

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

Внесіть свої поради з тестування до репозиторію документації Flight PHP!

Написано n0nag0n 2025

Guides/blog

Будівництво простого блогу з Flight PHP

Цей посібник проведе вас через створення базового блогу з використанням фреймворку Flight PHP. Ви налаштуєте проект, визначите маршрути, керуватимете дописами з 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 'Привіт, Flight!';
    });
    
    Flight::start();
  5. Запустіть вбудований сервер: Перевірте вашу установку за допомогою розробницького сервера PHP:

    php -S localhost:8000 -t public/

    Відвідайте http://localhost:8000, щоб побачити "Привіт, 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' => 'Мій блог']);
    });
    
    Flight::start();
  3. Створіть шаблон макету: У app/views/layout.latte:

    <!DOCTYPE html>
    <html>
    <head>
    <title>{$title}</title>
    </head>
    <body>
    <header>
        <h1>Мій блог</h1>
        <nav>
            <a href="/">Головна</a> | 
            <a href="/create">Створити допис</a>
        </nav>
    </header>
    <main>
        {block content}{/block}
    </main>
    <footer>
        <p>&copy; {date('Y')} Блог Flight</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": "Мій перший допис",
           "content": "Це мій перший допис у блозі з Flight PHP!"
       }
    ]

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

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

  1. Створіть routes.php: У app/config/routes.php:

    <?php
    Flight::route('/', function () {
       Flight::view()->render('home.latte', ['title' => 'Мій блог']);
    });
    
    Flight::route('/post/@slug', function ($slug) {
       Flight::view()->render('post.latte', ['title' => 'Допис: ' . $slug, 'slug' => $slug]);
    });
    
    Flight::route('GET /create', function () {
       Flight::view()->render('create.latte', ['title' => 'Створити допис']);
    });
  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' => 'Мій блог',
           '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' => 'Створити допис']);
    });

Крок 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">Назва:</label>
                <input type="text" name="title" id="title" required>
            </div>
            <div class="form-group">
                <label for="content">Зміст:</label>
                <textarea name="content" id="content" required></textarea>
            </div>
            <button type="submit">Зберегти допис</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.
    • Надішліть новий допис (наприклад, "Другий допис" з деяким змістом).
    • Перевірте головну сторінку, щоб побачити його в списку.

Крок 8: Поліпшіть обробку помилок

Перевизначте метод notFound для покращеного досвіду 404.

У index.php:

Flight::map('notFound', function () {
    Flight::view()->render('404.latte', ['title' => 'Сторінка не знайдена']);
});

Створіть app/views/404.latte:

{extends 'layout.latte'}

{block content}
    <h2>404 - {$title}</h2>
    <p>Вибачте, така сторінка не існує!</p>
{/block}

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

Висновок

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

License

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

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

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

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

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

About

Фреймворк Flight PHP

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

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

Огляд у відео

Досить просто, правда?
Дізнатися більше про Flight у документації!

Швидкий старт

Щоб швидко встановити базову версію, встановіть за допомогою Composer:

composer require flightphp/core

Або ви можете завантажити zip-архів репозиторію here. Тоді у вас буде базовий файл 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 у своєму браузері, щоб побачити вивід.

Skeleton/Boilerplate App

Є приклад додатку, який допоможе вам розпочати проект з Flight. Він має структуровану розмітку, базові налаштування, готові до використання, і обробляє скрипти composer прямо з коробки! Перегляньте flightphp/skeleton для готового проекту, або відвідайте сторінку examples для натхнення. Хочете побачити, як вписується ШІ? Explore AI-powered examples.

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

Дуже просто!

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

Він створить структуру проекту, налаштує потрібні файли, і ви готові до роботи!

Висока продуктивність

Flight — один з найшвидших фреймворків PHP. Його легке ядро означає менше накладу і більше швидкості — ідеально для традиційних додатків і сучасних проектів на основі ШІ. Ви можете переглянути всі бенчмарки на TechEmpower

Перегляньте бенчмарк нижче з деякими популярними PHP фреймворками.

Framework Plaintext Reqs/sec JSON Reqs/sec
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 і ШІ

Цікаво, як він працює з ШІ? Discover як Flight полегшує роботу з вашим улюбленим кодувальним LLM!

Спільнота

Ми в Matrix Chat

Matrix

І в Discord

Співпраця

Є два способи, як ви можете внести внесок у Flight:

  1. Внесіть внесок у основний фреймворк, відвідавши core repository.
  2. Допоможіть покращити документацію! Цей веб-сайт документації розміщений на Github. Якщо ви помітили помилку або хочете щось покращити, будь ласка, надішліть pull request. Ми любимо оновлення і нові ідеї — особливо щодо ШІ і нових технологій!

Вимоги

Flight вимагає PHP 7.4 або вище.

Примітка: PHP 7.4 підтримується, бо на момент написання (2024) PHP 7.4 є версією за замовчуванням для деяких LTS дистрибутивів Linux. Примусовий перехід на 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) {

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

Awesome-plugins/permissions

FlightPHP/Permissions

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

Клікніть тут, щоб перейти до репозиторію на GitHub.

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

Запустіть composer require flightphp/permissions, і ви на правильному шляху!

Використання

Спочатку вам потрібно налаштувати ваші дозволи, потім ви скажете вашому додатку, що означають ці дозволи. Врешті-решт, ви будете перевіряти ваші дозволи за допомогою $Permissions->has(), ->can(), або is(). has() і can() мають однакову функціональність, але називаються по-різному, щоб зробити ваш код читабельнішим.

Основний приклад

Припустимо, що у вас є функція у вашому додатку, яка перевіряє, чи ввійшов користувач. Ви можете створити об'єкт дозволу таким чином:

// index.php
require 'vendor/autoload.php';

// деякий код 

// потім, напевно, у вас є щось, що говорить вам, яка поточна роль особи
// ймовірно, у вас є щось, де ви витягуєте поточну роль
// з змінної сесії, яка це визначає
// після того, як хтось увійде, інакше у них буде роль 'гостя' або 'публічна'.
$current_role = 'admin';

// налаштування дозволів
$permission = new \flight\Permission($current_role);
$permission->defineRule('loggedIn', function($current_role) {
    return $current_role !== 'guest';
});

// Ви, напевно, захотите зберегти цей об'єкт у Flight десь 
Flight::set('permission', $permission);

Потім у контролері десь ви можете мати щось на зразок цього.

<?php

// якийсь контролер
class SomeController {
    public function someAction() {
        $permission = Flight::get('permission');
        if ($permission->has('loggedIn')) {
            // зробити щось
        } else {
            // зробити щось інше
        }
    }
}

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

$current_role = 'admin';

// налаштування дозволів
$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')) {
            // зробити щось
        } else {
            // зробити щось інше
        }
    }
}

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

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

Замикання

$Permission->defineRule('order', function(string $current_role, MyDependency $MyDependency = null) {
    // ... код
});

// у вашому файлі контролера
public function createOrder() {
    $MyDependency = Flight::myDependency();
    $permission = Flight::get('permission');
    if ($permission->can('order.create', $MyDependency)) {
        // зробити щось
    } else {
        // зробити щось інше
    }
}

Класи

namespace MyApp;

class Permissions {

    public function order(string $current_role, MyDependency $MyDependency = null) {
        // ... код
    }
}

Швидкий спосіб встановлення дозволів за допомогою класів

Ви також можете використовувати класи для визначення ваших дозволів. Це корисно, якщо у вас багато дозволів, і ви хочете зберегти ваш код чистим. Ви можете зробити щось на зразок цього:

<?php

// код завантаження
$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) {
        // Припускаємо, що ви налаштували це раніше
        /** @var \flight\database\PdoWrapper $db */
        $db = Flight::db();
        $allowed_permissions = [ 'read' ]; // всі можуть переглядати замовлення
        if($current_role === 'manager') {
            $allowed_permissions[] = 'create'; // менеджери можуть створювати замовлення
        }
        $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($current_role === 'admin') {
            $allowed_permissions[] = 'delete'; // адміністратори можуть видаляти замовлення
        }
        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 {
        // код для визначення дозволів
        return $permissions_array;
    }

    public function company(string $current_role, int $company_id): array {
        // код для визначення дозволів
        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('Ви не можете створити замовлення. Вибачте!');
        }
    }
}

Кешування

Щоб увімкнути кешування, ознайомтеся з простим wruczak/phpfilecache бібліотекою. Приклад увімкнення цього наведено нижче.


// цей $app може бути частиною вашого коду, або
// ви можете просто передати null, і це
// витягне з Flight::app() у конструкторі
$app = Flight::app();

// Поки що він приймає це як кеш файлів. Інші можна легко
// додати в майбутньому. 
$Cache = new Wruczek\PhpFileCache\PhpFileCache;

$Permissions = new \flight\Permission($current_role, $app, $Cache);
$Permissions->defineRulesFromClassMethods(MyApp\Permissions::class, 3600); // 3600 - це скільки секунд кешувати це. Залиште це, щоб не використовувати кешування

І вперед!

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/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/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/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

Розгін

Розгін - це CLI-додаток, який допомагає вам керувати вашими застосунками Flight. Він може генерувати контролери, відображати всі маршрути та багато іншого. Він базується на відмінній бібліотеці adhocore/php-cli.

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

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

Встановіть за допомогою composer.

composer require flightphp/runway

Основна конфігурація

Перший раз, коли ви запустите Розгін, він проведе вас через процес налаштування і створить файл конфігурації .runway.json у корені вашого проєкту. Цей файл міститиме необхідні конфігурації для коректної роботи Розгону.

Використання

Розгін має кілька команд, які ви можете використовувати для керування вашим застосунком Flight. Є два простих способи використовувати Розгін.

  1. Якщо ви використовуєте скелетний проєкт, ви можете запустити php runway [command] з кореня вашого проєкту.
  2. Якщо ви використовуєте Розгін як пакет, встановлений через composer, ви можете запустити vendor/bin/runway [command] з кореня вашого проєкту.

Для будь-якої команди ви можете передати прапор --help, щоб отримати більше інформації про те, як використовувати команду.

php runway routes --help

Ось кілька прикладів:

Генерація контролера

На основі конфігурації у вашому файлі .runway.json, за замовчуванням буде згенеровано контролер у каталозі app/controllers/.

php runway make:controller MyController

Генерація моделі Active Record

На основі конфігурації у вашому файлі .runway.json, за замовчуванням буде згенеровано контролер у каталозі app/records/.

php runway make:record users

Якщо, наприклад, у вас є таблиця users з наступною схемою: id, name, email, created_at, updated_at, буде створено файл подібний до наступного у файлі app/records/UserRecord.php:

<?php

declare(strict_types=1);

namespace app\records;

/**
 * Клас ActiveRecord для таблиці користувачів.
 * @link https://docs.flightphp.com/awesome-plugins/active-record
 * 
 * @property int $id
 * @property string $name
 * @property string $email
 * @property string $created_at
 * @property string $updated_at
 * // ви також можете додати зв'язки тут, як тільки визначите їх у масиві $relations
 * @property CompanyRecord $company Приклад зв'язку
 */
class UserRecord extends \flight\ActiveRecord
{
    /**
     * @var array $relations Встановити зв'язки для моделі
     *   https://docs.flightphp.com/awesome-plugins/active-record#relationships
     */
    protected array $relations = [];

    /**
     * Конструктор
     * @param mixed $databaseConnection З'єднання з базою даних
     */
    public function __construct($databaseConnection)
    {
        parent::__construct($databaseConnection, 'users');
    }
}

Відображення всіх маршрутів

Це відобразить всі маршрути, які наразі зареєстровані в Flight.

php runway routes

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

# Відображати лише маршрути GET
php runway routes --get

# Відображати лише маршрути POST
php runway routes --post

# тощо.

Налаштування Розгону

Якщо ви або створюєте пакет для Flight, або хочете додати свої власні команди у свій проєкт, ви можете це зробити, створивши каталог src/commands/, flight/commands/, app/commands/, або commands/ для вашого проєкту/пакету.

Щоб створити команду, вам просто потрібно розширити клас AbstractBaseCommand і реалізувати, принаймні, методи __construct та execute.

<?php

declare(strict_types=1);

namespace flight\commands;

class ExampleCommand extends AbstractBaseCommand
{
    /**
     * Конструктор
     *
     * @param array<string,mixed> $config JSON конфігурація з .runway-config.json
     */
    public function __construct(array $config)
    {
        parent::__construct('make:example', 'Створити приклад для документації', $config);
        $this->argument('<funny-gif>', 'Назва смішного гіфу');
    }

    /**
     * Виконує функцію
     *
     * @return void
     */
    public function execute(string $controller)
    {
        $io = $this->app()->io();

        $io->info('Створення прикладу...');

        // Зробіть щось тут

        $io->ok('Приклад створено!');
    }
}

Дивіться adhocore/php-cli Документацію для отримання додаткової інформації про те, як створити свої власні команди у вашому застосунку Flight!

Awesome-plugins/tracy_extensions

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

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

Це панель

Flight Bar

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

Flight Data Flight Database Flight Request

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

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

Виконайте composer require flightphp/tracy-extensions --dev, і ви готові!

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

Потрібно виконати дуже мало налаштувань, щоб запустити це. Вам потрібно ініціалізувати налагоджувач 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();
// You may need to specify your environment with Debugger::enable(Debugger::DEVELOPMENT)

// if you use database connections in your app, there is a 
// required PDO wrapper to use ONLY IN DEVELOPMENT (not production please!)
// It has the same parameters as a regular PDO connection
$pdo = new PdoQueryCapture('sqlite:test.db', 'user', 'pass');
// or if you attach this to the Flight framework
Flight::register('db', PdoQueryCapture::class, ['sqlite:test.db', 'user', 'pass']);
// now whenever you make a query it will capture the time, query, and parameters

// This connects the dots
if(Debugger::$showBar === true) {
    // This needs to be false or Tracy can't actually render :(
    Flight::set('flight.content_length', false);
    new TracyExtensionLoader(Flight::app());
}

// more code

Flight::start();

Додаткова конфігурація

Дані сесії

Якщо у вас є власний обробник сесій (наприклад, ghostff/session), ви можете передати будь-який масив даних сесії до Tracy, і він автоматично виведе його для вас. Ви передаєте його за допомогою ключа session_data у другому параметрі конструктора TracyExtensionLoader.


use Ghostff\Session\Session;
// or use flight\Session;

require 'vendor/autoload.php';

$app = Flight::app();

$app->register('session', Session::class);

if(Debugger::$showBar === true) {
    // This needs to be false or Tracy can't actually render :(
    Flight::set('flight.content_length', false);
    new TracyExtensionLoader(Flight::app(), [ 'session_data' => Flight::session()->getAll() ]);
}

// routes and other things...

Flight::start();

Latte

Для цього розділу потрібен PHP 8.1+.

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


require 'vendor/autoload.php';

$app = Flight::app();

$app->map('render', function($template, $data, $block = null) {
    $latte = new Latte\Engine;

    // other configurations...

    // only add the extension if Tracy Debug Bar is enabled
    if(Debugger::$showBar === true) {
        // this is where you add the Latte Panel to Tracy
        $latte->addExtension(new Latte\Bridges\Tracy\TracyExtension);
    }

    $latte->render($template, $data, $block);
});

Awesome-plugins/apm

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

Ласкаво просимо до FlightPHP APM — вашого особистого тренера продуктивності для додатку! Цей посібник — ваша дорожня карта для налаштування, використання та освоєння Application Performance Monitoring (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\Apm;

$ApmLogger = LoggerFactory::create(__DIR__ . '/../../.runway-config.json');
$Apm = new Apm($ApmLogger);
$Apm->bindEventsToFlightInstance($app);

// Якщо ви додаєте з'єднання з базою даних
// Має бути PdoWrapper або PdoQueryCapture з Tracy Extensions
$pdo = new PdoWrapper('mysql:host=localhost;dbname=example', 'user', 'pass', null, true); // <-- True обов'язково для увімкнення відстеження в APM.
$Apm->addPdoConnection($pdo);

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

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

$Apm = new Apm($ApmLogger, 0.1); // Логує 10% запитів

Це тримає продуктивність жвавою, але все одно дає солідні дані.

2. Налаштуйте це

Запустіть це, щоб створити .runway-config.json:

php vendor/bin/runway apm:init

Що це робить?

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

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

3. Обробіть метрики з Worker

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

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\PdoWrapper;

$pdo = new PdoWrapper('sqlite:/path/to/db.sqlite', null, null, null, true); // <-- True обов'язково для увімкнення відстеження в APM.
$Apm->addPdoConnection($pdo);

Що ви отримуєте:

Увага:

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

Опції Worker

Налаштуйте робочий процес на свій смак:

Приклад:

php vendor/bin/runway apm:worker --daemon --batch_size 100 --timeout 3600

Працює годину, обробляючи 100 метрик за раз.

Request ID у додатку

Кожен запит має унікальний ID запиту для відстеження. Ви можете використовувати цей ID у своєму додатку для кореляції логів і метрик. Наприклад, ви можете додати ID запиту на сторінку помилки:

Flight::map('error', function($message) {
    // Отримайте ID запиту з заголовка відповіді X-Flight-Request-Id
    $requestId = Flight::response()->getHeader('X-Flight-Request-Id');

    // Додатково ви можете отримати його з змінної Flight
    // Цей метод не працюватиме добре в swoole чи інших асинхронних платформах.
    // $requestId = Flight::get('apm.request_id');

    echo "Error: $message (Request ID: $requestId)";
});

Оновлення

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

php vendor/bin/runway apm:migrate

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

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

Очищення старих даних

Щоб тримати вашу базу даних охайною, ви можете очистити старі дані. Це особливо корисно, якщо ви ведете зайнятий додаток і хочете тримати розмір бази даних керованою. Ви можете зробити це, запустивши таку команду:

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.

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

Встановіть з Composer. І вам насправді захочеться встановити це без версії для розробників, оскільки 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 видима, то довжину вмісту не можна встановити Flight

    // Це специфічно для розширення Tracy для Flight, якщо ви це включили
    // інакше прокоментуйте це.
    new TracyExtensionLoader($app);
}

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

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

Awesome-plugins/active_record

Flight Active Record

Активний запис - це відображення сутності бази даних на об'єкт PHP. Простими словами, якщо у вас є таблиця користувачів у вашій базі даних, ви можете "перекласти" рядок у цій таблиці в клас 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 для таблиці користувачів.
 * @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(); // і ім'я, і пароль оновлюються.

copyFrom(array $data): ActiveRecord (v0.4.0)

Це псевдонім для методу dirty(). Це трохи ясніше, що ви робите.

$user->copyFrom([ 'name' => 'something', 'password' => password_hash('a different password') ]);
$user->update(); // і ім'я, і пароль оновлюються.

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-ін'єкцій. Є багато статей в Інтернеті, будь ласка, Google "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(), або заповнюючи третій параметр умови після поля та значення.

// Метод 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' // це якщо ви хочете повернути цю взаємовідносину назад до себе, наприклад: $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; // це ім'я користувача

Досить круто, чи не так?

Налаштування нестандартних даних

Іноді вам може знадобитися прикріпити щось унікальне до вашого 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 beforeUpdate(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 afterUpdate(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 'Він був сміливим солдатом... :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 щоразу, коли ви викликаєте активний запис, існують варіанти!

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

Примітка: Якщо ви плануєте юніт-тестування, робити це може додати певні труднощі до юніт-тестування, але в цілому, оскільки ви можете впроваджувати ваше з'єднання за допомогою setDatabaseConnection() або $config['connection'], це не так вже й погано.

Якщо вам потрібно оновити з'єднання з базою даних, наприклад, якщо ви запускаєте тривале CLI-скрипт і вам потрібно періодично оновлювати з'єднання, ви можете заново встановити з'єднання за допомогою $your_record->setDatabaseConnection($pdo_connection).

Внески

Будь ласка, робіть це. :D

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

Коли ви будете брати участь, переконайтеся, що ви запустили composer test-coverage, щоб підтримувати 100% покриття тестами (це не 100% покриття юніт-тестами, а скоріше інтеграційне тестування).

Також переконайтеся, що ви запустили 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, а інші є мікро/лайт бібліотеками, щоб допомогти вам розпочати.

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

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

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

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

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

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

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

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

Кешування

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

CLI

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

Куки

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

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

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

Бази Даних

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

Шифрування

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

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

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

Сесії

Сесії не дуже корисні для 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.

Ви також можете завантажити файли безпосередньо та розпакувати їх до вашої веб-каталоги.

Рекомендоване встановлення

Високо рекомендується починати з додатку flightphp/skeleton для будь-яких нових проектів. Встановлення дуже просте.

composer create-project flightphp/skeleton my-project/

Це налаштує структуру вашого проекту, налаштує автозавантаження з просторами імен, налаштує конфігурацію та надасть інші інструменти, такі як Tracy, Tracy Extensions та Runway

Налаштування вашого веб-сервера

Вбудований сервер розробки PHP

Це найпростіший спосіб запустити все. Ви можете використовувати вбудований сервер для запуску вашого додатку та навіть використовувати SQLite для бази даних (за умови, що sqlite3 встановлено на вашій системі) і не вимагати нічого особливого! Просто виконайте таку команду після встановлення PHP:

php -S localhost:8000
# або з додатком skeleton
composer start

Потім відкрийте браузер та перейдіть до http://localhost:8000.

Якщо ви хочете зробити корінь документів вашого проекту іншою директорією (Наприклад: ваш проект ~/myproject, але корінь документів ~/myproject/public/), ви можете виконати таку команду, перебуваючи в директорії ~/myproject:

php -S localhost:8000 -t public/
# з додатком skeleton це вже налаштовано
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

// If you're using Composer, require the autoloader.
// Якщо ви використовуєте Composer, підключіть автозавантажувач.
require 'vendor/autoload.php';
// if you're not using Composer, load the framework directly
// якщо ви не використовуєте Composer, завантажте фреймворк безпосередньо
// require 'flight/Flight.php';

// Then define a route and assign a function to handle the request.
// Потім визначте маршрут та призначте функцію для обробки запиту.
Flight::route('/', function () {
  echo 'hello world!';
});

// Finally, start the framework.
// Нарешті, запустіть фреймворк.
Flight::start();

З додатком skeleton це вже налаштовано та обробляється у вашому файлі app/config/routes.php. Сервіси налаштовуються в app/config/services.php

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

Якщо у вас вже встановлено php на системі, пропустіть ці інструкції та перейдіть до розділу завантаження

macOS

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

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

    • Відкрийте Термінал та виконайте:
      /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 for Windows та завантажте останню або конкретну версію (наприклад, 7.4, 8.0) як zip-файл non-thread-safe.
  2. Розпакуйте PHP:

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

    • Перейдіть до Властивостей системи > Змінні середовища.
    • Під Системні змінні, знайдіть Path та натисніть Редагувати.
    • Додайте шлях C:\php (або де ви розпакували PHP).
    • Натисніть OK, щоб закрити всі вікна.
  4. Налаштуйте PHP:

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

    • Відкрийте Командний рядок та виконайте:
      php -v

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

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

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

Ubuntu (20.04, 22.04 тощо)

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

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

    • Відкрийте Термінал та виконайте:
      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:

    • Відкрийте Термінал та виконайте:
      sudo dnf install epel-release
  2. Встановіть репозиторій Remi's:

    • Виконайте:
      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 нашої документації.