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 

Изменения в диспетчере

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

AI и опыт разработчика с Flight

Обзор

Flight упрощает суперзарядку ваших PHP-проектов с помощью инструментов на базе ИИ и современных рабочих процессов разработчика. С встроенными командами для подключения к провайдерам LLM (Large Language Model) и генерации специфических для проекта инструкций по кодированию с ИИ, 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

Эта статья была originally опубликована на Airpair в 2015 году. Вся заслуга принадлежит Airpair и Brian Fenton, который originally написал эту статью, хотя веб-сайт более не доступен, и статья существует только в 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 и посмотрите. Вы, вероятно, обнаружите, что значительная часть проблемы, которую вы пытаетесь решить, уже написана и протестирована.

Хотя tempting писать весь код самому (и нет ничего плохого в написании собственного фреймворка или библиотеки как опыта обучения) вы должны бороться с этими чувствами "Не Изобретено Здесь" и сэкономить себе кучу времени и головной боли. Следуйте доктрине 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, где найти все необходимые файлы для использования библиотек, которые мы только что установили. Чтобы использовать его, просто добавьте эту строку (обычно в файл bootstrap, который выполняется на каждый запрос):

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

Итак, это довольно базовая модель сущности. Но одна из этих вещей не подходит сюда. Единственная ответственность модели сущности должна заключаться в поведении, связанном с сущностью, которую она представляет, она не должна быть responsible за сохранение себя.

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 - Принцип открытости/замкнутости

Есть потрясающий тест для этого, который довольно точно суммирует, о чем этот принцип: подумайте о функции для реализации, вероятно, самой недавней, над которой вы работали или работаете. Можете ли вы реализовать эту функцию в существующей кодовой базе ТОЛЬКО путем добавления новых классов и без изменения каких-либо существующих классов в вашей системе? Ваша конфигурация и код wiring получают некоторое послабление, но в большинстве систем это удивительно сложно. Вам приходится полагаться на полиморфный dispatch, и большинство кодовых баз просто не настроены на это. Если вас это интересует, есть хороший Google talk на 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);
}

Это будет представлять наш базовый четырехсторонний shape. Ничего особенного здесь.

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

Вот наш первый shape, квадрат. Довольно простой shape, правда? Вы можете предположить, что есть конструктор, где мы устанавливаем размеры, но вы видите здесь из этой реализации, что длина и высота всегда будут одинаковыми. Квадраты такие.

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. Все еще имеет те же сигнатуры методов, это все еще четырехсторонний shape, но что, если мы начнем пытаться использовать их взаимозаменяемо? Теперь вдруг, если мы изменим высоту нашего Shape, мы больше не можем предположить, что длина нашего shape совпадет. Мы нарушили контракт, который имели с пользователем, когда дали им наш Square shape.

Это классический пример нарушения LSP, и нам нужен такой тип принципа, чтобы максимально использовать систему типов. Даже duck typing не скажет нам, если базовое поведение отличается, и поскольку мы не можем знать это без того, чтобы увидеть, как оно ломается, лучше убедиться, что оно не отличается изначально.

3.1.3 I - Принцип сегрегаации интерфейсов

Этот принцип говорит отдавать предпочтение многим малым, тонким интерфейсам по сравнению с одним большим. Интерфейсы должны основываться на поведении, а не на "это один из этих классов". Подумайте об интерфейсах, которые поставляются с PHP. Traversable, Countable, Serializable, вещи вроде того. Они рекламируют возможности, которыми обладает объект, а не то, от чего он наследует. Так что держите свои интерфейсы маленькими. Вы не хотите, чтобы интерфейс имел 30 методов, 3 - гораздо лучшая цель.

3.1.4 D - Принцип инверсии зависимостей

Вы, вероятно, слышали об этом в других местах, где говорили о Dependency Injection, но Dependency Inversion и Dependency Injection - не совсем одно и то же. Dependency inversion - это в основном способ сказать, что вы должны зависеть от абстракций в вашей системе, а не от ее деталей. Что это значит для вас в повседневной жизни?

Не используйте mysqli_query() напрямую по всему коду, используйте что-то вроде DataStore->query() вместо.

Ядро этого принципа - это абстракции. Это больше о том, чтобы сказать "используйте адаптер базы данных" вместо зависимости от прямых вызовов, как mysqli_query. Если вы напрямую используете mysqli_query в половине своих классов, то вы привязываете все напрямую к вашей базе данных. Ничего против MySQL, но если вы используете mysqli_query, этот тип низкоуровневых деталей должен быть скрыт в одном месте, а затем эта функциональность должна быть раскрыта через общий обертку.

Теперь я знаю, что это своего рода избитый пример, если вы подумаете об этом, потому что количество раз, когда вы фактически полностью измените движок базы данных после ввода продукта в производство, очень, очень мало. Я выбрал это, потому что подумал, что люди будут знакомы с идеей из своего собственного кода. Кроме того, даже если у вас есть база данных, с которой вы знаете, что останетесь, этот абстрактный объект-обертка позволяет вам исправлять ошибки, изменять поведение или реализовывать функции, которые вы желаете, чтобы ваша выбранная база данных имела. Это также делает unit testing возможным, где низкоуровневые вызовы не сделали бы.

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

Теперь у нас один уровень вложенности. Но, looking at this, все, что мы делаем, - это применяем функцию к каждому элементу массива. Нам даже не нужен цикл foreach для этого.

$data = array_filter($data);
$csvLines = array_map(function($row) {
    return implode(',', $row);
}, $data);

Теперь у нас нет вложенности вообще, и код, вероятно, будет быстрее, поскольку мы делаем весь цикл с нативными C-функциями вместо PHP. Нам приходится заниматься некоторым трюком, чтобы передать запятую в implode, так что вы можете утверждать, что остановка на предыдущем шаге гораздо понятнее.

4.2 Старайтесь не использовать else

Это действительно касается двух основных идей. Первая - несколько операторов return из метода. Если у вас достаточно информации, чтобы принять решение об результате метода, просто примите это решение и вернитесь. Вторая - идея, известная как Guard Clauses. Это в основном проверки валидации, объединенные с ранними возвратами, обычно в начале метода. Позвольте мне показать, что я имею в виду.

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

Для меня этот пример гораздо легче следовать. Здесь мы используем guard clauses, чтобы проверить наши начальные утверждения о параметрах, которые мы передаем, и немедленно выйти из метода, если они не проходят. Мы также больше не имеем промежуточную переменную для отслеживания суммы на протяжении всего метода. В этом случае мы убедились, что уже находимся на happy path, и можем просто делать то, для чего пришли. Опять же, мы могли бы сделать все эти проверки в одном if, но принцип должен быть ясен.

5 Unit testing

Unit testing - это практика написания малых тестов, которые проверяют поведение в вашем коде. Они почти всегда пишутся на том же языке, что и код (в данном случае PHP), и предназначены для того, чтобы быть достаточно быстрыми, чтобы запускаться в любое время. Они чрезвычайно ценны как инструмент для улучшения вашего кода. Помимо очевидных преимуществ обеспечения того, что ваш код делает то, что вы думаете, unit testing может предоставить очень полезную обратную связь дизайна. Если кусок кода трудно протестировать, это часто подчеркивает проблемы дизайна. Они также дают вам страховочную сеть против регрессий, и это позволяет вам рефакторить гораздо чаще и эволюционировать ваш код к чищему дизайну.

5.1 Инструменты

Существует несколько инструментов unit testing в PHP, но далеко не самый распространенный - PHPUnit. Вы можете установить его, скачав PHAR файл непосредственно, или установить с composer. Поскольку мы используем composer для всего остального, мы покажем этот метод. Кроме того, поскольку PHPUnit, вероятно, не будет развернут в production, мы можем установить его как зависимость dev с помощью следующей команды:

composer require --dev phpunit/phpunit

5.2 Тесты - это спецификация

Самая важная роль unit тестов в вашем коде - предоставить исполняемую спецификацию того, что код предполагается делать. Даже если код теста неверен или код имеет ошибки, знание того, что система должна делать, бесценно.

5.3 Пишите тесты сначала

Если у вас была возможность увидеть набор тестов, написанных перед кодом и один, написанный после завершения кода, они поразительно отличаются. "После" тесты гораздо больше озабочены деталями реализации класса и обеспечением хорошего покрытия строк, в то время как "перед" тесты больше о проверке желаемого внешнего поведения. Это именно то, что нас интересует с unit тестами в любом случае, - убедиться, что класс демонстрирует правильное поведение. Тесты, ориентированные на реализацию, на самом деле затрудняют рефакторинг, потому что они ломаются, если внутренности классов изменяются, и вы только что лишили себя преимуществ сокрытия информации от OOP.

5.4 Что делает хороший unit тест

Хорошие unit тесты делят много следующих характеристик:

Есть причины пойти против некоторых из них, но как общие рекомендации они послужат вам хорошо.

5.5 Когда тестирование болезненно

Unit testing заставляет вас почувствовать боль плохого дизайна спереди - Michael Feathers

Когда вы пишете unit тесты, вы заставляете себя фактически использовать класс для достижения вещей. Если вы пишете тесты в конце или, что хуже, просто бросаете код через стену для QA или кого-то, чтобы написать тесты, вы не получаете никакой обратной связи о том, как класс на самом деле себя ведет. Если мы пишем тесты и класс - настоящая боль в использовании, мы узнаем об этом, пока пишем его, что почти самое дешевое время, чтобы исправить это.

Если класс трудно протестировать, это flaw дизайна. Разные недостатки проявляют себя по-разному, хотя. Если вам приходится делать тонну mocking, ваш класс, вероятно, имеет слишком много зависимостей или ваши методы делают слишком много. Чем больше настройки вам приходится делать для каждого теста, тем больше вероятность, что ваши методы делают слишком много. Если вам приходится писать очень запутанные сценарии тестов, чтобы проверить поведение, методы класса, вероятно, делают слишком много. Если вам приходится копаться внутри кучи приватных методов и состояния, чтобы протестировать вещи, возможно, другой класс пытается выбраться. Unit testing очень хорош в разоблачении "iceberg классов", где 80% того, что делает класс, спрятано в защищенном или приватном коде. Раньше я был большим поклонником делать как можно больше защищенным, но теперь я понял, что просто делал свои индивидуальные классы responsible за слишком многое, и настоящее решение - разбить класс на меньшие куски.

Написано Brian Fenton - Brian Fenton - PHP-разработчик в течение 8 лет в Среднем Западе и Bay Area, в настоящее время в Thismoment. Он фокусируется на craftsmanship кода и принципах дизайна. Блог на www.brianfenton.us, Twitter на @brianfenton. Когда он не занят отцом, он наслаждается едой, пивом, играми и обучением.

Learn/security

Безопасность

Обзор

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

Понимание

Существует ряд распространенных угроз безопасности, о которых вы должны знать при создании веб-приложений. Некоторые из наиболее распространенных угроз включают:

Шаблоны помогают с XSS, экранируя вывод по умолчанию, чтобы вам не приходилось об этом помнить. Сессии могут помочь с CSRF, храня токен CSRF в сессии пользователя, как описано ниже. Использование подготовленных запросов с PDO может помочь предотвратить атаки инъекции SQL (или использование удобных методов в классе 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 или где у вас есть маршруты
// К сведению, эта пустая строка группы действует как глобальный middleware для
// всех маршрутов. Конечно, вы можете сделать то же самое и добавить
// это только к конкретным маршрутам.
Flight::group('', function(Router $router) {
    $router->get('/users', [ 'UserController', 'getUsers' ]);
    // больше маршрутов
}, [ SecurityHeadersMiddleware::class ]);

Межсайтовый запрос подделки (CSRF)

Межсайтовый запрос подделки (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);
        }
    }
});

Межсайтовый скриптинг (XSS)

Межсайтовый скриптинг (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

Инъекция SQL — это тип атаки, при которой вредоносный пользователь может внедрить SQL-код в вашу базу данных. Это может быть использовано для кражи информации из вашей базы данных или выполнения действий в вашей базе данных. Снова вы никогда не должны доверять вводу от ваших пользователей! Всегда предполагайте, что они жаждут крови. Вы можете использовать подготовленные запросы в ваших объектах PDO, чтобы предотвратить инъекцию SQL.

// Предполагая, что у вас зарегистрирован 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, которая вернет всех пользователей.

var_dump($users); // это выведет всех пользователей в базе данных, а не только одного с конкретным именем пользователя

CORS

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

Примечание: Хотите узнать больше о маршрутизации? Посмотрите страницу "почему фреймворк?" для более подробного объяснения.

Базовое использование

Определение простого маршрута

Базовая маршрутизация в 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 () с позиционными параметрами не поддерживается. Ex: :'\(

Важное предупреждение

Хотя в примере выше кажется, что @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 (Ex: 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 и т.д.), единственный тип маршрутов, где это доступно, — это либо прямое создание объекта самостоятельно с использованием контейнера для создания вашего объекта, либо вы можете использовать строки для определения класса и метода для вызова. Вы можете перейти на страницу Внедрение зависимостей для получения дополнительной информации.

Вот быстрый пример:


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

Передача выполнения следующему маршруту

Устарело Вы можете передать выполнение следующему соответствующему маршруту, вернув 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

См. больше деталей на странице групповой 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
    {
    }
}

Примечание: Вы можете просмотреть newly added routes с помощью 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() на маршруте. Если вы делаете это, вы должны установить все заголовки вручную перед выводом чего-либо клиенту. Это делается с помощью функции php header() или метода 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 и 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, расширив ваше приложение через extending. Вот пример:

// 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). См. раздел basic usage о том, как это сделать.

Кроме того, есть некоторые полезные значения по умолчанию, которые облегчат вам жизнь при использовании Flight.

Экземпляр Engine

Если вы используете экземпляр Engine в ваших контроллерах/middleware, вот как вы бы его настроили:


// Где-то в вашем bootstrap-файле
$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. Затем мы покажем вам практический пример того, как это функционирует.

Запрос пользователя по URL /api ----> 
    Middleware->before() выполняется ----->
        Вызываемый метод, прикреплённый к /api, выполняется, и ответ генерируется ------>
    Middleware->after() выполняется ----->
Пользователь получает ответ от сервера

А вот практический пример:

Пользователь переходит по URL /dashboard
    LoggedInMiddleware->before() выполняется
        before() проверяет наличие действительной сессии входа
            если да, ничего не делать и продолжить выполнение
            если нет, перенаправить пользователя на /login
                Вызываемый метод, прикреплённый к /api, выполняется, и ответ генерируется
    LoggedInMiddleware->after() ничего не определено, поэтому позволяет выполнению продолжиться
Пользователь получает HTML дашборда от сервера

Порядок выполнения

Функции 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();

// Это выведет "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); 
// также ->addMiddleware([ $MyMiddleware, $MyMiddleware2 ]);

Flight::start();

// Это отобразит "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 может быть передан или нет
        $jobId = $params['jobId'] ?? 0;

        // возможно, если нет ID задания, вам не нужно ничего искать.
        if($jobId === 0) {
            return;
        }

        // выполнить поиск какого-то рода в вашей базе данных
        $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) {

    // Эта группа ниже всё ещё получает middleware родителя
    // Но параметры передаются в одном единственном массиве 
    // в middleware.
    $router->group('/job/@jobId', function(Router $router) {
        $router->get('', [ JobController::class, 'view' ]);
        $router->put('', [ JobController::class, 'update' ]);
        $router->delete('', [ JobController::class, 'delete' ]);
        // больше маршрутов...
    });
}, [ RouteSecurityMiddleware::class ]);

Группировка маршрутов с middleware

Вы можете добавить группу маршрутов, и каждый маршрут в этой группе будет иметь одинаковый middleware. Это полезно, если вам нужно сгруппировать множество маршрутов, например, с помощью middleware Auth для проверки API-ключа в заголовке.


// добавлено в конце метода группы
Flight::group('/api', function() {

    // Этот "пустой" маршрут на самом деле соответствует /api
    Flight::route('', function() { echo 'api'; }, false, 'api');
    // Это соответствует /api/users
    Flight::route('/users', function() { echo 'users'; }, false, 'users');
    // Это соответствует /api/users/1234
    Flight::route('/users/@id', function($id) { echo 'user:'.$id; }, false, 'user_view');
}, [ new ApiAuthMiddleware() ]);

Если вы хотите применить глобальный middleware ко всем вашим маршрутам, вы можете добавить "пустую" группу:


// добавлено в конце метода группы
Flight::group('', function() {

    // Это всё ещё /users
    Flight::route('/users', function() { echo 'users'; }, false, 'users');
    // И это всё ещё /users/1234
    Flight::route('/users/@id', function($id) { echo 'user:'.$id; }, false, 'user_view');
}, [ ApiAuthMiddleware::class ]); // или [ new ApiAuthMiddleware() ], одно и то же

Распространённые случаи использования

Валидация 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);

        // выполнить поиск в вашей базе данных для API-ключа
        $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' ]);
    // больше маршрутов...
}, [ 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' ]);
    // больше маршрутов...
}, [ 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'];

        // выполнить поиск какого-то рода в вашей базе данных
        $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' ]);
    // больше маршрутов...
}, [ 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;
        }

        // поскольку это true, всё просто продолжается
    }
}

Пример перенаправления

Вот пример перенаправления пользователя на страницу входа:

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);
            // или
            Flight::json(['error' => 'You must be logged in to access this page.'], 403);
            exit;
            // или
            Flight::halt(403, json_encode(['error' => 'You must be logged in to access this page.']);
        }
    }
}

См. также

Устранение неисправностей

Журнал изменений

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

// Добавьте фильтр before
Flight::before('hello', function (array &$params, string &$output): bool {
  // Манипулируйте параметром
  $params[0] = 'Fred';
  return true;
});

// Добавьте фильтр after
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 и т.д. в ваш проект. Вы можете захватить эти заголовки (язык браузера, тип сжатия, который они могут обрабатывать, пользовательский агент и т.д.) и захватить тело и 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

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"}, /* больше пользователей */ ]

Примечание: По умолчанию 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);
        // нет выхода; здесь не нужно.
    }

    // Продолжите с остальной частью маршрута
});

До 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() вы теперь можете подключаться к ключевым моментам жизненного цикла вашего приложения или определять свои собственные события (например, уведомления и emails), чтобы сделать ваш код более модульным и расширяемым. Эти методы являются частью mappable methods в Flight, что означает, что вы можете переопределить их поведение в соответствии с вашими потребностями.

Понимание

События позволяют разделять разные части вашего приложения, чтобы они не зависели друг от друга слишком сильно. Это разделение — часто называемое decoupling — делает ваш код проще для обновления, расширения или отладки. Вместо того чтобы писать всё в одном большом блоке, вы можете разделить логику на меньшие, независимые части, которые реагируют на конкретные действия (события).

Представьте, что вы строите приложение для блога:

Без событий вы бы запихнули всё это в одну функцию. С событиями вы можете разделить: одна часть сохраняет комментарий, другая вызывает событие вроде '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 "Welcome back, $username!";

    // you can send an email if the login is from a new location
});

Здесь, когда событие 'user.login' вызывается, оно приветствует пользователя по имени и может также включать логику для отправки email, если нужно.

Примечание: Callback может быть функцией, анонимной функцией или методом из класса.

Вызов событий

Чтобы событие произошло, используйте Flight::triggerEvent(). Это говорит Flight выполнить все слушатели, зарегистрированные для этого события, передавая любые данные, которые вы предоставите.

Flight::triggerEvent(string $event, ...$args): void

Простой пример

$username = 'alice';
Flight::triggerEvent('user.login', $username);

Это вызывает событие 'user.login' и отправляет 'alice' слушателю, который мы определили ранее, что выведет: Welcome back, alice!.

Остановка событий

Если слушатель возвращает false, дополнительные слушатели для этого события не будут выполнены. Это позволяет остановить цепочку событий на основе конкретных условий. Помните, порядок слушателей имеет значение, поскольку первый, вернувший false, остановит остальные.

Пример:

Flight::onEvent('user.login', function ($username) {
    if (isBanned($username)) {
        logoutUser($username);
        return false; // Stops subsequent listeners
    }
});
Flight::onEvent('user.login', function ($username) {
    sendWelcomeEmail($username); // this is never sent
});

Переопределение методов событий

Flight::onEvent() и Flight::triggerEvent() доступны для расширения, что означает, что вы можете переопределить, как они работают. Это отлично для продвинутых пользователей, которые хотят кастомизировать систему событий, например, добавляя логирование или изменяя, как события диспетчеризуются.

Пример: Кастомизация onEvent

Flight::map('onEvent', function (string $event, callable $callback) {
    // Log every event registration
    error_log("New event listener added for: $event");
    // Call the default behavior (assuming an internal event system)
    Flight::_onEvent($event, $callback);
});

Теперь каждый раз, когда вы регистрируете событие, оно логируется перед продолжением.

Почему переопределять?

Куда размещать события

Если вы новичок в концепциях событий в вашем проекте, вы можете задаться вопросом: куда мне регистрировать все эти события в приложении? Простота Flight означает, что нет строгого правила — вы можете размещать их там, где это имеет смысл для вашего проекта. Однако, поддерживая их организованными, вы помогаете поддерживать код по мере роста приложения. Вот несколько практических вариантов и лучших практик, адаптированных к лёгковесной природе Flight:

Вариант 1: В основном index.php

Для маленьких приложений или быстрых прототипов вы можете регистрировать события прямо в файле index.php рядом с маршрутами. Это держит всё в одном месте, что нормально, когда простота — ваш приоритет.

require 'vendor/autoload.php';

// Register events
Flight::onEvent('user.login', function ($username) {
    error_log("$username logged in at " . date('Y-m-d H:i:s'));
});

// Define routes
Flight::route('/login', function () {
    $username = 'bob';
    Flight::triggerEvent('user.login', $username);
    echo "Logged in!";
});

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 logged in at " . date('Y-m-d H:i:s'));
});

Flight::onEvent('user.registered', function ($email, $name) {
    echo "Email sent to $email: Welcome, $name!";
});
// index.php
require 'vendor/autoload.php';
require 'app/config/events.php';

Flight::route('/login', function () {
    $username = 'bob';
    Flight::triggerEvent('user.login', $username);
    echo "Logged in!";
});

Flight::start();

Вариант 3: Рядом с местом вызова

Другой подход — регистрировать события близко к месту их вызова, например, внутри контроллера или определения маршрута. Это хорошо работает, если событие специфично для одной части приложения.

Flight::route('/signup', function () {
    // Register event here
    Flight::onEvent('user.registered', function ($email) {
        echo "Welcome email sent to $email!";
    });

    $email = 'jane@example.com';
    Flight::triggerEvent('user.registered', $email);
    echo "Signed up!";
});

Лучшая практика для Flight

Совет: Группируйте по назначению

В events.php группируйте связанные события (например, все события, связанные с пользователем, вместе) с комментариями для ясности:

// app/config/events.php
// User Events
Flight::onEvent('user.login', function ($username) {
    error_log("$username logged in");
});
Flight::onEvent('user.registered', function ($email) {
    echo "Welcome to $email!";
});

// Page Events
Flight::onEvent('page.updated', function ($pageId) {
    Flight::cache()->delete("page_$pageId");
});

Эта структура хорошо масштабируется и остаётся дружелюбной для новичков.

Реальные примеры

Давайте пройдёмся по некоторым реальным сценариям, чтобы показать, как работают события и почему они полезны.

Пример 1: Логирование входа пользователя

// Step 1: Register a listener
Flight::onEvent('user.login', function ($username) {
    $time = date('Y-m-d H:i:s');
    error_log("$username logged in at $time");
});

// Step 2: Trigger it in your app
Flight::route('/login', function () {
    $username = 'bob'; // Pretend this comes from a form
    Flight::triggerEvent('user.login', $username);
    echo "Hi, $username!";
});

Почему полезно: Код входа не нуждается в знании о логировании — он просто вызывает событие. Вы можете позже добавить больше слушателей (например, отправить приветственный email) без изменения маршрута.

Пример 2: Уведомление о новых пользователях

// Listener for new registrations
Flight::onEvent('user.registered', function ($email, $name) {
    // Simulate sending an email
    echo "Email sent to $email: Welcome, $name!";
});

// Trigger it when someone signs up
Flight::route('/signup', function () {
    $email = 'jane@example.com';
    $name = 'Jane';
    Flight::triggerEvent('user.registered', $email, $name);
    echo "Thanks for signing up!";
});

Почему полезно: Логика регистрации сосредоточена на создании пользователя, в то время как событие обрабатывает уведомления. Вы можете добавить больше слушателей (например, логировать регистрацию) позже.

Пример 3: Очистка кэша

// Listener to clear a cache
Flight::onEvent('page.updated', function ($pageId) {
    // if using the flightphp/cache plugin
    Flight::cache()->delete("page_$pageId");
    echo "Cache cleared for page $pageId.";
});

// Trigger when a page is edited
Flight::route('/edit-page/(@id)', function ($pageId) {
    // Pretend we updated the page
    Flight::triggerEvent('page.updated', $pageId);
    echo "Page $pageId updated.";
});

Почему полезно: Код редактирования не заботится о кэшировании — он просто сигнализирует об обновлении. Другие части приложения могут реагировать по необходимости.

Лучшие практики

Система событий в Flight PHP с Flight::onEvent() и Flight::triggerEvent() даёт вам простой, но мощный способ строить гибкие приложения. Позволяя разным частям приложения общаться друг с другом через события, вы можете держать код организованным, переиспользуемым и простым для расширения. Будь то логирование действий, отправка уведомлений или управление обновлениями, события помогают делать это без запутывания логики. Плюс, с возможностью переопределения этих методов, у вас есть свобода адаптировать систему под ваши нужды. Начните с одного события и посмотрите, как оно трансформирует структуру вашего приложения!

Встроенные события

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

Список встроенных событий

См. также

Устранение неисправностей

Журнал изменений

Learn/templates

HTML-шаблоны и представления

Обзор

Flight предоставляет базовую функциональность шаблонизации HTML по умолчанию. Шаблонизация — это очень эффективный способ отделить логику приложения от слоя представления.

Понимание

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

Базовое использование

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 на свой собственный пользовательский класс:

// создание вашего пользовательского класса Router
class MyRouter extends \flight\net\Router {
    // переопределение методов здесь
    // например, сокращение для GET-запросов, чтобы удалить
    // функцию передачи маршрута
    public function get($pattern, $callback, $alias = '') {
        return parent::get($pattern, $callback, false, $alias);
    }
}

// Регистрация вашего пользовательского класса
Flight::register('router', MyRouter::class);

// Когда Flight загружает экземпляр Router, он загрузит ваш класс
$myRouter = Flight::router();
$myRouter->get('/hello', function() {
  echo "Hello World!";
}, 'hello_alias');

Однако методы фреймворка, такие как map и register, нельзя переопределять. Вы получите ошибку, если попытаетесь это сделать (см. ниже для списка методов).

Отображаемые методы фреймворка

Ниже приведен полный набор методов для фреймворка. Он состоит из основных методов, которые являются обычными статическими методами, и расширяемых методов, которые являются отображенными методами, которые можно фильтровать или переопределять.

Основные методы

Эти методы являются основными для фреймворка и не могут быть переопределены.

Flight::map(string $name, callable $callback, bool $pass_route = false) // Создает пользовательский метод фреймворка.
Flight::register(string $name, string $class, array $params = [], ?callable $callback = null) // Регистрирует класс для метода фреймворка.
Flight::unregister(string $name) // Отменяет регистрацию класса для метода фреймворка.
Flight::before(string $name, callable $callback) // Добавляет фильтр перед методом фреймворка.
Flight::after(string $name, callable $callback) // Добавляет фильтр после метода фреймворка.
Flight::path(string $path) // Добавляет путь для автозагрузки классов.
Flight::get(string $key) // Получает переменную, установленную Flight::set().
Flight::set(string $key, mixed $value) // Устанавливает переменную внутри движка Flight.
Flight::has(string $key) // Проверяет, установлена ли переменная.
Flight::clear(array|string $key = []) // Очищает переменную.
Flight::init() // Инициализирует фреймворк с настройками по умолчанию.
Flight::app() // Получает экземпляр объекта приложения
Flight::request() // Получает экземпляр объекта запроса
Flight::response() // Получает экземпляр объекта ответа
Flight::router() // Получает экземпляр объекта маршрутизатора
Flight::view() // Получает экземпляр объекта представления

Расширяемые методы

Flight::start() // Запускает фреймворк.
Flight::stop() // Останавливает фреймворк и отправляет ответ.
Flight::halt(int $code = 200, string $message = '') // Останавливает фреймворк с опциональным кодом статуса и сообщением.
Flight::route(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') // Отображает шаблон URL на callback.
Flight::post(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') // Отображает шаблон URL POST-запроса на callback.
Flight::put(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') // Отображает шаблон URL PUT-запроса на callback.
Flight::patch(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') // Отображает шаблон URL PATCH-запроса на callback.
Flight::delete(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') // Отображает шаблон URL DELETE-запроса на callback.
Flight::group(string $pattern, callable $callback) // Создает группировку для URL, шаблон должен быть строкой.
Flight::getUrl(string $name, array $params = []) // Генерирует URL на основе псевдонима маршрута.
Flight::redirect(string $url, int $code) // Перенаправляет на другой URL.
Flight::download(string $filePath) // Скачивает файл.
Flight::render(string $file, array $data, ?string $key = null) // Рендерит файл шаблона.
Flight::error(Throwable $error) // Отправляет ответ HTTP 500.
Flight::notFound() // Отправляет ответ HTTP 404.
Flight::etag(string $id, string $type = 'string') // Выполняет кэширование HTTP ETag.
Flight::lastModified(int $time) // Выполняет кэширование HTTP последнего изменения.
Flight::json(mixed $data, int $code = 200, bool $encode = true, string $charset = 'utf8', int $option) // Отправляет JSON-ответ.
Flight::jsonp(mixed $data, string $param = 'jsonp', int $code = 200, bool $encode = true, string $charset = 'utf8', int $option) // Отправляет JSONP-ответ.
Flight::jsonHalt(mixed $data, int $code = 200, bool $encode = true, string $charset = 'utf8', int $option) // Отправляет JSON-ответ и останавливает фреймворк.
Flight::onEvent(string $event, callable $callback) // Регистрирует слушатель события.
Flight::triggerEvent(string $event, ...$args) // Запускает событие.

Любые пользовательские методы, добавленные с помощью map и register, также могут быть отфильтрованы. Для примеров того, как фильтровать эти методы, см. руководство Filtering Methods.

Расширяемые классы фреймворка

Есть несколько классов, функциональность которых вы можете переопределить, расширив их и регистрируя свой собственный класс. Эти классы:

Flight::app() // Класс приложения — расширьте класс flight\Engine
Flight::request() // Класс запроса — расширьте класс flight\net\Request
Flight::response() // Класс ответа — расширьте класс flight\net\Response
Flight::router() // Класс маршрутизатора — расширьте класс flight\net\Router
Flight::view() // Класс представления — расширьте класс flight\template\View
Flight::eventDispatcher() // Класс диспетчера событий — расширьте класс flight\core\Dispatcher

Отображение пользовательских методов

Чтобы отобразить свой собственный простой пользовательский метод, вы используете функцию map:

// Отображение вашего метода
Flight::map('hello', function (string $name) {
  echo "hello $name!";
});

// Вызов вашего пользовательского метода
Flight::hello('Bob');

Хотя возможно создавать простые пользовательские методы, рекомендуется просто создавать стандартные функции в PHP. Это обеспечивает автодополнение в IDE и легче читается. Эквивалент приведенного выше кода будет:

function hello(string $name) {
  echo "hello $name!";
}

hello('Bob');

Это используется чаще, когда вам нужно передавать переменные в ваш метод, чтобы получить ожидаемое значение. Использование метода register(), как ниже, больше подходит для передачи конфигурации, а затем вызова вашего предварительно настроенного класса.

Регистрация пользовательских классов

Чтобы зарегистрировать свой собственный класс и настроить его, вы используете функцию register. Преимущество этого над map() заключается в том, что вы можете повторно использовать тот же класс при вызове этой функции (это будет полезно с Flight::db(), чтобы делить один и тот же экземпляр).

// Регистрация вашего класса
Flight::register('user', User::class);

// Получение экземпляра вашего класса
$user = Flight::user();

Метод register также позволяет передавать параметры конструктору вашего класса. Таким образом, когда вы загружаете свой пользовательский класс, он будет предварительно инициализирован. Вы можете определить параметры конструктора, передав дополнительный массив. Вот пример загрузки соединения с базой данных:

// Регистрация класса с параметрами конструктора
Flight::register('db', PDO::class, ['mysql:host=localhost;dbname=test', 'user', 'pass']);

// Получение экземпляра вашего класса
// Это создаст объект с заданными параметрами
//
// new PDO('mysql:host=localhost;dbname=test','user','pass');
//
$db = Flight::db();

// и если вам понадобится это позже в вашем коде, вы просто вызываете тот же метод снова
class SomeController {
  public function __construct() {
    $this->db = Flight::db();
  }
}

Если вы передадите дополнительный параметр callback, он будет выполнен сразу после создания класса. Это позволяет выполнить любые процедуры настройки для вашего нового объекта. Функция callback принимает один параметр — экземпляр нового объекта.

// Callback будет передан объект, который был создан
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 = Flight::db();

// Новый экземпляр класса
$new = Flight::db(false);

Примечание: Помните, что отображенные методы имеют приоритет над зарегистрированными классами. Если вы объявите оба с одним и тем же именем, будет вызван только отображенный метод.

Примеры

Вот несколько примеров того, как вы можете расширить Flight функциональностью, которая не встроена в ядро.

Логирование

Flight не имеет встроенной системы логирования, однако очень легко использовать библиотеку логирования с Flight. Вот пример с использованием библиотеки Monolog:

// services.php

// Регистрация логгера с 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));
});

Теперь, когда он зарегистрирован, вы можете использовать его в своем приложении:

// В вашем контроллере или маршруте
Flight::log()->warning('This is a warning message');

Это запишет сообщение в указанный вами файл лога. Что, если вы хотите записать что-то, когда происходит ошибка? Вы можете использовать метод error:

// В вашем контроллере или маршруте
Flight::map('error', function(Throwable $ex) {
    Flight::log()->error($ex->getMessage());
    // Отображение вашей пользовательской страницы ошибки
    include 'errors/500.html';
});

Вы также можете создать базовую систему APM (Application Performance Monitoring), используя методы before и after:

// В вашем файле services.php

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

    // Вы также можете добавить заголовки запроса или ответа
    // для их логирования (будьте осторожны, поскольку это будет много 
    // данных, если у вас много запросов)
    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

// Регистрация кэша с Flight
Flight::register('cache', \flight\Cache::class, [ __DIR__ . '/../cache/' ], function(\flight\Cache $cache) {
    $cache->setDevMode(ENVIRONMENT === 'development');
});

Теперь, когда он зарегистрирован, вы можете использовать его в своем приложении:

// В вашем контроллере или маршруте
$data = Flight::cache()->get('my_cache_key');
if (empty($data)) {
    // Выполните некоторую обработку, чтобы получить данные
    $data = [ 'some' => 'data' ];
    Flight::cache()->set('my_cache_key', $data, 3600); // кэшировать на 1 час
}

Легкая инстанциация объектов DIC

Если вы используете DIC (Dependency Injection Container) в своем приложении, вы можете использовать Flight, чтобы помочь вам инстанцировать ваши объекты. Вот пример с использованием библиотеки Dice:

// services.php

// создание нового контейнера
$container = new \Dice\Dice;
// не забудьте переприсвоить его самому себе, как ниже!
$container = $container->addRule('PDO', [
    // shared означает, что тот же объект будет возвращен каждый раз
    'shared' => true,
    'constructParams' => ['mysql:host=localhost;dbname=test', 'user', 'pass' ]
]);

// теперь мы можем создать отображаемый метод для создания любого объекта. 
Flight::map('make', function($class, $params = []) use ($container) {
    return $container->create($class, $params);
});

// Это регистрирует обработчик контейнера, чтобы Flight знал, как использовать его для контроллеров/промежуточного ПО
Flight::registerContainerHandler(function($class, $params) {
    Flight::make($class, $params);
});

// предположим, у нас есть следующий пример класса, который принимает объект PDO в конструкторе
class EmailCron {
    protected PDO $pdo;

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

    public function send() {
        // код, который отправляет email
    }
}

// И наконец, вы можете создавать объекты с использованием dependency injection
$emailCron = Flight::make(EmailCron::class);
$emailCron->send();

Круто, правда?

См. также

Устранение неполадок

Журнал изменений

Learn/json

Обёртка JSON

Обзор

Класс 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;
// Вывод: {"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; // Вывод: Flight

Если вы хотите ассоциативный массив вместо объекта, передайте true в качестве второго аргумента:

$data = Json::decode($json, true);
echo $data['framework']; // Вывод: Flight

Если декодирование не удастся, вы получите исключение с ясным сообщением об ошибке.

Валидация JSON

Проверьте, является ли строка валидным JSON:

if (Json::isValid($json)) {
  // Это валидно!
} else {
  // Не валидный JSON
}

Получение последней ошибки

Если вы хотите проверить последнее сообщение об ошибке JSON (из встроенных функций PHP):

$error = Json::getLastError();
if ($error !== '') {
  echo "Последняя ошибка JSON: $error";
}

Расширенное использование

Вы можете настроить опции кодирования и декодирования, если вам нужно больше контроля (см. опции json_encode в PHP):

// Кодирование с опцией HEX_TAG
$json = Json::encode($data, JSON_HEX_TAG);

// Декодирование с пользовательской глубиной
$data = Json::decode($json, false, 1024);

См. также

Устранение неисправностей

Журнал изменений

Learn/flight_vs_slim

Flight vs Slim

Что такое Slim?

Slim — это PHP-микрофреймворк, который помогает быстро писать простые, но мощные веб-приложения и API.

Много вдохновения для некоторых функций версии 3 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

Unit Testing в Flight PHP с PHPUnit

Этот гид вводит в unit testing в Flight PHP с использованием PHPUnit, предназначен для начинающих, которые хотят понять почему unit testing важен и как применять его на практике. Мы сосредоточимся на тестировании поведения — обеспечении того, что ваше приложение делает то, что ожидается, например, отправка email или сохранение записи — вместо тривиальных вычислений. Мы начнем с простого route handler и перейдем к более сложному controller, включая dependency injection (DI) и mocking сторонних сервисов.

Почему Unit Test?

Unit testing обеспечивает, что ваш код ведет себя как ожидается, ловит баги до того, как они попадут в production. Это особенно ценно в Flight, где легковесный routing и гибкость могут привести к сложным взаимодействиям. Для solo-разработчиков или команд unit tests действуют как safety net, документируя ожидаемое поведение и предотвращая регрессии при возвращении к коду позже. Они также улучшают дизайн: код, который трудно тестировать, часто сигнализирует о чрезмерной сложности или тесной связанности классов.

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

Общие Руководящие Принципы

  1. Тестируйте Поведение, Не Реализацию: Сосредоточьтесь на результатах (например, «email отправлен» или «запись сохранена») вместо внутренних деталей. Это делает тесты устойчивыми к рефакторингу.
  2. Перестаньте использовать Flight::: Статические методы Flight невероятно удобны, но усложняют тестирование. Вы должны привыкнуть использовать переменную $app из $app = Flight::app();. $app имеет все те же методы, что и Flight::. Вы все еще сможете использовать $app->route() или $this->app->json() в вашем controller и т.д. Также вы должны использовать реальный Flight router с $router = $app->router() и затем вы сможете использовать $router->get(), $router->post(), $router->group() и т.д. См. Routing.
  3. Держите Тесты Быстрыми: Быстрые тесты поощряют частое выполнение. Избегайте медленных операций, таких как вызовы базы данных в unit tests. Если у вас есть медленный тест, это признак, что вы пишете integration test, а не unit test. Integration tests — это когда вы действительно вовлекаете реальные базы данных, реальные HTTP-вызовы, реальную отправку email и т.д. У них есть свое место, но они медленные и могут быть flaky, то есть иногда падают по неизвестной причине.
  4. Используйте Описательные Имена: Имена тестов должны четко описывать тестируемое поведение. Это улучшает читаемость и поддерживаемость.
  5. Избегайте Globals Как Чумы: Минимизируйте использование $app->set() и $app->get(), поскольку они действуют как global state, требуя mocks в каждом тесте. Предпочитайте DI или DI container (см. Dependency Injection Container). Даже использование метода $app->map() технически является "global" и должно избегаться в пользу DI. Используйте библиотеку сессий, такую как flightphp/session, чтобы вы могли mock объект сессии в ваших тестах. Не вызывайте $_SESSION напрямую в вашем коде, поскольку это внедряет global variable в ваш код, усложняя тестирование.
  6. Используйте Dependency Injection: Внедряйте зависимости (например, PDO, mailers) в controllers для изоляции логики и упрощения mocking. Если у вас есть класс с слишком многими зависимостями, рассмотрите рефакторинг его в меньшие классы, каждый с одной ответственностью, следуя SOLID principles.
  7. Mock Сторонние Сервисы: Mock базы данных, HTTP-клиенты (cURL) или email-сервисы, чтобы избежать внешних вызовов. Тестируйте на один-два уровня в глубину, но позволяйте вашей основной логике работать. Например, если ваше приложение отправляет SMS, вы НЕ хотите реально отправлять SMS каждый раз, когда запускаете тесты, потому что эти расходы накопятся (и это будет медленнее). Вместо этого mock сервис SMS и просто проверьте, что ваш код вызвал сервис SMS с правильными параметрами.
  8. Стремитесь к Высокому Покрытию, Не к Совершенству: 100% покрытие строк хорошо, но это не значит, что все в вашем коде протестировано правильно (погуглите branch/path coverage в PHPUnit). Приоритизируйте критические поведения (например, регистрацию пользователя, ответы API и захват неудачных ответов).
  9. Используйте Controllers для Routes: В ваших определениях routes используйте controllers, а не closures. flight\Engine $app по умолчанию внедряется в каждый controller через конструктор. В тестах используйте $app = new Flight\Engine() для инстанцирования Flight в тесте, внедрите его в ваш controller и вызывайте методы напрямую (например, $controller->register()). См. Extending Flight и Routing.
  10. Выберите Стиль Mocking и Придерживайтесь Его: PHPUnit поддерживает несколько стилей mocking (например, prophecy, встроенные mocks), или вы можете использовать anonymous classes, которые имеют свои преимущества, такие как code completion, поломка при изменении определения метода и т.д. Просто будьте последовательны в ваших тестах. См. PHPUnit Mock Objects.
  11. Используйте protected visibility для методов/свойств, которые вы хотите тестировать в подклассах: Это позволяет переопределять их в тестовых подклассах без их публичности, это особенно полезно для anonymous class mocks.

Настройка PHPUnit

Сначала настройте PHPUnit в вашем проекте Flight PHP с использованием Composer для удобного тестирования. См. PHPUnit Getting Started guide для более подробной информации.

  1. В директории вашего проекта запустите:

    composer require --dev phpunit/phpunit

    Это установит последнюю версию PHPUnit как development dependency.

  2. Создайте директорию tests в корне вашего проекта для файлов тестов.

  3. Добавьте скрипт теста в composer.json для удобства:

    // other composer.json content
    "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 для выполнения тестов.

Тестирование Простого Route Handler

Давайте начнем с базового route, который валидирует 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, чтобы проверить, что route ведет себя как ожидается. Для большего количества информации о requests и responses в Flight см. соответствующие docs.

Использование Dependency Injection для Testable Controllers

Для более сложных сценариев используйте dependency injection (DI), чтобы сделать controllers testable. Избегайте globals Flight (например, Flight::set(), Flight::map(), Flight::register()), поскольку они действуют как global state, требуя mocks для каждого теста. Вместо этого используйте DI container Flight, DICE, PHP-DI или manual DI.

Давайте используем flight\database\PdoWrapper вместо raw PDO. Этот wrapper гораздо проще mock и unit test!

Вот controller, который сохраняет пользователя в базу данных и отправляет welcome 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']);
    }
}

Ключевые Моменты:

Тестирование Controller с Mocks

Теперь протестируем поведение UserController: валидацию email, сохранение в базу данных и отправку email. Мы замоким базу данных и mailer, чтобы изолировать controller.

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

class UserControllerDICTest extends TestCase {
    public function testValidEmailSavesAndSendsEmail() {

        // Иногда смешивание стилей mocking необходимо
        // Здесь мы используем встроенный mock PHPUnit для PDOStatement
        $statementMock = $this->createMock(PDOStatement::class);
        $statementMock->method('execute')->willReturn(true);
        // Используя anonymous class для mocking PdoWrapper
        $mockDb = new class($statementMock) extends PdoWrapper {
            protected $statementMock;
            public function __construct($statementMock) {
                $this->statementMock = $statementMock;
            }

            // Когда мы моким его таким образом, мы не делаем реальный вызов базы данных.
            // Мы можем дополнительно настроить это, чтобы изменить mock PDOStatement для симуляции сбоев и т.д.
            public function runQuery(string $sql, array $params = []): PDOStatement {
                return $this->statementMock;
            }
        };
        $mockMailer = new class implements MailerInterface {
            public $sentEmail = null;
            public function sendWelcome($email): bool {
                $this->sentEmail = $email;
                return true;    
            }
        };
        $app = new Engine();
        $app->request()->data->email = 'test@example.com';
        $controller = new UserControllerDIC($app, $mockDb, $mockMailer);
        $controller->register();
        $response = $app->response()->getBody();
        $result = json_decode($response, true);
        $this->assertEquals('success', $result['status']);
        $this->assertEquals('User registered', $result['message']);
        $this->assertEquals('test@example.com', $mockMailer->sentEmail);
    }

    public function testInvalidEmailSkipsSaveAndEmail() {
         $mockDb = new class() extends 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']);
    }
}

Ключевые Моменты:

Слишком Много Mocking

Будьте осторожны, чтобы не mock слишком много вашего кода. Позвольте мне дать пример ниже, почему это может быть плохой идеей, используя наш 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);
    }
}

И теперь overmocked unit test, который на самом деле ничего не тестирует:

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

Ура, у нас есть unit tests и они проходят! Но подождите, что если я на самом деле изменю внутреннюю работу 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;
    }
}

Если я запущу свои unit tests выше, они все равно пройдут! Но потому что я не тестировал поведение (на самом деле позволяя некоторому коду работать), я потенциально закодировал баг, ожидающий проявления в production. Тест должен быть модифицирован, чтобы учесть новое поведение, и также противоположность, когда поведение не то, что мы ожидаем.

Полный Пример

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

Распространенные Ошибки

Масштабирование с Unit Tests

Unit tests сияют в больших проектах или при возвращении к коду через месяцы. Они документируют поведение и ловят регрессии, спасая вас от повторного изучения вашего app. Для solo devs тестируйте критические пути (например, регистрацию пользователя, обработку платежей). Для команд тесты обеспечивают последовательное поведение среди вкладов. См. 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

Настоящим предоставляется разрешение на бесплатное использование любому лицу, получившему копию данного программного обеспечения и сопроводительной документации (далее - "Программное обеспечение"), без ограничений, включая право использовать, копировать, изменять, объединять, публиковать, распространять, подлицензировать и/или продавать копии Программного обеспечения и разрешать лицам, которым предоставляется Программное обеспечение, сделать то же самое, при соблюдении следующих условий:

Вышеприведенное уведомление об авторском праве и это уведомление о разрешении должны быть включены во все копии или существенные части Программного обеспечения.

ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ ПРЕДОСТАВЛЯЕТСЯ "КАК ЕСТЬ", БЕЗ КАКИХ-ЛИБО ГАРАНТИЙ, ВЫРАЖЕННЫХ ИЛИ ПОДРАЗУМЕВАЕМЫХ, ВКЛЮЧАЯ, НО НЕ ОГРАНИЧИВАЯСЬ ГАРАНТИЯМИ ТОВАРНОГО СОСТОЯНИЯ, ПРИГОДНОСТИ ДЛЯ КОНКРЕТНОЙ ЦЕЛИ И НЕНАРУШЕНИЯ. НИ В КОЕМ СЛУЧАЕ АВТОРЫ ИЛИ ПРАВООБЛАДАТЕЛИ НЕ НЕСУТ ОТВЕТСТВЕННОСТИ ПО ОТВЕТСТВЕННОСТИ, ВЫТЕКАЮЩЕЙ ИЗ ДОГОВОРА, ДЕЛИКТА ИЛИ ИНАЧЕ, В СВЯЗИ С ПРОГРАММНЫМ ОБЕСПЕЧЕНИЕМ ИЛИ ИСПОЛЬЗОВАНИЕМ ИЛИ ДРУГИМИ ОБРАЩЕНИЯМИ С ПРОГРАММНЫМ ОБЕСПЕЧЕНИЕМ.

About

Фреймворк PHP Flight

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)

Есть пример приложения, чтобы помочь вам начать проект с Flight. В нем есть структурированная разметка, базовые конфигурации и обработка сценариев Composer прямо из коробки! Посмотрите flightphp/skeleton для готового проекта или посетите страницу examples для вдохновения. Хотите увидеть, как вписывается ИИ? Изучите примеры на базе ИИ.

Установка примера приложения

Очень просто!

# Создайте новый проект
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 и ИИ

Интересно, как он работает с ИИ? Узнайте, как 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 является версией по умолчанию для некоторых дистрибутивов Linux с долгосрочной поддержкой. Принудительный переход на PHP >8 вызвал бы проблемы для пользователей. Фреймворк также поддерживает PHP >8.

Лицензия

Flight распространяется под MIT лицензией.

Awesome-plugins/php_cookie

Cookies

overclokk/cookie это простая библиотека для управления куки в вашем приложении.

Установка

Установка проста с помощью composer.

composer require overclokk/cookie

Использование

Использование так же просто, как регистрация нового метода в классе 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 require defuse/php-encryption

Настройка

Затем вам нужно сгенерировать ключ шифрования.

vendor/bin/generate-defuse-key

Это выдаст ключ, который вам нужно будет хранить в надежном месте. Вы можете сохранить ключ в вашем файле app/config/config.php в массиве внизу файла. Хотя это не идеальное место, это хотя бы что-то.

Использование

Теперь, когда у вас есть библиотека и ключ шифрования, вы можете начать шифровать и дешифровать данные.


use Defuse\Crypto\Crypto;
use Defuse\Crypto\Key;

/*
 * Set in your bootstrap or public/index.php file
 */

// Метод шифрования
Flight::map('encrypt', function($raw_data) {
    $encryption_key = /* $config['encryption_key'] or a file_get_contents of where you put the key */;
    return Crypto::encrypt($raw_data, Key::loadFromAsciiSafeString($encryption_key));
});

// Метод дешифрования
Flight::map('decrypt', function($encrypted_data) {
    $encryption_key = /* $config['encryption_key'] or a file_get_contents of where you put the key */;
    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"); // return data to be cached
}, 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')) {
    // do something
}

Очистить кэш

Вы используете метод flush() для очистки всего кэша.

Flight::cache()->flush();

Извлечь метаданные с кэшем

Если вы хотите извлечь временные метки и другие метаданные о записи кэша, убедитесь, что вы передаете true в качестве соответствующего параметра.

$data = $cache->refreshIfExpired("simple-cache-meta-test", function () {
    echo "Refreshing data!" . PHP_EOL;
    return date("H:i:s"); // return data to be cached
}, 10, true); // true = return with metadata
// или
$data = $cache->get("simple-cache-meta-test", true); // true = return with metadata

/*
Example cached item retrieved with metadata:
{
    "time":1511667506, <-- save unix timestamp
    "expire":10,       <-- expire time in seconds
    "data":"04:38:26", <-- unserialized data
    "permanent":false
}

Using metadata, we can, for example, calculate when item was saved or when it expires
We can also access the data itself with the "data" key
*/

$expiresin = ($data["time"] + $data["expire"]) - time(); // get unix timestamp when data expires and subtract current timestamp from it
$cacheddate = $data["data"]; // we access the data itself with the "data" key

echo "Latest cache save: $cacheddate, expires in $expiresin seconds";

Документация

Посетите https://github.com/flightphp/cache, чтобы просмотреть код. Убедитесь, что вы посмотрите папку examples для дополнительных способов использования кэша.

Awesome-plugins/permissions

FlightPHP/Права доступа

Это модуль разрешений, который можно использовать в ваших проектах, если у вас есть несколько ролей в вашем приложении, и каждая роль имеет немного разную функциональность. Этот модуль позволяет определить разрешения для каждой роли, а затем проверить, имеет ли текущий пользователь разрешение на доступ к определенной странице или выполнение определенного действия.

Нажмите сюда для репозитория на GitHub.

Установка

Запустите composer require flightphp/permissions и вы готовы к работе!

Использование

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

Базовый пример

Давайте предположим, что у вас есть функция в вашем приложении, которая проверяет, вошел ли пользователь в систему. Вы можете создать объект разрешений следующим образом:

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

// некоторый код

// затем у вас вероятно есть что-то, что говорит вам, какая текущая роль у человека
// скорее всего у вас есть что-то, откуда вы извлекаете текущую роль
// из переменной сеанса, которая определяет это
// после входа в систему у кого-то должна быть роль 'guest' или 'public'.
$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 {
            // сделать что-то еще
        }
    }
}

Внедрение зависимостей

Вы можете внедрять зависимости в замыкание, которое определяет разрешения. Это полезно, если у вас есть какой-то переключатель, идентификатор или любая другая точка данных, которую вы хотите проверить. То же самое работает для вызовов вида Class->Method, за исключением того, что аргументы определяются в методе.

Замыкания

$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 - это система управления процессами, которая обеспечивает постоянную работу ваших процессов рабочих. Вот более полное руководство по настройке его с вашим рабочим процессом Простой очереди задач:

Установка 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 Менеджер сессий (неблокирующий, flash, segment, шифрование сессий). Использует PHP open_ssl для необязательного шифрования/дешифрования данных сессий. Поддерживает File, MySQL, Redis и Memcached.

Нажмите здесь, чтобы просмотреть код.

Установка

Установите с помощью 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();
});

// Эта проверка может быть в логике ограниченной страницы или обернута в промежуточное ПО.
Flight::route('/some-restricted-page', function() {
    $session = Flight::session();

    if(!$session->get('is_logged_in')) {
        Flight::redirect('/login');
    }

    // выполните здесь логику ограниченной страницы
});

// версия с промежуточным ПО
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 (от dev 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 = 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);

Объект миграции контролирует версию базы данных.

Создание контроля версий в вашем проекте

<?php
// Зарегистрируйте Базу данных или Базы данных, которые могут обрабатывать этот URI:
\ByJG\DbMigration\Migration::registerDatabase(\ByJG\DbMigration\Database\MySqlDatabase::class);

// Создайте экземпляр миграции
$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";
});

Получение экземпляра драйвера Db

<?php
$migration->getDbDriver();

Чтобы использовать это, пожалуйста, посетите: https://github.com/byjg/anydataset-db

Избежание частичной миграции (недоступно для MySQL)

Частичная миграция возникает, когда скрипт миграции прерывается в середине процесса из-за ошибки или ручного прерывания.

Таблица миграции будет иметь статус partial up или partial down, и ее необходимо исправить вручную перед тем, как снова мигрировать.

Чтобы избежать этой ситуации, вы можете указать, что миграция будет выполняться в транзакционном контексте. Если скрипт миграции не удастся выполнить, транзакция будет отменена, а таблица миграции будет отмечена как complete, и версия будет сразу же предыдущей версией перед скриптом, который вызвал ошибку.

Чтобы включить эту функцию, вам необходимо вызвать метод withTransactionEnabled, передав true как параметр:

<?php
$migration->withTransactionEnabled(true);

ПРИМЕЧАНИЕ: Эта функция недоступна для MySQL, так как она не поддерживает DDL команды внутри транзакции. Если вы используете этот метод с MySQL, миграция проигнорирует его без уведомления. Дополнительная информация: https://dev.mysql.com/doc/refman/8.0/en/cannot-roll-back.html

Советы по написанию SQL миграций для Postgres

О создании триггеров и SQL функций

-- ДЕЛАЙТЕ
CREATE FUNCTION emp_stamp() RETURNS trigger AS $emp_stamp$
    BEGIN
        -- Проверьте, что empname и зарплата указаны
        IF NEW.empname IS NULL THEN
            RAISE EXCEPTION 'empname не может быть пустым'; -- не имеет значения, пустые ли эти комментарии
        END IF; --
        IF NEW.salary IS NULL THEN
            RAISE EXCEPTION '% не может иметь пустую зарплату', 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;

-- НЕ ДЕЛАЙТЕ
CREATE FUNCTION emp_stamp() RETURNS trigger AS $emp_stamp$
    BEGIN
        -- Проверьте, что empname и зарплата указаны
        IF NEW.empname IS NULL THEN
            RAISE EXCEPTION 'empname не может быть пустым';
        END IF;
        IF NEW.salary IS NULL THEN
            RAISE EXCEPTION '% не может иметь пустую зарплату', 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 начал разбивать файлы миграции по последовательности точка с запятой + EOL вместо обычной точки с запятой. Таким образом, если вы добавите пустой комментарий после каждой внутренней точки с запятой в определении функции, byjg/migration сможет корректно его разобрать.

К сожалению, если вы забудете добавить хотя бы один из этих комментариев, библиотека разобьет оператор CREATE FUNCTION на несколько частей, и миграция потерпит неудачу.

Избегайте символа двоеточия (:)

-- ДЕЛАЙТЕ
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
);

-- НЕ ДЕЛАЙТЕ
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 Session - Легковесный обработчик сессий на основе файлов

Это легковесный плагин для обработки сессий на основе файлов для Flight PHP Framework. Он предоставляет простое, но мощное решение для управления сессиями, с функциями, такими как неблокирующее чтение сессий, необязательное шифрование, автоматическая фиксация изменений и режим тестирования для разработки. Данные сессий хранятся в файлах, что идеально подходит для приложений, не требующих базы данных.

Если вы хотите использовать базу данных, ознакомьтесь с плагином ghostff/session, который имеет многие из этих функций, но с использованием базы данных.

Посетите репозиторий на Github для полного исходного кода и деталей.

Установка

Установите плагин через 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_id' => $session->get('user_id')]);
    }
});

Flight::route('/logout', function() {
    $session = Flight::session();
    $session->clear(); // Очистить все данные сессии
    Flight::json(['message' => 'Выход выполнен успешно']);
});

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 Кастомный 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'); // Дешифруется при извлечении
});

Регенерация сессии

Регенерируйте ID сессии для безопасности (например, после входа):

Flight::route('/post-login', function() {
    $session = Flight::session();
    $session->regenerate(); // Новый ID, сохранить данные
    // ИЛИ
    $session->regenerate(true); // Новый ID, удалить старые данные
});

Пример промежуточного ПО

Защитите маршруты с помощью аутентификации на основе сессий:

Flight::route('/admin', function() {
    Flight::json(['message' => 'Добро пожаловать в панель администратора']);
})->addMiddleware(function() {
    $session = Flight::session();
    if (!$session->get('is_admin')) {
        Flight::halt(403, 'Доступ запрещен');
    }
});

Это простой пример использования в промежуточном ПО. Для более подробного примера см. документацию по middleware.

Методы

Класс Session предоставляет эти методы:

Все методы, кроме get() и id(), возвращают экземпляр Session для цепочки вызовов.

Почему использовать этот плагин?

Технические детали

Вклад

Вклад приветствуется! Создайте форк репозитория, внесите изменения и отправьте пул-реквест. Сообщайте об ошибках или предлагайте функции через трекер задач на Github.

Лицензия

Этот плагин лицензирован по лицензии MIT. См. репозиторий на Github для деталей.

Awesome-plugins/runway

Взлетная полоса

Взлетная полоса — это приложение CLI, которое помогает управлять приложениями Flight. Он может генерировать контроллеры, отображать все маршруты и многое другое. Он основан на отличной библиотеке adhocore/php-cli.

Нажмите здесь, чтобы просмотреть код.

Установка

Установите через composer.

composer require flightphp/runway

Основная настройка

Первый раз, когда вы запускаете Взлетную полосу, она проведет вас через процесс настройки и создаст файл конфигурации .runway.json в корне вашего проекта. Этот файл будет содержать несколько необходимых конфигураций для работы Взлетной полосы должным образом.

Использование

У Взлетной полосы есть несколько команд, которые вы можете использовать для управления вашим приложением Flight. Есть два простых способа использования Взлетной полосы.

  1. Если вы используете каркас проекта, вы можете запустить php runway [команда] из корня вашего проекта.
  2. Если вы используете Взлетную полосу как пакет, установленный через composer, вы можете запустить vendor/bin/runway [команда] из корня вашего проекта.

Для любой команды вы можете передать флаг --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;

/**
 * Класс Active Record для таблицы пользователей.
 * @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();
// Возможно, вам нужно указать вашу среду с помощью Debugger::enable(Debugger::DEVELOPMENT)

// если вы используете подключения к базе данных в вашем приложении, есть
// обязательная обертка PDO, которую нужно использовать ТОЛЬКО В РАЗРАБОТКЕ (не в продакшене, пожалуйста!)
// Она имеет те же параметры, что и обычное подключение PDO
$pdo = new PdoQueryCapture('sqlite:test.db', 'user', 'pass');
// или если вы подключаете это к фреймворку Flight
Flight::register('db', PdoQueryCapture::class, ['sqlite:test.db', 'user', 'pass']);
// теперь каждый раз, когда вы выполняете запрос, он захватит время, запрос и параметры

// Это соединяет точки
if(Debugger::$showBar === true) {
    // Это должно быть false, иначе Tracy не сможет отобразиться :(
    Flight::set('flight.content_length', false);
    new TracyExtensionLoader(Flight::app());
}

// more code

Flight::start();

Дополнительная конфигурация

Данные сессии

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


use Ghostff\Session\Session;
// или используйте flight\Session;

require 'vendor/autoload.php';

$app = Flight::app();

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

if(Debugger::$showBar === true) {
    // Это должно быть false, иначе Tracy не сможет отобразиться :(
    Flight::set('flight.content_length', false);
    new TracyExtensionLoader(Flight::app(), [ 'session_data' => Flight::session()->getAll() ]);
}

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

    // добавляйте расширение только если панель отладки Tracy включена
    if(Debugger::$showBar === true) {
        // здесь вы добавляете панель Latte в Tracy
        $latte->addExtension(new Latte\Bridges\Tracy\TracyExtension);
    }

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

Awesome-plugins/apm

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

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

Посмотрите демо панели управления для сайта Flight Docs.

FlightPHP APM

Почему APM важен

Представьте: ваше приложение — это оживленный ресторан. Без способа отслеживать, сколько времени занимают заказы или где кухня тормозит, вы угадываете, почему клиенты уходят недовольными. APM — ваш су-шеф: он следит за каждым шагом, от входящих запросов до запросов к базе данных, и отмечает всё, что вас замедляет. Медленные страницы отпугивают пользователей (исследования говорят, что 53% уходят, если сайт загружается дольше 3 секунд!), а APM помогает поймать эти проблемы до того, как они укусят. Это проактивное спокойствие — меньше моментов «почему это сломано?», больше побед «смотрите, как круто это работает!».

Установка

Начните с Composer:

composer require flightphp/apm

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

Поддерживаемые базы данных

FlightPHP APM в настоящее время поддерживает следующие базы данных для хранения метрик:

Вы можете выбрать тип базы данных на шаге конфигурации (см. ниже). Убедитесь, что в вашей среде PHP установлены необходимые расширения (например, pdo_sqlite или pdo_mysql).

Начало работы

Вот ваш пошаговый путь к крутости APM:

1. Регистрация APM

Вставьте это в ваш index.php или файл services.php, чтобы начать отслеживание:

use flight\apm\logger\LoggerFactory;
use flight\Apm;

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

// Если вы добавляете подключение к базе данных
// Должно быть PdoWrapper или PdoQueryCapture из расширений Tracy
$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. Обработка метрик с помощью воркера

Воркер превращает сырые метрики в данные, готовые для панели. Запустите его один раз:

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

Что вы получаете:

Внимание:

Пример вывода:

Опции воркера

Настройте воркер по вкусу:

Пример:

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

Работает час, обрабатывая 100 метрик за раз.

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

Трейси - удивительный обработчик ошибок, который можно использовать с Flight. У него есть ряд панелей, которые могут помочь вам отлаживать ваше приложение. Он также очень легок в расширении и добавлении собственных панелей. Команда Flight создала несколько панелей специально для проектов Flight с плагином flightphp/tracy-extensions.

Установка

Установите с помощью composer. И вам действительно захочется установить это без версии для разработчиков, так как у Трейси есть компонент обработки ошибок для продакшена.

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); // если панель отладки видима, тогда длина содержимого не может быть установлена Flight

    // Это специфично для Расширения Трейси для 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('некоторый классный пароль');
$user->insert();
// или $user->save();

echo $user->id; // 1

$user->name = 'Joseph Mamma';
$user->password = password_hash('некоторый классный пароль снова!!!');
$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-фреймворком Flight. Полностью вам решать.

Отдельно

Просто убедитесь, что вы передаете подключение PDO в конструктор.

$pdo_connection = new PDO('sqlite:test.db'); // это просто для примера, вы, вероятно, используете реальное подключение к базе данных

$User = new User($pdo_connection);

Не хотите всегда устанавливать ваше подключение к базе данных в конструкторе? См. Управление подключением к базе данных для других идей!

Зарегистрировать как метод в Flight

Если вы используете PHP-фреймворк 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' ]);
        // вы также можете установить первичный ключ таким образом вместо массива выше.
        $this->primaryKey = 'uuid';
    }

    protected function beforeInsert(self $self) {
        $self->uuid = uniqid(); // или как вам нужно сгенерировать ваши уникальные идентификаторы
    }
}

Если вы не установите первичный ключ перед вставкой, он будет установлен на 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' => 'что-то', 'password' => password_hash('другой пароль') ]);
$user->update(); // обновлены как имя, так и пароль.

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

Это псевдоним для метода dirty(). Немного более понятно, что вы делаете.

$user->copyFrom([ 'name' => 'что-то', 'password' => password_hash('другой пароль') ]);
$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-инъекций. В Интернете есть много статей, пожалуйста, поищите "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)

Ограничьте количество возвращаемых записей. Если задано второе целое число, оно будет сдвинуто, ограничение также как в 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();

Условия ИЛИ

Возможно, вы захотите обернуть ваши условия в оператор ИЛИ. Это делается либо с помощью методов startWrap() и endWrap(), либо путем заполнения 3-го параметра условия после поля и значения.

// Метод 1
$user->eq('id', 1)->startWrap()->eq('name', 'demo')->or()->eq('name', 'test')->endWrap('OR')->find();
// Это будет эквивалентно `id = 1 AND (name = 'demo' OR name = 'test')`

// Метод 2
$user->eq('id', 1)->eq('name', 'demo', 'OR')->find();
// Это будет эквивалентно `id = 1 OR name = 'demo'`

Отношения

Вы можете установить несколько видов отношений, используя эту библиотеку. Вы можете установить отношения один->много и один->один между таблицами. Это требует небольшой дополнительной настройки в классе заранее.

Установка массива $relations не сложна, но угадать правильный синтаксис может быть сложно.

protected array $relations = [
    // вы можете назвать ключ как угодно. Имя ActiveRecord, вероятно, хорошее. Например: user, contact, client
    'user' => [
        // обязательно
        // self::HAS_MANY, self::HAS_ONE, self::BELONGS_TO
        self::HAS_ONE, // это тип отношения

        // обязательно
        'Some_Class', // это "другой" ActiveRecord, на который это будет ссылаться

        // обязательно
        // в зависимости от типа отношения
        // self::HAS_ONE = внешний ключ, который ссылается на соединение
        // self::HAS_MANY = внешний ключ, который ссылается на соединение
        // self::BELONGS_TO = локальный ключ, который ссылается на соединение
        'local_or_foreign_key',
        // просто FYI, это также соединяется только с первичным ключом "другой" модели

        // опционально
        [ '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)

Возможно, у вас есть случай использования для изменения данных послеINSERT?

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

Примечание: Если вы планируете unit-тестирование, делать это может создать некоторые проблемы с unit-тестами, но в целом, потому что вы можете инъектировать ваше подключение с помощью setDatabaseConnection() или $config['connection'], это не слишком плохо.

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

Участие

Пожалуйста, сделайте это. :D

Настройка

Когда вы участвуете, убедитесь, что вы выполняете команду composer test-coverage, чтобы поддерживать 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>
                <!-- your nav elements here -->
            </nav>
        </header>
        <div id="content">
            <!-- This is the magic right here -->
            {block content}{/block}
        </div>
        <div id="footer">
            &copy; Copyright
        </div>
    </body>
</html>

А теперь у нас есть ваш файл, который будет отображаться внутри этого блока content:

<!-- app/views/home.latte -->
<!-- This tells Latte that this file is "inside" the layout.latte file -->
{extends layout.latte}

<!-- This is the content that will be rendered inside the layout inside the content block -->
{block content}
    <h1>Home Page</h1>
    <p>Welcome to my app!</p>
{/block}

Затем, когда вы будете отображать это внутри своей функции или контроллера, вы сделаете что-то вроде этого:

// simple route
Flight::route('/', function () {
    Flight::render('home.latte', [
        'title' => 'Home Page'
    ]);
});

// or if you're using a controller
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;

    // This will only add the extension if the 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($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 и 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

// Если вы используете Composer, подключите автозагрузчик.
// require 'vendor/autoload.php';
// если вы не используете Composer, загрузите фреймворк напрямую
// require 'flight/Flight.php';

// Затем определите маршрут и назначьте функцию для обработки запроса.
Flight::route('/', function () {
  echo 'hello world!';
});

// Наконец, запустите фреймворк.
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 для Windows и скачайте последнюю или конкретную версию (например, 7.4, 8.0) в виде zip-файла, не требующего потокобезопасности.
  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, они являются ценными ресурсами, созданными сообществом. Они охватывают различные темы и сценарии использования, предоставляя дополнительные insights по использованию Flight PHP.

Создание RESTful API с Flight Framework

Это руководство проведет вас через создание RESTful API с использованием Flight PHP. В нем рассматриваются основы настройки API, определения маршрутов и возврата ответов в формате JSON.

Создание простого блога

Это руководство проведет вас через создание базового блога с использованием Flight PHP. Оно состоит из 2 частей: одна охватывает основы, а другая — более продвинутые темы и доработки для блога, готового к производству.

Создание API для Pokémon в PHP: Руководство для начинающих

Это забавное руководство проведет вас через создание простого API для Pokémon с использованием Flight PHP. В нем рассматриваются основы настройки API, определения маршрутов и возврата ответов в формате JSON.

Вклад в проект

У вас есть идея для руководства? Вы нашли ошибку? Мы приветствуем вклад! Наши руководства поддерживаются в репозитории документации FlightPHP.

Если вы создали что-то интересное с помощью Flight и хотите поделиться этим в виде руководства, пожалуйста, отправьте pull request. Обмен знаниями помогает сообществу Flight расти.

Ищете документацию по API?

Если вы ищете конкретную информацию о основных функциях и методах Flight, загляните в раздел Learn нашей документации.