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

Четкая многоуровневая конфигурация (значения по умолчанию в файле + секреты из окружения) также помогает инструментам ИИ для кодирования: агенты узнают одно место для литералов и одно место для секретов, вместо того чтобы изобретать чтение $_ENV внутри контроллеров.

Понимание

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

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

В структурированном приложении (включая skeleton) вы обычно загружаете настройки проекта из app/config/config.php, а затем применяете соответствующие ключи к Engine (например, flight.base_url, flight.views.path). Вы также можете внедрить небольшой объект конфигурации в контроллеры вместо чтения глобальных переменных повсюду — это удобнее для тестов и для агентов, следующих AGENTS.md.

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

Параметры конфигурации Flight

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

Конфигурация загрузчика

Существует также дополнительный параметр конфигурации для загрузчика. Он позволяет автоматически загружать классы с символом _ в имени класса.

// Включить загрузку классов с подчеркиваниями
// По умолчанию true
Loader::$v2ClassLoading = false;

Помните, что автозагрузка также зависит от регистра папок, соответствующего вашим пространствам имен — особенно с макетом App\ + app/Controller/ в skeleton.

Конфигурация проекта и .env (паттерн skeleton)

Ядро Flight не требует файлов .env. Многие приложения используют только массив конфигурации PHP. Официальный skeleton наслаивает конфигурацию так, чтобы секреты не попадали в git, а Runway мог безопасно перезаписывать литеральную конфигурацию:

  1. .env / реальное окружение — секреты и переопределения для деплоя (игнорируется git).
  2. app/config/config.php — литеральные значения PHP-массива по умолчанию (скопированы из config_sample.php). Предпочтительно не использовать выражения $_ENV[...] внутри этого файла: такие инструменты, как runway config:set, могут перезаписать его статическими значениями и встроить секреты в файл.
  3. Слияние при загрузке — env побеждает для сопоставленных ключей; код приложения читает объект конфигурации или $app->get(), а не $_ENV в контроллерах.

Пример структуры config_sample.php / config.php (упрощенно):

<?php
// Только литералы — секреты принадлежат .env для рабочего процесса skeleton
return [
    'app' => [
        'env' => 'development',
        'debug' => true,
        'base_url' => '/',
        'timezone' => 'UTC',
    ],
    'database' => [
        'driver' => 'sqlite', // или mysql, или '' для отключения
        'host' => 'localhost',
        'dbname' => '',
        'user' => '',
        'password' => '',
        'file_path' => __DIR__ . '/../../database.sqlite',
    ],
    // ...
];
# .env.example → .env (skeleton)
APP_ENV=development
APP_DEBUG=true
FLIGHT_BASE_URL=/
DB_DRIVER=sqlite
# DB_PASSWORD=...

Это разделение намеренно для проектов, дружественных к ИИ: инструкции могут говорить «значения по умолчанию в config.php, секреты в .env, внедряйте Config / Engine — никогда не изобретайте доступ к окружению в контроллере». Существующие приложения могут полностью игнорировать .env и хранить один файл конфигурации.

Переменные

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

// Сохранить вашу переменную
Flight::set('id', 123);

// В другом месте вашего приложения
$id = Flight::get('id');

Чтобы проверить, была ли установлена переменная, вы можете сделать:

if (Flight::has('id')) {
  // Сделать что-то
}

Вы можете очистить переменную следующим образом:

// Очищает переменную id
Flight::clear('id');

// Очищает все переменные
Flight::clear();

Примечание: Тот факт, что вы можете установить переменную, не означает, что вы должны это делать. Используйте эту функцию экономно. Причина в том, что все, что здесь хранится, становится глобальной переменной. Глобальные переменные вредны, потому что они могут быть изменены из любого места вашего приложения, что затрудняет поиск ошибок. Кроме того, это может усложнить такие вещи, как модульное тестирование. Предпочитайте внедрение через конструктор (как в skeleton + настройка Dice) для сервисов и конфигурации, которые нужны контроллерам.

Ошибки и исключения

Все ошибки и исключения перехватываются Flight и передаются методу error, если для flight.handle_errors установлено значение true.

Поведение по умолчанию — отправлять общий ответ HTTP 500 Internal Server Error с некоторой информацией об ошибке.

Вы можете переопределить это поведение для своих нужд:

Flight::map('error', function (Throwable $error) {
  // Обработка ошибки
  echo $error->getTraceAsString();
});

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

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

404 Не найдено

Когда URL не может быть найден, Flight вызывает метод notFound. Поведение по умолчанию — отправить ответ HTTP 404 Not Found с простым сообщением.

Вы можете переопределить это поведение для своих нужд:

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

См. также

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

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

Learn/ai

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

Обзор

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

Встроенные команды Runway для подключения к LLM-провайдерам и генерации инструкций проекта помогают вам и вашей команде получать последовательную и релевантную помощь, не вставляя один и тот же контекст в каждый чат.

Понимание

AI-ассистенты наиболее полезны, когда они понимают контекст, соглашения и цели вашего проекта. AI-помощники Flight позволяют вам:

Эти функции поставляются с основным CLI Flight (через Runway) и уже подключены в официальном стартовом проекте flightphp/skeleton.

Что скелет включает для AI

Официальный стартовый проект рассматривает AGENTS.md как источник истины для AI-инструментов:

Файл Роль
AGENTS.md (корень проекта) Глобальные правила, поток загрузки, пространства имен, DI, «чего не делать»
Локальные AGENTS.md в app/, migrations/, tests/ и т.д. Легкие подсказки для конкретной папки при работе в этом дереве
SECURITY.md Секреты, заголовки, XSS/SQL, отчеты — безопасность остается осознанной и отдельной

В скелете нет отдельного файла стиля для Copilot / Cursor / Gemini / Windsurf. Укажите вашему ассистенту на корневой AGENTS.md (и позвольте ему переходить по ссылкам на локальные файлы). Люди могут полностью игнорировать эти файлы и использовать README; структура в любом случае одинакова.

Документация учит API; скелет учит структуре. Короткие примеры Flight:: в этих документах отлично подходят для обучения. В приложении на скелете предпочитайте классы App\…, внедрение зависимостей через конструктор и $this->app вместо статического фасада в контроллерах. См. Установка и Автозагрузка.

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

Настройка учетных данных LLM

Команда ai:init проведет вас через подключение проекта к LLM-провайдеру.

php runway ai:init

Вам будет предложено:

Это создает учетные данные, используемые для последующих LLM-запросов (например, для генерации инструкций).

Пример:

Добро пожаловать в AI Init!
Какой LLM API вы хотите использовать? [1] openai, [2] grok, [3] claude: 1
Введите базовый URL для LLM API [https://api.openai.com]:
Введите ваш API-ключ для openai: sk-...
Введите имя модели, которую хотите использовать (например, gpt-4, claude-3-opus и т.д.) [gpt-4o]:
Учетные данные сохранены в .runway-creds.json

Генерация AI-инструкций для конкретного проекта

Команда ai:generate-instructions создает или обновляет инструкции для AI-ассистентов кодирования, адаптированные под ваш проект.

php runway ai:generate-instructions

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

В зависимости от версии CLI и параметров команда также может записывать копии для конкретных инструментов в старых рабочих процессах (например, файлы правил Copilot, Cursor, Windsurf или Gemini). Для новых проектов на основе скелета рассматривайте AGENTS.md (а также любые локальные файлы AGENTS.md, которые вы храните в app/) как единственный источник истины — не поддерживайте пять расходящихся файлов инструкций вручную.

Пример:

Опишите, для чего ваш проект? Мой потрясающий API
Какую базу данных вы планируете использовать? MySQL
Какой HTML-шаблонизатор вы планируете использовать (если есть)? twig
Является ли безопасность важным элементом этого проекта? (y/n) y
...
AI-инструкции успешно обновлены.

Теперь AI-инструменты могут предлагать код, соответствующий вашему реальному стеку и структуре, а не общему учебнику по PHP.

Продвинутое использование

Смотрите также

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

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

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

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

Понимание

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

Шаблоны помогают с XSS, экранируя вывод по умолчанию (Twig и Latte делают это; используйте это преимущество). Сессии могут помочь с CSRF, сохраняя CSRF-токен в сессии пользователя, как описано ниже. Использование подготовленных запросов с PDO — или помощников в SimplePdo — помогает предотвратить SQL-инъекции. CORS можно обработать с помощью простого хука перед вызовом Flight::start().

Все эти методы работают вместе, чтобы поддерживать безопасность ваших веб-приложений. Всегда следует помнить о необходимости изучения и понимания лучших практик безопасности. Не просите ИИ-ассистента «отключить CSP» или ослабить заголовки только для того, чтобы страница загрузилась, не понимая компромиссов.

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

Заголовки

HTTP-заголовки — один из самых простых способов защитить ваши веб-приложения. Вы можете использовать заголовки для предотвращения кликджекинга, XSS и других атак. Существует несколько способов добавить эти заголовки в ваше приложение.

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

Скелет включает App\Middleware\SecurityHeadersMiddleware (CSP с nonce для каждого запроса, frame options, HSTS и другое). Предпочтительнее осознанно расширять его, а не отключать заголовки.

Добавление вручную

Вы можете вручную добавить эти заголовки с помощью метода header объекта Flight\Response.

// Устанавливает заголовок X-Frame-Options для предотвращения кликджекинга
Flight::response()->header('X-Frame-Options', 'SAMEORIGIN');

// Устанавливает заголовок Content-Security-Policy для предотвращения XSS
// Примечание: этот заголовок может быть очень сложным, поэтому вам понадобится
//  обратиться к примерам в интернете для вашего приложения
Flight::response()->header("Content-Security-Policy", "default-src 'self'");

// Устанавливает заголовок X-XSS-Protection для предотвращения XSS
Flight::response()->header('X-XSS-Protection', '1; mode=block');

// Устанавливает заголовок X-Content-Type-Options для предотвращения определения MIME-типа по содержимому
Flight::response()->header('X-Content-Type-Options', 'nosniff');

// Устанавливает заголовок Referrer-Policy для контроля объёма передаваемой информации о источнике
Flight::response()->header('Referrer-Policy', 'no-referrer-when-downgrade');

// Устанавливает заголовок Strict-Transport-Security для принудительного использования HTTPS
Flight::response()->header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');

// Устанавливает заголовок Permissions-Policy для управления доступными функциями и API
Flight::response()->header('Permissions-Policy', 'geolocation=()');

Их можно добавить в начало ваших файлов routes.php или index.php.

Добавление в качестве фильтра

Вы также можете добавить их в фильтр/хук следующим образом:

// Добавить заголовки в фильтре
Flight::before('start', function() {
    Flight::response()->header('X-Frame-Options', 'SAMEORIGIN');
    Flight::response()->header("Content-Security-Policy", "default-src 'self'");
    Flight::response()->header('X-XSS-Protection', '1; mode=block');
    Flight::response()->header('X-Content-Type-Options', 'nosniff');
    Flight::response()->header('Referrer-Policy', 'no-referrer-when-downgrade');
    Flight::response()->header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
    Flight::response()->header('Permissions-Policy', 'geolocation=()');
});

Добавление в качестве middleware

Вы также можете добавить их как класс middleware, что обеспечивает наибольшую гибкость в выборе маршрутов, к которым это применяется. В целом эти заголовки должны применяться ко всем HTML и API ответам.

Путь и пространство имён в стиле скелета (регистр папки соответствует App\Middleware):

// app/Middleware/SecurityHeadersMiddleware.php

namespace App\Middleware;

use flight\Engine;

class SecurityHeadersMiddleware
{
    protected Engine $app;

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

    public function before(array $params): void
    {
        $response = $this->app->response();
        // Предпочитайте CSP nonce из bootstrap при наличии встроенных скриптов (скелет устанавливает csp_nonce)
        $nonce = $this->app->get('csp_nonce');
        $csp = $nonce
            ? "default-src 'self'; script-src 'self' 'nonce-{$nonce}'; style-src 'self' 'nonce-{$nonce}'"
            : "default-src 'self'";

        $response->header('X-Frame-Options', 'SAMEORIGIN');
        $response->header('Content-Security-Policy', $csp);
        $response->header('X-XSS-Protection', '1; mode=block');
        $response->header('X-Content-Type-Options', 'nosniff');
        $response->header('Referrer-Policy', 'no-referrer-when-downgrade');
        $response->header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
        $response->header('Permissions-Policy', 'geolocation=()');
    }
}

// app/config/routes.php — группа с пустой строкой = глобальный middleware для всех маршрутов
use App\Middleware\SecurityHeadersMiddleware;
use flight\net\Router;

$router->group('', function (Router $router) {
    $router->get('/users', [ \App\Controller\UserController::class, 'getUsers' ]);
    // другие маршруты
}, [SecurityHeadersMiddleware::class]);

Старые проекты могут по-прежнему использовать app/middlewares и app\middlewares; это работает, если папки совпадают. Новые приложения на скелете используют app/Middleware/ и App\Middleware. См. Автозагрузка.

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

Межсайтовая подделка запросов (CSRF) — это тип атаки, при которой вредоносный веб-сайт может заставить браузер пользователя отправить запрос на ваш сайт. Это может быть использовано для выполнения действий на вашем сайте без ведома пользователя. Flight не предоставляет встроенного механизма защиты от CSRF, но вы можете легко реализовать свой собственный с помощью 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>
Использование Twig (по умолчанию в скелете)

Зарегистрируйте функцию Twig или передавайте токен в каждое представление формы. Минимальный пример с глобальной переменной + полем формы:

// При настройке Twig (например, services.php)
$twig->addGlobal('csrf_token', $app->session()->get('csrf_token'));
{# app/views/form.twig #}
<form method="post">
    <input type="hidden" name="csrf_token" value="{{ csrf_token }}">
    {# остальные поля #}
</form>
Использование Latte

Вы также можете настроить пользовательскую функцию для вывода CSRF-токена в ваших шаблонах Latte.


Flight::map('render', function(string $template, array $data, ?string $block): void {
    $latte = new Latte\Engine;

    // другие настройки...

    // Устанавливаем пользовательскую функцию для вывода CSRF-токена
    $latte->addFunction('csrf', function() {
        $csrfToken = Flight::session()->get('csrf_token');
        return new \Latte\Runtime\Html('<input type="hidden" name="csrf_token" value="' . $csrfToken . '">');
    });

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

И теперь в ваших шаблонах Latte вы можете использовать функцию csrf() для вывода CSRF-токена.

<form method="post">
    {csrf()}
    <!-- остальные поля формы -->
</form>

Проверка CSRF-токена

Вы можете проверить CSRF-токен несколькими способами.

Middleware
// app/Middleware/CsrfMiddleware.php

namespace App\Middleware;

use flight\Engine;

class CsrfMiddleware
{
    protected Engine $app;

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

    public function before(array $params): void
    {
        if($this->app->request()->method == 'POST') {
            $token = $this->app->request()->data->csrf_token;
            if($token !== $this->app->session()->get('csrf_token')) {
                $this->app->halt(403, 'Invalid CSRF token');
            }
        }
    }
}

// routes.php
use App\Middleware\CsrfMiddleware;

$router->group('', function ($router) {
    $router->get('/users', [ \App\Controller\UserController::class, 'getUsers' ]);
    // другие маршруты
}, [CsrfMiddleware::class]);
Событийные фильтры
// Этот 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 на вашу страницу. Этот код может быть использован для кражи информации у ваших пользователей или выполнения действий на вашем сайте. Используя класс представления Flight или шаблонизатор, такой как Twig или Latte, вы можете легко экранировать вывод для предотвращения XSS-атак.

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

// Это экранирует вывод
Flight::view()->set('name', $name);
// Это выведет: &lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;

// Twig (по умолчанию в скелете) и Latte экранируют по умолчанию — предпочитайте их обычному PHP echo
Flight::render('template', ['name' => $name]);
// Twig: {{ name }}  → экранировано
// Избегайте |raw / неэкранированного вывода, если только контент полностью не доверенный

SQL-инъекции

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

// Предполагая, что Flight::db() зарегистрирован как SimplePdo (или вы внедряете SimplePdo в контроллер)
$statement = Flight::db()->prepare('SELECT * FROM users WHERE username = :username');
$statement->execute([':username' => $username]);
$users = $statement->fetchAll();

// SimplePdo (предпочтительно) — однострочные запросы со связанными параметрами
$users = Flight::db()->fetchAll('SELECT * FROM users WHERE username = :username', [ 'username' => $username ]);

// Та же идея с плейсхолдерами ?
$users = Flight::db()->fetchAll('SELECT * FROM users WHERE username = ?', [ $username ]);

В контроллерах в стиле скелета предпочитайте внедрение SimplePdo через конструктор вместо Flight::db(), чтобы тесты и ИИ-сгенерированный код оставались согласованными (DIC).

Небезопасный пример

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

// конечный пользователь заполняет веб-форму.
// в поле формы хакер вводит что-то вроде:
$username = "' OR 1=1; -- ";

$sql = "SELECT * FROM users WHERE username = '$username' LIMIT 5";
$users = Flight::db()->fetchAll($sql);
// После сборки запроса он выглядит так
// SELECT * FROM users WHERE username = '' OR 1=1; -- LIMIT 5

// Это выглядит странно, но это валидный запрос, который будет работать. На самом деле,
// это очень распространённая SQL-инъекция, которая вернёт всех пользователей.

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

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

Проверка JSONP-колбэка

Если вы используете метод Flight::jsonp() во Flight, учтите, что Flight проверяет имя параметра callback в JSONP на соответствие строгому регулярному выражению из белого списка (/^[A-Za-z_$][\w$.]{0,127}$/). Любое имя callback, не соответствующее этому шаблону, приведёт к исключению во Flight, предотвращая внедрение произвольного JavaScript через вредоносное значение callback.

Эта проверка встроена и не требует дополнительной настройки, но о ней полезно знать при отладке неожиданных ошибок от JSONP-эндпоинтов.

CORS

Совместное использование ресурсов между источниками (CORS) — это механизм, который позволяет запрашивать многие ресурсы (например, шрифты, JavaScript и т.д.) на веб-странице с другого домена, отличного от домена, на котором ресурс был создан. Flight не имеет встроенной функциональности, но это легко можно обработать с помощью хука, который выполняется перед вызовом метода Flight::start().

// app/Utils/CorsUtil.php (скелет: папка Utils в PascalCase → App\Utils)

namespace App\Utils;

use flight\Engine;

class CorsUtil
{
    protected Engine $app;

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

    public function set(array $params = []): void
    {
        $request = $this->app->request();
        $response = $this->app->response();
        if ($request->getVar('HTTP_ORIGIN') !== '') {
            $this->allowOrigins();
            $response->header('Access-Control-Allow-Credentials', 'true');
            $response->header('Access-Control-Max-Age', '86400');
        }

        if ($request->method === 'OPTIONS') {
            if ($request->getVar('HTTP_ACCESS_CONTROL_REQUEST_METHOD') !== '') {
                $response->header(
                    'Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD'
                );
            }
            if ($request->getVar('HTTP_ACCESS_CONTROL_REQUEST_HEADERS') !== '') {
                $response->header(
                    "Access-Control-Allow-Headers",
                    $request->getVar('HTTP_ACCESS_CONTROL_REQUEST_HEADERS')
                );
            }

            $response->status(200);
            $response->send();
            exit;
        }
    }

    private function allowOrigins(): void
    {
        // настройте здесь ваши разрешённые хосты.
        $allowed = [
            'capacitor://localhost',
            'ionic://localhost',
            'http://localhost',
            'http://localhost:4200',
            'http://localhost:8080',
            'http://localhost:8100',
        ];

        $request = $this->app->request();

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

// bootstrap / маршруты — запускается перед стартом
$app = Flight::app();
$cors = new \App\Utils\CorsUtil($app);
$app->before('start', [ $cors, 'set' ]);

Усиление конфигурации Flight

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

flight.allow_method_override

По умолчанию Flight позволяет клиентам переопределять HTTP-метод запроса с помощью заголовка X-HTTP-Method-Override или поля _method в теле POST. Хотя это удобно для HTML-форм, которые могут отправлять только GET/POST, это может быть опасно, если вы этого не ожидаете — злоумышленник может подделать DELETE или PUT запросы через обычную форму.

Если ваше приложение не полагается на это поведение (например, вы создаёте API, потребляемое современными клиентами или JavaScript-фронтендами, которые могут отправлять любые HTTP-глаголы), вам следует отключить его:

// В вашем файле index.php или bootstrap, перед Flight::start()
Flight::set('flight.allow_method_override', false);

Значение по умолчанию — true для обратной совместимости, но настоятельно рекомендуется устанавливать false для любого приложения, которому явно не нужна функция переопределения.

flight.debug

Во Flight есть настройка flight.debug, которая управляет отображением подробной информации об ошибке (сообщение исключения, код и полный стек-трейс) в браузере при возникновении необработанного исключения. По умолчанию false, что означает показ только общего сообщения 500 Internal Server Error — никакие внутренние детали не утекают клиенту.

Никогда не включайте это на production-сервере. Используйте только локально или в staging-окружении:

// Безопасно только для локальной разработки — НИКОГДА в production
Flight::set('flight.debug', true);

Когда flight.debug имеет значение false (по умолчанию), вы по-прежнему можете перехватывать ошибки, включив flight.log_errors:

// Логировать ошибки на стороне сервера, не раскрывая их клиенту
Flight::set('flight.debug', false);
Flight::set('flight.log_errors', true);

Рекомендуемая production-конфигурация

// index.php или применяется из конфигурации приложения / bootstrap
Flight::set('flight.allow_method_override', false);
Flight::set('flight.debug', false);
Flight::set('flight.log_errors', true);

Обработка ошибок

Скрывайте чувствительные детали ошибок в production, чтобы избежать утечки информации злоумышленникам. В production логируйте ошибки вместо их отображения, установив display_errors в 0.

// В вашем bootstrap.php или index.php

// добавьте это в ваш app/config/config.php
$environment = ENVIRONMENT;
if ($environment === 'production') {
    ini_set('display_errors', 0); // Отключить отображение ошибок
    ini_set('log_errors', 1);     // Вместо этого логировать ошибки
    ini_set('error_log', '/path/to/error.log');
}

// В ваших маршрутах или контроллерах
// Используйте Flight::halt() для контролируемых ответов с ошибками
Flight::halt(403, 'Access denied');

Санитизация ввода

Никогда не доверяйте пользовательскому вводу. Санитизируйте его с помощью filter_var перед обработкой, чтобы предотвратить проникновение вредоносных данных. Предпочитайте чтение ввода через $app->request() (или Flight::request()) вместо сырых $_GET / $_POST в коде приложения.


// Предположим, есть $_POST запрос с $_POST['input'] и $_POST['email']

// Санитизируем строковый ввод
$clean_input = filter_var(Flight::request()->data->input, FILTER_SANITIZE_STRING);
// Санитизируем email
$clean_email = filter_var(Flight::request()->data->email, FILTER_SANITIZE_EMAIL);

Хеширование паролей

Храните пароли безопасно и проверяйте их с помощью встроенных функций PHP, таких как password_hash и password_verify. Пароли никогда не должны храниться в открытом виде, а также не должны шифроваться обратимыми методами. Хеширование гарантирует, что даже в случае компрометации вашей базы данных фактические пароли останутся защищёнными.

$password = Flight::request()->data->password;
// Хешируйте пароль при сохранении (например, при регистрации)
$hashed_password = password_hash($password, PASSWORD_DEFAULT);

// Проверяйте пароль (например, при входе)
if (password_verify($password, $stored_hash)) {
    // Пароль совпадает
}

Ограничение частоты запросов

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

// Предполагая, что у вас установлен и зарегистрирован flightphp/cache
// Использование flightphp/cache в фильтре
Flight::before('start', function() {
    $cache = Flight::cache();
    $ip = Flight::request()->ip;
    $key = "rate_limit_{$ip}";
    $attempts = (int) $cache->retrieve($key);

    if ($attempts >= 10) {
        Flight::halt(429, 'Too many requests');
    }

    $cache->set($key, $attempts + 1, 60); // Сброс через 60 секунд
});

Смотрите также

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

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

Learn/routing

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

Обзор

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

Понимание

Маршрутизация — это основной механизм, который связывает HTTP-запросы с логикой вашего приложения во Flight. Определяя маршруты, вы указываете, как разные URL-адреса запускают конкретный код — через функции, методы классов или действия контроллеров. Система маршрутизации Flight гибкая: она поддерживает базовые шаблоны, именованные параметры, регулярные выражения и расширенные возможности, такие как внедрение зависимостей и ресурсная маршрутизация. Такой подход поддерживает организованность и простоту сопровождения кода, оставаясь быстрым и простым для новичков и расширяемым для продвинутых пользователей.

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

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

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

Базовая маршрутизация во Flight выполняется путём сопоставления URL-шаблона с функцией обратного вызова или массивом из класса и метода.

Flight::route('/', function(){
    echo 'hello world!';
});

Маршруты сопоставляются в порядке их определения. Будет вызван первый маршрут, соответствующий запросу.

Использование функций в качестве обратных вызовов

Обратный вызов может быть любым вызываемым объектом. Таким образом, вы можете использовать обычную функцию:

function hello() {
    echo 'hello world!';
}

Flight::route('/', 'hello');

Использование классов и методов в качестве контроллера

Вы также можете использовать метод класса (статический или обычный):

class GreetingController {
    public function hello() {
        echo 'hello world!';
    }
}

Flight::route('/', [ 'GreetingController','hello' ]);
// или
Flight::route('/', [ GreetingController::class, 'hello' ]); // предпочтительный способ
// или
Flight::route('/', [ 'GreetingController::hello' ]);
// или 
Flight::route('/', [ 'GreetingController->hello' ]);

Или создав объект сначала, а затем вызвав метод:

use flight\Engine;

// GreetingController.php
class GreetingController
{
    protected Engine $app
    public function __construct(Engine $app) {
        $this->app = $app;
        $this->name = 'John Doe';
    }

    public function hello() {
        echo "Hello, {$this->name}!";
    }
}

// index.php
$app = Flight::app();
$greeting = new GreetingController($app);

Flight::route('/', [ $greeting, 'hello' ]);

Примечание: По умолчанию, когда контроллер вызывается внутри фреймворка, класс flight\Engine всегда внедряется, если вы не укажете иное через контейнер внедрения зависимостей.

Маршрутизация по конкретным методам

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

Flight::route('GET /', function () {
  echo 'I received a GET request.';
});

Flight::route('POST /', function () {
  echo 'I received a POST request.';
});

// Вы не можете использовать Flight::get() для маршрутов, так как это метод
//    для получения переменных, а не для создания маршрута.
Flight::post('/', function() { /* код */ });
Flight::patch('/', function() { /* код */ });
Flight::put('/', function() { /* код */ });
Flight::delete('/', function() { /* код */ });

Вы также можете сопоставить несколько методов с одним обратным вызовом, используя разделитель |:

Flight::route('GET|POST /', function () {
  echo 'I received either a GET or a POST request.';
});

Особая обработка HEAD и OPTIONS запросов

Flight предоставляет встроенную обработку для HTTP-запросов HEAD и OPTIONS:

HEAD-запросы

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

OPTIONS-запросы

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

// Для маршрута, определённого как:
Flight::route('GET|POST /users', function() { /* ... */ });

// OPTIONS-запрос к /users ответит:
//
// Статус: 204 No Content
// Allow: GET, POST, HEAD, OPTIONS

Использование объекта Router

Кроме того, вы можете получить объект Router, который содержит несколько вспомогательных методов для использования:


$router = Flight::router();

// сопоставляет все методы, как Flight::route()
$router->map('/', function() {
    echo 'hello world!';
});

// GET-запрос
$router->get('/users', function() {
    echo 'users';
});
$router->post('/users',             function() { /* код */});
$router->put('/users/update/@id',   function() { /* код */});
$router->delete('/users/@id',       function() { /* код */});
$router->patch('/users/@id',        function() { /* код */});

Регулярные выражения (Regex)

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

Flight::route('/user/[0-9]+', function () {
  // Это будет соответствовать /user/1234
});

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

Именованные параметры

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

Flight::route('/@name/@id', function (string $name, string $id) {
  echo "hello, $name ($id)!";
});

Вы также можете включить регулярные выражения в ваши именованные параметры, используя разделитель ::

Flight::route('/@name/@id:[0-9]{3}', function (string $name, string $id) {
  // Это будет соответствовать /bob/123
  // Но не будет соответствовать /bob/12345
});

Примечание: Сопоставление групп регулярных выражений () с позиционными параметрами не поддерживается. Например: :'\(

Важное предостережение

Хотя в приведённом выше примере кажется, что @name напрямую связан с переменной $name, это не так. Порядок параметров в функции обратного вызова определяет, что именно будет в неё передано. Если вы поменяете порядок параметров в функции обратного вызова, переменные также поменяются. Вот пример:

Flight::route('/@name/@id', function (string $id, string $name) {
  echo "hello, $name ($id)!";
});

И если вы перейдёте по следующему URL: /bob/123, вывод будет hello, 123 (bob)!. Пожалуйста, будьте внимательны при настройке маршрутов и функций обратного вызова!

Необязательные параметры

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

Flight::route(
  '/blog(/@year(/@month(/@day)))',
  function(?string $year, ?string $month, ?string $day) {
    // Это будет соответствовать следующим URL:
    // /blog/2012/12/10
    // /blog/2012/12
    // /blog/2012
    // /blog
  }
);

Любые необязательные параметры, которые не были сопоставлены, будут переданы как NULL.

Подстановочная маршрутизация (Wildcard)

Сопоставление выполняется только по отдельным сегментам URL. Если вы хотите сопоставить несколько сегментов, вы можете использовать подстановочный знак *.

Flight::route('/blog/*', function () {
  // Это будет соответствовать /blog/2000/02/01
});

Чтобы направить все запросы на один обратный вызов, вы можете сделать следующее:

Flight::route('*', function () {
  // Сделать что-то
});

Обработчик 404 Not Found

По умолчанию, если URL не найден, Flight отправляет очень простой ответ HTTP 404 Not Found. Если вы хотите настроить ответ 404, вы можете сопоставить собственный метод notFound:

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

    // Вы также можете использовать Flight::render() с пользовательским шаблоном.
    $output = <<<HTML
        <h1>My Custom 404 Not Found</h1>
        <h3>The page you have requested {$url} could not be found.</h3>
        HTML;

    $this->response()
        ->clearBody()
        ->status(404)
        ->write($output)
        ->send();
});

Обработчик 405 Method Not Allowed

По умолчанию, если URL найден, но метод не разрешён, Flight отправляет очень простой ответ HTTP 405 Method Not Allowed (например: Method Not Allowed. Allowed Methods are: GET, POST). Он также включает заголовок Allow с разрешёнными методами для этого URL.

Если вы хотите более настраиваемый ответ 405, вы можете сопоставить собственный метод methodNotFound:

use flight\net\Route;

Flight::map('methodNotFound', function(Route $route) {
    $url = Flight::request()->url;
    $methods = implode(', ', $route->methods);

    // Вы также можете использовать Flight::render() с пользовательским шаблоном.
    $output = <<<HTML
        <h1>My Custom 405 Method Not Allowed</h1>
        <h3>The method you have requested for {$url} is not allowed.</h3>
        <p>Allowed Methods are: {$methods}</p>
        HTML;

    $this->response()
        ->clearBody()
        ->status(405)
        ->setHeader('Allow', $methods)
        ->write($output)
        ->send();
});

Продвинутое использование

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

Если вы хотите использовать внедрение зависимостей через контейнер (PSR-11, PHP-DI, Dice и т. д.), это доступно только для тех типов маршрутов, где вы либо сами создаёте объект, а контейнер используете для создания вашего объекта, либо используете строки для определения класса и вызываемого метода. Вы можете перейти на страницу Внедрение зависимостей для получения дополнительной информации.

Вот краткий пример:


use flight\database\SimplePdo;

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

    public function hello(int $id) {
        // сделать что-то с $this->db
        $name = $this->db->fetchField("SELECT name FROM users WHERE id = ?", [ $id ]);
        echo "Hello, world! My name is {$name}!";
    }
}

// index.php

// Настройте контейнер с необходимыми параметрами
// Смотрите страницу о внедрении зависимостей для получения дополнительной информации о PSR-11
$dice = new \Dice\Dice();

// Не забудьте переназначить переменную через '$dice = '!!!!!
$dice = $dice->addRule(SimplePdo::class, [
    'shared' => true,
    'constructParams' => [ 
        'mysql:host=localhost;dbname=test', 
        'root',
        'password'
    ]
]);

// Зарегистрируйте обработчик контейнера
Flight::registerContainerHandler(function($class, $params) use ($dice) {
    return $dice->create($class, $params);
});

// Маршруты как обычно
Flight::route('/hello/@id', [ 'Greeting', 'hello' ]);
// или
Flight::route('/hello/@id', 'Greeting->hello');
// или
Flight::route('/hello/@id', 'Greeting::hello');

Flight::start();

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

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

Flight::route('/user/@name', function (string $name) {
  // Проверка некоторого условия
  if ($name !== "Bob") {
    // Перейти к следующему маршруту
    return true;
  }
});

Flight::route('/user/*', function () {
  // Этот маршрут будет вызван
});

Теперь рекомендуется использовать промежуточное ПО (middleware) для обработки сложных случаев, подобных этому.

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

Назначив маршруту псевдоним, вы можете позже динамически вызывать этот псевдоним в вашем приложении для генерации URL в коде (например, ссылка в HTML-шаблоне или создание URL для перенаправления).

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

// позже в коде где-нибудь
class UserController {
    public function update() {

        // код для сохранения пользователя...
        $id = $user['id']; // например, 5

        $redirectUrl = Flight::getUrl('user_view', [ 'id' => $id ]); // вернёт '/users/5'
        Flight::redirect($redirectUrl);
    }
}

Это особенно полезно, если ваш URL изменился. В приведённом выше примере предположим, что пользователи были перемещены на /admin/users/@id. Благодаря псевдониму маршрута вам больше не нужно искать все старые URL в вашем коде и изменять их, потому что псевдоним теперь вернёт /admin/users/5, как в примере выше.

Псевдонимы маршрутов также работают в группах:

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

Просмотр информации о маршруте

Если вы хотите просмотреть информацию о сопоставленном маршруте, есть два способа:

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

executedRoute

Flight::route('/', function() {
  $route = Flight::router()->executedRoute;
  // Сделать что-то с $route
  // Массив сопоставленных HTTP-методов
  $route->methods;

  // Массив именованных параметров
  $route->params;

  // Соответствующее регулярное выражение
  $route->regex;

  // Содержит содержимое любого '*', использованного в URL-шаблоне
  $route->splat;

  // Показывает путь URL... если вам действительно это нужно
  $route->pattern;

  // Показывает, какое промежуточное ПО назначено этому маршруту
  $route->middleware;

  // Показывает псевдоним, назначенный этому маршруту
  $route->alias;
});

Примечание: Свойство executedRoute будет установлено только после выполнения маршрута. Если вы попытаетесь получить к нему доступ до выполнения маршрута, оно будет NULL. Вы также можете использовать executedRoute в промежуточном ПО!

Передача true в определение маршрута

Flight::route('/', function(\flight\net\Route $route) {
  // Массив сопоставленных HTTP-методов
  $route->methods;

  // Массив именованных параметров
  $route->params;

  // Соответствующее регулярное выражение
  $route->regex;

  // Содержит содержимое любого '*', использованного в URL-шаблоне
  $route->splat;

  // Показывает путь URL... если вам действительно это нужно
  $route->pattern;

  // Показывает, какое промежуточное ПО назначено этому маршруту
  $route->middleware;

  // Показывает псевдоним, назначенный этому маршруту
  $route->alias;
}, true); // <-- Этот параметр true обеспечивает это

Группировка маршрутов и промежуточное ПО

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

Flight::group('/api/v1', function () {
  Flight::route('/users', function () {
    // Соответствует /api/v1/users
  });

  Flight::route('/posts', function () {
    // Соответствует /api/v1/posts
  });
});

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

Flight::group('/api', function () {
  Flight::group('/v1', function () {
    // Flight::get() получает переменные, а не устанавливает маршрут! Смотрите объектный контекст ниже
    Flight::route('GET /users', function () {
      // Соответствует GET /api/v1/users
    });

    Flight::post('/posts', function () {
      // Соответствует POST /api/v1/posts
    });

    Flight::put('/posts/1', function () {
      // Соответствует PUT /api/v1/posts
    });
  });
  Flight::group('/v2', function () {

    // Flight::get() получает переменные, а не устанавливает маршрут! Смотрите объектный контекст ниже
    Flight::route('GET /users', function () {
      // Соответствует GET /api/v2/users
    });
  });
});

Группировка с объектным контекстом

Вы всё ещё можете использовать группировку маршрутов с объектом Engine следующим образом:

$app = Flight::app();

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

  // используем переменную $router
  $router->get('/users', function () {
    // Соответствует GET /api/v1/users
  });

  $router->post('/posts', function () {
    // Соответствует POST /api/v1/posts
  });
});

Примечание: Это предпочтительный способ определения маршрутов и групп с объектом $router.

Группировка с промежуточным ПО

Вы также можете назначить промежуточное ПО группе маршрутов:

Flight::group('/api/v1', function () {
  Flight::route('/users', function () {
    // Соответствует /api/v1/users
  });
}, [ MyAuthMiddleware::class ]); // или [ new MyAuthMiddleware() ], если вы хотите использовать экземпляр

Подробнее на странице промежуточное ПО для групп.

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

Вы можете создать набор маршрутов для ресурса с помощью метода resource. Это создаст набор маршрутов для ресурса, следующих RESTful-соглашениям.

Чтобы создать ресурс, сделайте следующее:

Flight::resource('/users', UsersController::class);

И что произойдёт в фоновом режиме, так это создание следующих маршрутов:

[
      'index' => 'GET /users',
      'create' => 'GET /users/create',
      'store' => 'POST /users',
      'show' => 'GET /users/@id',
      'edit' => 'GET /users/@id/edit',
      'update' => 'PUT /users/@id',
      'destroy' => 'DELETE /users/@id'
]

Ваш контроллер будет использовать следующие методы:

class UsersController
{
    public function index(): void
    {
    }

    public function show(string $id): void
    {
    }

    public function create(): void
    {
    }

    public function store(): void
    {
    }

    public function edit(string $id): void
    {
    }

    public function update(string $id): void
    {
    }

    public function destroy(string $id): void
    {
    }
}

Примечание: Вы можете просмотреть добавленные маршруты с помощью runway, выполнив php runway routes.

Настройка ресурсных маршрутов

Есть несколько параметров для настройки ресурсных маршрутов.

Базовый псевдоним (Alias Base)

Вы можете настроить aliasBase. По умолчанию псевдонимом является последняя часть указанного URL. Например, /users/ приведёт к aliasBase значению users. Когда эти маршруты создаются, псевдонимы будут users.index, users.create и т. д. Если вы хотите изменить псевдоним, установите aliasBase в нужное значение.

Flight::resource('/users', UsersController::class, [ 'aliasBase' => 'user' ]);
Only и Except

Вы также можете указать, какие маршруты создавать, используя параметры only и except.

// Разрешить только эти методы и запретить остальные
Flight::resource('/users', UsersController::class, [ 'only' => [ 'index', 'show' ] ]);
// Запретить только эти методы и разрешить остальные
Flight::resource('/users', UsersController::class, [ 'except' => [ 'create', 'store', 'edit', 'update', 'destroy' ] ]);

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

Промежуточное ПО

Вы также можете указать промежуточное ПО для выполнения на каждом из маршрутов, созданных методом resource.

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

Потоковые ответы (Streaming)

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

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

Потоковая передача с ручной установкой заголовков

Вы можете отправить потоковый ответ клиенту, используя метод stream() на маршруте. Если вы это делаете, вы должны вручную установить все заголовки до того, как выведете что-либо клиенту. Это делается с помощью PHP-функции header() или метода Flight::response()->setRealHeader().

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

    $response = Flight::response();

    // очевидно, вы должны очистить путь и т.д.
    $fileNameSafe = basename($filename);

    // Если вам нужно установить дополнительные заголовки после выполнения маршрута,
    // вы должны определить их до любого вывода.
    // Все они должны быть прямым вызовом функции header() или
    // вызовом Flight::response()->setRealHeader()
    header('Content-Disposition: attachment; filename="'.$fileNameSafe.'"');
    // или
    $response->setRealHeader('Content-Disposition: attachment; filename="'.$fileNameSafe.'"');

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

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

    // вручную установите длину содержимого, если хотите
    header('Content-Length: '.filesize($filePath));
    // или
    $response->setRealHeader('Content-Length: '.filesize($filePath));

    // Потоковая передача файла клиенту по мере его чтения
    readfile($filePath);

// Это волшебная строка здесь
})->stream();

Потоковая передача с заголовками

Вы также можете использовать метод streamWithHeaders() для установки заголовков перед началом потоковой передачи.

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

    // вы можете добавить любые дополнительные заголовки здесь
    // просто необходимо использовать header() или Flight::response()->setRealHeader()

    // как бы вы ни получали данные, просто для примера...
    $users_stmt = Flight::db()->query("SELECT id, first_name, last_name FROM users");

    echo '{';
    $user_count = count($users);
    while($user = $users_stmt->fetch(PDO::FETCH_ASSOC)) {
        echo json_encode($user);
        if(--$user_count > 0) {
            echo ',';
        }

        // Это необходимо для отправки данных клиенту
        ob_flush();
    }
    echo '}';

// Вот так вы установите заголовки перед началом потоковой передачи.
})->streamWithHeaders([
    'Content-Type' => 'application/json',
    'Content-Disposition' => 'attachment; filename="users.json"',
    // необязательный код состояния, по умолчанию 200
    'status' => 200
]);

Смотрите также

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

404 Not Found или неожиданное поведение маршрута

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

Flight::route('/hello', function(){
    // Это может вызвать ошибку 404 Not Found
    return 'Hello World';
});

// Вероятно, вам нужно это
Flight::route('/hello', function(){
    echo 'Hello World';
});

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

История изменений

Learn/learn

Узнайте о Flight

Flight — это быстрый, простой и расширяемый фреймворк для PHP. Он довольно универсален и может использоваться для создания любых веб-приложений. Он создан с учётом простоты и написан так, чтобы его было легко понимать и использовать — как людям, так и ИИ-помощникам для программирования.

Примечание: Вы увидите примеры, в которых используется Flight:: как статическая переменная, а также примеры с объектом Engine $app->. Оба подхода взаимозаменяемы. $app и $this->app в контроллере/промежуточном ПО — это рекомендуемый подход команды Flight (и именно его придерживаются официальный скелет и AGENTS.md для новых проектов).

Основные компоненты

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

Узнайте, как управлять маршрутами для вашего веб-приложения. Это также включает группировку маршрутов, параметры маршрутов и промежуточное ПО.

Промежуточное ПО

Узнайте, как использовать промежуточное ПО для фильтрации запросов и ответов в вашем приложении.

Автозагрузка

Узнайте, как автозагружать собственные классы. Регистр папок должен соответствовать вашим пространствам имён — скелет использует App\ и папки в PascalCase, например app/Controller/.

Запросы

Узнайте, как обрабатывать запросы и ответы в вашем приложении.

Ответы

Узнайте, как отправлять ответы вашим пользователям.

HTML-шаблоны

Узнайте, как рендерить HTML с помощью Twig (по умолчанию в скелете), Latte или других движков — не только встроенных PHP-представлений.

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

Узнайте, как защитить ваше приложение от распространённых угроз безопасности.

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

Узнайте, как настроить фреймворк для вашего приложения.

Менеджер событий

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

Расширение Flight

Узнайте, как расширить фреймворк, добавляя собственные методы и классы.

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

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

Контейнер внедрения зависимостей (DIC)

Узнайте, как использовать контейнеры внедрения зависимостей (DIC) для управления зависимостями вашего приложения.

Вспомогательные классы

Коллекции

Коллекции используются для хранения данных и предоставления к ним доступа как в виде массива, так и в виде объекта для удобства использования.

JSON-обёртка

Содержит несколько простых функций, чтобы кодирование и декодирование JSON было последовательным.

SimplePdo

PDO иногда может доставлять больше хлопот, чем необходимо. SimplePdo — это современный вспомогательный класс для PDO с удобными методами, такими как insert(), update(), delete() и transaction(), которые значительно упрощают операции с базой данных.

PdoWrapper (устаревший)

Оригинальная PDO-обёртка устарела начиная с версии v3.18.0. Пожалуйста, используйте вместо неё SimplePdo.

Обработчик загруженных файлов

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

Важные концепции

Почему фреймворк?

Краткая статья о том, почему вам стоит использовать фреймворк. Хорошо бы понять преимущества использования фреймворка, прежде чем начать им пользоваться.

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

Flight в сравнении с другими фреймворками

Если вы переходите на Flight с другого фреймворка, такого как Laravel, Slim, Fat-Free или Symfony, эта страница поможет вам понять различия между ними.

Другие темы

Модульное тестирование

Следуйте этому руководству, чтобы узнать, как модульно тестировать ваш код Flight и сделать его по-настоящему надёжным.

ИИ и опыт разработчика

Flight создан для работы с LLM для написания кода: AGENTS.md, команды Runway ai:* и понятная структура скелета — чтобы агенты придерживались одного паттерна.

Переход с v2 на v3

Обратная совместимость в основном сохранена, но есть некоторые изменения, о которых вам следует знать при переходе с v2 на v3.

Learn/unit_testing

Модульное тестирование

Обзор

Модульное тестирование во Flight помогает вам убедиться, что ваше приложение ведёт себя ожидаемо, выявлять ошибки на ранних стадиях и упрощать поддержку кодовой базы. Flight разработан для бесперебойной работы с PHPUnit — самым популярным фреймворком для тестирования PHP.

Понимание

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

Ключевые принципы:

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

Настройка PHPUnit

  1. Установите PHPUnit через Composer:
    composer require --dev phpunit/phpunit
  2. Создайте каталог tests в корне вашего проекта.
  3. Добавьте тестовый скрипт в ваш composer.json:
    "scripts": {
        "test": "phpunit --configuration phpunit.xml"
    }
  4. Создайте файл phpunit.xml:
    <?xml version="1.0" encoding="UTF-8"?>
    <phpunit bootstrap="vendor/autoload.php">
        <testsuites>
            <testsuite name="Flight Tests">
                <directory>tests</directory>
            </testsuite>
        </testsuites>
    </phpunit>

Теперь вы можете запускать тесты с помощью composer test.

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

Предположим, у вас есть маршрут, который проверяет 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\SimplePdo;

class UserController {
    protected $app;
    protected $db;
    protected $mailer;
    public function __construct($app, $db, $mailer) {
        $this->app = $app;
        $this->db = $db;
        $this->mailer = $mailer;
    }
    public function register() {
        $email = $this->app->request()->data->email;
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            return $this->app->json(['status' => 'error', 'message' => 'Invalid email']);
        }
        $this->db->runQuery('INSERT INTO users (email) VALUES (?)', [$email]);
        $this->mailer->sendWelcome($email);
        return $this->app->json(['status' => 'success', 'message' => 'User registered']);
    }
}

И тест с моками:

use PHPUnit\Framework\TestCase;

class UserControllerDICTest extends TestCase {
    public function testValidEmailSavesAndSendsEmail() {
        $mockDb = $this->createMock(flight\database\SimplePdo::class);
        $mockDb->method('runQuery')->willReturn(true);
        $mockMailer = new class {
            public $sentEmail = null;
            public function sendWelcome($email) { $this->sentEmail = $email; return true; }
        };
        $app = new flight\Engine();
        $app->request()->data->email = 'test@example.com';
        $controller = new UserController($app, $mockDb, $mockMailer);
        $controller->register();
        $response = $app->response()->getBody();
        $result = json_decode($response, true);
        $this->assertEquals('success', $result['status']);
        $this->assertEquals('User registered', $result['message']);
        $this->assertEquals('test@example.com', $mockMailer->sentEmail);
    }
}

Продвинутое использование

Смотрите также

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

История изменений

Learn/flight_vs_symfony

Сравнение Flight и Symfony

Что такое Symfony?

Symfony - набор многоразовых компонентов PHP и фреймворк PHP для веб-проектов.

Стандартный фундамент, на котором строятся лучшие приложения на PHP. Выберите любые из 50 доступных автономных компонентов для ваших собственных приложений.

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

Преимущества по сравнению с Flight

Недостатки по сравнению с Flight

Learn/flight_vs_another_framework

Сравнение Flight с другим фреймворком

Если вы переходите с другого фреймворка, такого как Laravel, Slim, Fat-Free или Symfony, на Flight, эта страница поможет вам понять различия между ними.

Laravel

Laravel - это полнофункциональный фреймворк со всеми плюшками и удивительной экосистемой, сосредоточенной на разработчике, но за счет производительности и сложности.

Сравните Laravel и Flight.

Slim

Slim - это микро-фреймворк, похожий на Flight. Он разработан с упором на легкость использования, но может быть немного сложнее, чем Flight.

Сравните Slim и Flight.

Fat-Free

Fat-Free - это полностековый фреймворк в намного меньшем объеме. Хотя в нем есть все необходимые инструменты, его архитектура данных может усложнить некоторые проекты более, чем это необходимо.

Сравните Fat-Free и Flight.

Symfony

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

Сравните Symfony и Flight.

Learn/pdo_wrapper

PdoWrapper Класс-помощник PDO

ПРЕДУПРЕЖДЕНИЕ

Устарело: PdoWrapper устарел начиная с Flight v3.18.0. Он не будет удален в будущих версиях, но будет поддерживаться для обратной совместимости. Пожалуйста, используйте SimplePdo вместо него, который предлагает те же функции плюс дополнительные вспомогательные методы для распространенных операций с базой данных.

Обзор

Класс PdoWrapper в Flight — это удобный помощник для работы с базами данных с использованием PDO. Он упрощает распространенные задачи с базами данных, добавляет полезные методы для получения результатов и возвращает результаты в виде Collections для легкого доступа. Он также поддерживает логирование запросов и мониторинг производительности приложений (APM) для продвинутых случаев использования.

Понимание

Работа с базами данных в PHP может быть немного многословной, особенно при прямом использовании PDO. PdoWrapper расширяет PDO и добавляет методы, которые делают запросы, получение и обработку результатов гораздо проще. Вместо жонглирования подготовленными выражениями и режимами получения вы получаете простые методы для распространенных задач, и каждая строка возвращается как Collection, так что вы можете использовать нотацию массива или объекта.

Вы можете зарегистрировать PdoWrapper как общую службу в Flight, а затем использовать его в любом месте вашего приложения через Flight::db().

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

Регистрация помощника PDO

Сначала зарегистрируйте класс PdoWrapper в Flight:

Flight::register('db', \flight\database\PdoWrapper::class, [
    'mysql:host=localhost;dbname=cool_db_name', 'user', 'pass', [
        PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'utf8mb4\'',
        PDO::ATTR_EMULATE_PREPARES => false,
        PDO::ATTR_STRINGIFY_FETCHES => false,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
    ]
]);

Теперь вы можете использовать Flight::db() в любом месте для получения соединения с базой данных.

Выполнение запросов

runQuery()

function runQuery(string $sql, array $params = []): PDOStatement

Используйте это для INSERT, UPDATE или когда вы хотите получить результаты вручную:

$db = Flight::db();
$statement = $db->runQuery("SELECT * FROM users WHERE status = ?", ['active']);
while ($row = $statement->fetch()) {
    // $row 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 // последний параметр включает 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) — это мощное расширение, которое позволяет управлять зависимостями вашего приложения. Это также одна из главных причин, по которой Flight хорошо сочетается с инструментами ИИ для написания кода и модульными тестами: контроллеры получают то, что им нужно, через конструктор, а не обращаются к глобальным переменным.

Понимание

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

DIC — это удобный способ создания и управления вашими классами в централизованном месте. Это полезно, когда вам нужно передавать один и тот же объект нескольким классам (контроллерам, промежуточному ПО, командам и так далее).

Официальный flightphp/skeleton подключает Dice в app/config/services.php, заменяет общий экземпляр flight\Engine и разрешает цели маршрутов вида [App\Controller\HomeController::class, 'index']. Для новых проектов предпочтительнее использовать именно этот шаблон, чтобы и люди, и агенты редактировали одни и те же места.

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

Старый подход мог выглядеть так:


require 'vendor/autoload.php';

// класс для управления пользователями из базы данных
class UserController {

    protected PDO $pdo;

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

    public function view(int $id) {
        $stmt = $this->pdo->prepare('SELECT * FROM users WHERE id = :id');
        $stmt->execute(['id' => $id]);

        print_r($stmt->fetch());
    }
}

// в вашем файле routes.php

$db = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');

$UserController = new UserController($db);
Flight::route('/user/@id', [ $UserController, 'view' ]);
// другие маршруты UserController...

Flight::start();

Из приведённого выше кода видно, что мы создаём новый объект PDO и передаём его в наш класс UserController. Для небольшого приложения это нормально, но по мере роста приложения вы обнаружите, что создаёте или передаёте один и тот же объект PDO в нескольких местах. Вот тут-то и пригодится DIC.

Вот тот же пример с использованием DIC (на основе Dice):


require 'vendor/autoload.php';

// тот же класс, что и выше. Ничего не изменилось
class UserController {

    protected PDO $pdo;

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

    public function view(int $id) {
        $stmt = $this->pdo->prepare('SELECT * FROM users WHERE id = :id');
        $stmt->execute(['id' => $id]);

        print_r($stmt->fetch());
    }
}

// создаем новый контейнер
$container = new \Dice\Dice;

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

// Это регистрирует обработчик контейнера, чтобы Flight знал о его использовании.
Flight::registerContainerHandler(function($class, $params) use ($container) {
    return $container->create($class, $params);
});

// теперь мы можем использовать контейнер для создания нашего UserController
Flight::route('/user/@id', [ UserController::class, 'view' ]);

Flight::start();

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


// Если все ваши контроллеры имеют конструктор, которому нужен объект PDO
// в каждый из маршрутов ниже он будет внедрен автоматически!!!
Flight::route('/company/@id', [ CompanyController::class, 'view' ]);
Flight::route('/organization/@id', [ OrganizationController::class, 'view' ]);
Flight::route('/category/@id', [ CategoryController::class, 'view' ]);
Flight::route('/settings', [ SettingsController::class, 'view' ]);

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

Создание централизованного обработчика DIC

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

// services.php

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

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

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


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

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

    public function send() {
        // код, который отправляет электронное письмо
    }
}

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

flightphp/container

У Flight есть плагин, предоставляющий простой PSR-11-совместимый контейнер, который можно использовать для управления вашим внедрением зависимостей. Вот краткий пример его использования:


// index.php, например
require 'vendor/autoload.php';

use flight\Container;

$container = new Container;

$container->set(PDO::class, fn(): PDO => new PDO('sqlite::memory:'));

Flight::registerContainerHandler([$container, 'get']);

class TestController {
  private PDO $pdo;

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

  function index() {
    var_dump($this->pdo);
    // выведет это правильно!
  }
}

Flight::route('GET /', [TestController::class, 'index']);

Flight::start();

Продвинутое использование flightphp/container

Вы также можете разрешать зависимости рекурсивно. Вот пример:

<?php

require 'vendor/autoload.php';

use flight\Container;

class User {}

interface UserRepository {
  function find(int $id): ?User;
}

class PdoUserRepository implements UserRepository {
  private PDO $pdo;

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

  function find(int $id): ?User {
    // Реализация ...
    return null;
  }
}

$container = new Container;

$container->set(PDO::class, static fn(): PDO => new PDO('sqlite::memory:'));
$container->set(UserRepository::class, PdoUserRepository::class);

$userRepository = $container->get(UserRepository::class);
var_dump($userRepository);

/*
object(PdoUserRepository)#4 (1) {
  ["pdo":"PdoUserRepository":private]=>
  object(PDO)#3 (0) {
  }
}
 */

DICE

Вы также можете создать свой собственный обработчик DIC. Это полезно, если у вас есть кастомный контейнер, который вы хотите использовать и который не является PSR-11 (Dice). Смотрите базовое использование, чтобы узнать, как это сделать.

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

Экземпляр Engine (требуется для внедрения $app)

Если вы указываете тип flight\Engine в контроллерах или промежуточном ПО, Dice не должен создавать новый Engine. Подставьте тот же экземпляр из начальной загрузки. Именно так делает официальный скелет, и это шаблон, который AGENTS.md ожидает для контроллеров, сгенерированных ИИ:

// Где-то в вашем файле начальной загрузки / services.php
use flight\Engine;
use flight\database\SimplePdo;

$app = Flight::app(); // или $engine = Flight::app();

$container = new \Dice\Dice;
$container = $container->addRule('*', [
    'substitutions' => [
        // Критически важно: используйте загруженный Engine — не позволяйте Dice создавать `new Engine()`
        Engine::class => $app,
        // Для нового кода предпочитайте SimplePdo
        // SimplePdo::class => $db,
        // Config::class => $config,
        // \Twig\Environment::class => $twig,
    ]
]);

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

// Необязательный помощник для кода вне маршрутов
$app->map('make', function ($class, $params = []) use ($container) {
    return $container->create($class, $params);
});
// app/Controller/MyController.php (структура скелета — регистр папки соответствует пространству имен)
namespace App\Controller;

use flight\Engine;

class MyController
{
    protected Engine $app;

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

    public function index(): void
    {
        // Никакого фасада Flight:: в прикладном слое — проще тестировать и понятнее для ИИ-инструментов
        $this->app->render('welcome', ['message' => 'Hello']);
    }
}
// app/config/routes.php
use App\Controller\MyController;

$router->get('/', [MyController::class, 'index']);

Если вы пропустите подстановку Engine, Dice может создать второй Engine, и ваш контроллер не будет использовать общие маршруты, конфигурацию или сопоставленный Twig render из начальной загрузки.

Добавление других общих сервисов (SimplePdo, Config, Twig)

use flight\database\SimplePdo;
use flight\Engine;

// После того как вы создали $db, $config, $twig в services.php:
$substitutions = [
    Engine::class => $app,
    SimplePdo::class => $db,
    // App\Utils\Config::class => $config,
    // \Twig\Environment::class => $twig,
];

$container = $container->addRule('*', [
    'substitutions' => $substitutions,
]);

Тогда контроллеры могут принимать SimplePdo $db (или ваш тип конфигурации) в конструкторе и никогда не вызывать Flight::db(). Это соответствует рекомендациям из модульного тестирования и стилю проекта скелета.

Добавление других классов

Если у вас есть другие классы, которые вы хотите добавить в контейнер, с 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';

use flight\database\SimplePdo;

// та же идея UserController, что и выше, с указанием типа SimplePdo вместо сырого PDO

$container = new \League\Container\Container();
$container->add(UserController::class)->addArgument(SimplePdo::class);
$container->add(SimplePdo::class)
    ->addArgument('mysql:host=localhost;dbname=test')
    ->addArgument('user')
    ->addArgument('pass');
Flight::registerContainerHandler($container);

Flight::route('/user', [ 'UserController', 'view' ]);

Flight::start();

Это может быть немного более многословно, чем предыдущий пример с Dice, но оно выполняет свою задачу с теми же преимуществами!

Смотрите также

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

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

Learn/middleware

Middleware

Обзор

Flight поддерживает middleware для маршрутов и групп маршрутов. Middleware — это часть вашего приложения, где код выполняется до (или после) обратного вызова маршрута. Это отличный способ добавить проверки аутентификации API в ваш код или убедиться, что пользователь имеет разрешение на доступ к маршруту.

Понимание

Middleware может значительно упростить ваше приложение. Вместо сложного наследования абстрактных классов или переопределения методов middleware позволяет контролировать маршруты, присваивая им вашу пользовательскую логику приложения. Вы можете думать о middleware как о сэндвиче. У вас хлеб снаружи, а затем слои ингредиентов, такие как салат, помидоры, мясо и сыр. Затем представьте, что каждый запрос похож на укус сэндвича, где вы сначала едите внешние слои и продвигаетесь к центру.

Вот визуализация того, как работает middleware. Затем мы покажем вам практический пример того, как это функционирует.

Запрос пользователя по 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-шаблонизации. Шаблонизация — это очень эффективный способ отделить логику приложения от уровня представления. Выделенный движок (Twig, Latte и т.д.) также даёт инструментам ИИ для написания кода знакомый ограниченный синтаксис, поэтому они с меньшей вероятностью будут встраивать бизнес-логику прямо в ваш HTML.

Понимание

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

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

Flight позволяет заменить стандартный движок представлений, просто сопоставив render (или зарегистрировав класс представления). Прокрутите вниз, чтобы узнать о Twig, Latte, Smarty, Blade и других.

Параметр скелета по умолчанию: официальный flightphp/skeleton использует только Twig в каталоге app/views/ (*.twig). Контроллеры вызывают $this->app->render('welcome', $data) (расширение необязательно). Это выбор приложения для новых проектов, а не требование ядра Flight. Latte и другие движки полностью поддерживаются.

Twig

по умолчанию в скелете

Twig — это гибкий, быстрый и безопасный шаблонизатор, используемый в Symfony и многих других PHP-проектах. Инструменты ИИ для написания кода особенно хорошо знают Twig, и он по умолчанию автоматически экранирует вывод, что помогает защититься от XSS.

Установка

composer require twig/twig

(Уже включён, если вы выполнили composer create-project flightphp/skeleton.)

Базовая конфигурация

Переопределите метод render, чтобы использовать Twig вместо стандартного PHP-рендерера:

// переопределяем метод render, чтобы использовать Twig вместо стандартного PHP-рендерера
Flight::map('render', function(string $template, array $data): void {
    $loader = new \Twig\Loader\FilesystemLoader(Flight::get('flight.views.path'));
    $twig = new \Twig\Environment($loader, [
        // Где Twig хранит скомпилированные шаблоны
        'cache' => __DIR__ . '/../cache/twig',
        'auto_reload' => true,
    ]);

    // Разрешаем "welcome" или "welcome.twig"
    if (substr($template, -5) !== '.twig') {
        $template .= '.twig';
    }

    echo $twig->render($template, $data);
});

В скелете эта настройка находится в app/config/services.php (общее окружение Twig, путь к кэшу, глобальные переменные, такие как base_url / nonce для CSP). Предпочтительнее внедрять Engine и вызывать $app->render() из контроллеров, чтобы код оставался удобным для ИИ и тестирования.

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

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

{# app/views/home.twig #}
<html>
  <head>
    <title>{% if title %}{{ title }} - {% endif %}My App</title>
    <link rel="stylesheet" href="style.css">
  </head>
  <body>
    <h1>Hello, {{ name }}!</h1>
  </body>
</html>
// routes.php
Flight::route('/@name', function ($name) {
    Flight::render('home.twig', [
        'title' => 'Home Page',
        'name' => $name
    ]);
});

Когда вы откроете /Bob в браузере, результат будет следующим:

<html>
  <head>
    <title>Home Page - My App</title>
    <link rel="stylesheet" href="style.css">
  </head>
  <body>
    <h1>Hello, Bob!</h1>
  </body>
</html>

Дополнительное чтение

Более полный пример использования Twig с макетами приведён в разделе awesome plugins этой документации. О метриках времени рендеринга на панели Tracy см. панель Twig в Tracy Extensions.

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

Latte

отличная альтернатива

Latte — это многофункциональный движок с синтаксисом, похожим на PHP. Он по-прежнему является отличным выбором для приложений на Flight; скелет просто стандартизирует использование Twig как единого варианта по умолчанию (особенно полезно, когда ИИ генерирует шаблоны).

Установка

composer require latte/latte

Базовая конфигурация

Основная идея — переопределить метод render, чтобы использовать Latte вместо стандартного PHP-рендерера.

// переопределяем метод render, чтобы использовать Latte вместо стандартного PHP-рендерера
Flight::map('render', function(string $template, array $data, ?string $block): void {
    $latte = new Latte\Engine;

    // Где Latte хранит свой кэш
    $latte->setTempDirectory(__DIR__ . '/../cache/');

    $finalPath = Flight::get('flight.views.path') . $template;

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

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

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

<!-- app/views/home.latte -->
<html>
  <head>
    <title>{$title ? $title . ' - '}My App</title>
    <link rel="stylesheet" href="style.css">
  </head>
  <body>
    <h1>Hello, {$name}!</h1>
  </body>
</html>
// routes.php
Flight::route('/@name', function ($name) {
    Flight::render('home.latte', [
        'title' => 'Home Page',
        'name' => $name
    ]);
});

Когда вы откроете /Bob в браузере, результат будет следующим:

<html>
  <head>
    <title>Home Page - My App</title>
    <link rel="stylesheet" href="style.css">
  </head>
  <body>
    <h1>Hello, Bob!</h1>
  </body>
</html>

Дополнительное чтение

Более сложный пример использования Latte с макетами приведён в разделе 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 как класс представления
// Также передаём callback-функцию для настройки 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 как класс представления
// Также передаём callback-функцию для настройки BladeOne при загрузке
Flight::register('view', BladeOne::class, [], function (BladeOne $blade) {
  $views = __DIR__ . '/../views';
  $cache = __DIR__ . '/../cache';

  $blade->setPath($views);
  $blade->setCompiledPath($cache);
});

// Передаём данные шаблона
Flight::view()->share('name', 'Bob');

// Отображаем шаблон
echo Flight::view()->run('hello', []);

Для полноты картины вам также следует переопределить стандартный метод render во Flight:

<?php
Flight::map('render', function(string $template, array $data): void {
  echo Flight::view()->run($template, $data);
});

В этом примере файл шаблона hello.blade.php может выглядеть так:

<?php
Hello, {{ $name }}!

Результат будет следующим:

Hello, Bob!

Смотрите также

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

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

Learn/simple_pdo

Класс помощника SimplePdo PDO

Обзор

Класс SimplePdo в Flight — это современный, функциональный помощник для работы с базами данных с использованием PDO. Он расширяет PdoWrapper и добавляет удобные методы-помощники для распространённых операций с базой данных, таких как insert(), update(), delete() и транзакции. Он упрощает задачи работы с базой данных, возвращает результаты в виде Collections для лёгкого доступа и поддерживает логирование запросов и мониторинг производительности приложений (APM) для продвинутых сценариев использования.

Понимание

Класс SimplePdo предназначен для упрощения работы с базами данных в PHP. Вместо манипуляций с подготовленными выражениями, режимами выборки и многословными SQL-операциями вы получаете чистые, простые методы для распространённых задач. Каждая строка возвращается как Collection, так что вы можете использовать нотацию массива ($row['name']) и нотацию объекта ($row->name).

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

Вы можете зарегистрировать SimplePdo как общую службу в Flight, а затем использовать её в любом месте вашего приложения через Flight::db().

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

Регистрация SimplePdo

Сначала зарегистрируйте класс SimplePdo в Flight:

Flight::register('db', \flight\database\SimplePdo::class, [
    'mysql:host=localhost;dbname=cool_db_name', 'user', 'pass', [
        PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'utf8mb4\'',
        PDO::ATTR_EMULATE_PREPARES => false,
        PDO::ATTR_STRINGIFY_FETCHES => false,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
    ]
]);

ПРИМЕЧАНИЕ

Если вы не укажете PDO::ATTR_DEFAULT_FETCH_MODE, SimplePdo автоматически установит его в PDO::FETCH_ASSOC за вас.

Теперь вы можете использовать Flight::db() в любом месте для получения соединения с базой данных.

Выполнение запросов

runQuery()

function runQuery(string $sql, array $params = []): PDOStatement

Используйте это для INSERT, UPDATE или когда вы хотите вручную получить результаты:

$db = Flight::db();
$statement = $db->runQuery("SELECT * FROM users WHERE status = ?", ['active']);
while ($row = $statement->fetch()) {
    // $row 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;

СОВЕТ

SimplePdo автоматически добавляет LIMIT 1 к запросам fetchRow(), если он ещё не присутствует, что делает ваши запросы более эффективными.

fetchAll()

function fetchAll(string $sql, array $params = []): array<Collection>

Получите все строки как массив Collections:

$users = Flight::db()->fetchAll("SELECT * FROM users WHERE status = ?", ['active']);
foreach ($users as $user) {
    echo $user['name'];
    // or
    echo $user->name;
}

fetchColumn()

function fetchColumn(string $sql, array $params = []): array

Выберите одну колонку как массив:

$ids = Flight::db()->fetchColumn("SELECT id FROM users WHERE active = ?", [1]);
// Returns: [1, 2, 3, 4, 5]

fetchPairs()

function fetchPairs(string $sql, array $params = []): array

Выберите результаты как пары ключ-значение (первая колонка как ключ, вторая как значение):

$userNames = Flight::db()->fetchPairs("SELECT id, name FROM users");
// Returns: [1 => 'John', 2 => 'Jane', 3 => 'Bob']

Использование заполнителей IN()

Вы можете использовать один ? в предложении IN() и передать массив:

$ids = [1, 2, 3];
$users = Flight::db()->fetchAll("SELECT * FROM users WHERE id IN (?)", [$ids]);

Методы-помощники

Одно из главных преимуществ SimplePdo по сравнению с PdoWrapper — добавление удобных методов-помощников для распространённых операций с базой данных.

insert()

function insert(string $table, array $data): string

Вставьте одну или несколько строк и верните ID последней вставки.

Одиночная вставка:

$id = Flight::db()->insert('users', [
    'name' => 'John',
    'email' => 'john@example.com'
]);

Пакетная вставка:

$id = Flight::db()->insert('users', [
    ['name' => 'John', 'email' => 'john@example.com'],
    ['name' => 'Jane', 'email' => 'jane@example.com'],
]);

update()

function update(string $table, array $data, string $where, array $whereParams = []): int

Обновите строки и верните количество затронутых строк:

$affected = Flight::db()->update(
    'users',
    ['name' => 'Jane', 'email' => 'jane@example.com'],
    'id = ?',
    [1]
);

ПРИМЕЧАНИЕ

В SQLite rowCount() возвращает количество строк, где данные действительно изменились. Если вы обновляете строку с теми же значениями, которые у неё уже есть, rowCount() вернёт 0. Это отличается от поведения MySQL при использовании PDO::MYSQL_ATTR_FOUND_ROWS.

delete()

function delete(string $table, string $where, array $whereParams = []): int

Удалите строки и верните количество удалённых строк:

$deleted = Flight::db()->delete('users', 'id = ?', [1]);

transaction()

function transaction(callable $callback): mixed

Выполните обратный вызов в рамках транзакции. Транзакция автоматически фиксируется при успехе или откатывается при ошибке:

$result = Flight::db()->transaction(function($db) {
    $db->insert('users', ['name' => 'John']);
    $db->insert('logs', ['action' => 'user_created']);
    return $db->lastInsertId();
});

Если в обратном вызове возникает любое исключение, транзакция автоматически откатывается, и исключение повторно выбрасывается.

Продвинутое использование

Логирование запросов и APM

Если вы хотите отслеживать производительность запросов, включите отслеживание APM при регистрации:

Flight::register('db', \flight\database\SimplePdo::class, [
    'mysql:host=localhost;dbname=cool_db_name',
    'user',
    'pass',
    [/* PDO options */],
    [
        'trackApmQueries' => true,
        'maxQueryMetrics' => 1000
    ]
]);

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

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

Это вызовет событие (flight.db.queries) с метриками соединения и запросов, на которое вы можете подписаться с помощью системы событий Flight.

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

Flight::route('/users', function () {
    // 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');

    // Get a single column
    $ids = Flight::db()->fetchColumn('SELECT id FROM users');

    // Get key-value pairs
    $userNames = Flight::db()->fetchPairs('SELECT id, name FROM users');

    // Special IN() syntax
    $users = Flight::db()->fetchAll('SELECT * FROM users WHERE id IN (?)', [[1,2,3,4,5]]);

    // Insert a new user
    $id = Flight::db()->insert('users', [
        'name' => 'Bob',
        'email' => 'bob@example.com'
    ]);

    // Bulk insert users
    Flight::db()->insert('users', [
        ['name' => 'Bob', 'email' => 'bob@example.com'],
        ['name' => 'Jane', 'email' => 'jane@example.com']
    ]);

    // Update a user
    $affected = Flight::db()->update('users', ['name' => 'Bob'], 'id = ?', [123]);

    // Delete a user
    $deleted = Flight::db()->delete('users', 'id = ?', [123]);

    // Use a transaction
    $result = Flight::db()->transaction(function($db) {
        $db->insert('users', ['name' => 'John', 'email' => 'john@example.com']);
        $db->insert('audit_log', ['action' => 'user_created']);
        return $db->lastInsertId();
    });
});

Миграция с PdoWrapper

Если вы сейчас используете PdoWrapper, миграция на SimplePdo будет простой:

  1. Обновите вашу регистрацию:

    // Old
    Flight::register('db', \flight\database\PdoWrapper::class, [ /* ... */ ]);
    
    // New
    Flight::register('db', \flight\database\SimplePdo::class, [ /* ... */ ]);
  2. Все существующие методы PdoWrapper работают в SimplePdo — Нет разрушительных изменений. Ваш существующий код продолжит работать.

  3. Опционально используйте новые методы-помощники — Начните использовать insert(), update(), delete() и transaction() для упрощения вашего кода.

См. также

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

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

Learn/collections

Коллекции

Обзор

Класс Collection во Flight — это удобный инструмент для управления наборами данных. Он позволяет получать доступ к данным и управлять ими как через синтаксис массивов, так и через синтаксис объектов, делая ваш код чище и гибче.

Понимание

Collection — это, по сути, обёртка над массивом, но с дополнительными возможностями. Вы можете использовать его как массив, перебирать его, подсчитывать элементы и даже обращаться к элементам как к свойствам объекта. Это особенно полезно, когда нужно передавать структурированные данные в приложении или сделать код более читаемым.

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

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

Создание коллекции

Вы можете создать коллекцию, просто передав массив в её конструктор:

use flight\util\Collection;

$data = [
  'name' => 'Flight',
  'version' => 3,
  'features' => ['routing', 'views', 'extending']
];

$collection = new Collection($data);

Доступ к элементам

Вы можете получать доступ к элементам, используя как синтаксис массива, так и синтаксис объекта:

// Синтаксис массива
echo $collection['name']; // Вывод: FlightPHP

// Синтаксис объекта
echo $collection->version; // Вывод: 3

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

Установка элементов

Вы также можете устанавливать элементы, используя любую из этих нотаций:

// Синтаксис массива
$collection['author'] = 'Mike Cao';

// Синтаксис объекта
$collection->license = 'MIT';

Проверка и удаление элементов

Проверьте, существует ли элемент:

if (isset($collection['name'])) {
  // Сделать что-то
}

if (isset($collection->version)) {
  // Сделать что-то
}

Удалите элемент:

unset($collection['author']);
unset($collection->license);

Перебор коллекции

Коллекции являются итерируемыми, поэтому вы можете использовать их в цикле foreach:

foreach ($collection as $key => $value) {
  echo "$key: $value\n";
}

Подсчёт элементов

Вы можете подсчитать количество элементов в коллекции:

echo count($collection); // Вывод: 4

Получение всех ключей или данных

Получить все ключи:

$keys = $collection->keys(); // ['name', 'version', 'features', 'license']

Получить все данные в виде массива:

$data = $collection->getData();

Очистка коллекции

Удалить все элементы:

$collection->clear();

Сериализация в JSON

Коллекции можно легко преобразовать в JSON:

echo json_encode($collection);
// Вывод: {"name":"FlightPHP","version":3,"features":["routing","views","extending"],"license":"MIT"}

Продвинутое использование

При необходимости вы можете полностью заменить внутренний массив данных:

$collection->setData(['foo' => 'bar']);

Коллекции особенно полезны, когда нужно передавать структурированные данные между компонентами или предоставить более объектно-ориентированный интерфейс для данных массива.

Смотрите также

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

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

Learn/flight_vs_fat_free

Flight против Fat-Free

Что такое Fat-Free?

Fat-Free (ласково известный как F3) — это мощный, но простой в использовании PHP-микрофреймворк, предназначенный для быстрого создания динамических и надежных веб-приложений.

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

Обновления становятся все реже, а сообщество уже не так активно, как прежде. Код достаточно прост, но иногда отсутствие дисциплины в синтаксисе может затруднять чтение и понимание. Он работает с PHP 8.3, но сам код все еще выглядит так, будто он застрял в PHP 5.3.

Преимущества по сравнению с Flight

Недостатки по сравнению с Flight

Learn/extending

Расширение

Обзор

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

Понимание

Есть 2 способа расширить функциональность Flight:

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

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

Если вы ищете DIC (Dependency Injection Container), перейдите на страницу Dependency Injection Container.

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

Переопределение методов фреймворка

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

Например, когда Flight не может сопоставить URL с маршрутом, он вызывает метод notFound, который отправляет общий ответ HTTP 404. Вы можете переопределить это поведение, используя метод map:

Flight::map('notFound', function() {
  // Отображение пользовательской страницы 404
  include 'errors/404.html';
});

Flight также позволяет заменить основные компоненты фреймворка. Например, вы можете заменить стандартный класс Router на свой собственный пользовательский класс:

// создание вашего пользовательского класса 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 против Slim

Что такое Slim?

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

Многие идеи для некоторых функций v3 во Flight на самом деле пришли из Slim. Группировка маршрутов и выполнение middleware в определенном порядке — две функции, вдохновленные Slim. Slim v3 вышел с ориентацией на простоту, но о v4 были неоднозначные отзывы.

Преимущества по сравнению с Flight

Недостатки по сравнению с Flight

Learn/autoloading

Автозагрузка

Обзор

Автозагрузка — это концепция в PHP, при которой вы указываете каталог или каталоги для загрузки классов. Это гораздо полезнее, чем использование require или include для загрузки классов. Это также требование для использования пакетов Composer.

Правильная настройка автозагрузки важна и для разработки с помощью ИИ: агенты размещают файлы там, куда указывает пространство имён. Если регистр папок и регистр пространства имён не совпадают, на Linux появляются ошибки «класс не найден», даже если на Mac с регистронезависимой файловой системой всё «работало».

Понимание

По умолчанию все классы Flight автоматически загружаются благодаря Composer. Для ваших классов приложения есть два распространённых подхода:

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

Использование автозагрузчика значительно упрощает ваш код. Вместо стены из include / require в начале каждого файла классы загружаются при первом использовании.

Чувствительность к регистру (прочитайте дважды)

Пространства имён должны соответствовать структуре каталогов и регистру букв в этих каталогах.

Работает Ломается на Linux
App\Controller\HomeControllerapp/Controller/HomeController.php App\Controller\… с каталогом app/controllers/
app\controllers\MyControllerapp/controllers/MyController.php Смешивание App\ с нижним регистром controllers

Пространства имён PHP не чувствительны к регистру в некоторых контекстах, но Composer и файловая система — нет. Официальный скелетон придерживается следующих стандартов:

В старой документации и примерах сообщества иногда использовался нижний регистр app\controllers. Это по-прежнему работает, если ваши каталоги в нижнем регистре, — но новые проекты на скелетоне используют App\ + каталоги в PascalCase. Выберите одно соглашение для проекта и придерживайтесь его, чтобы люди и ИИ-инструменты не придумывали вторую структуру.

Скелетон (рекомендуется для новых проектов)

После composer create-project flightphp/skeleton код приложения автозагружается через Composer — Flight::path() для классов App\ не требуется:

{
  "autoload": {
    "psr-4": {
      "App\\": "app/"
    }
  }
}
// app/Controller/HomeController.php
namespace App\Controller;

use flight\Engine;

class HomeController
{
    protected Engine $app;

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

    public function index(): void
    {
        $this->app->render('welcome', ['message' => 'Hello!']);
    }
}
// app/config/routes.php — Dice сопоставляет App\Controller\… через контейнер
$router->get('/', [HomeController::class, 'index']);

Смотрите Установка для полного дерева каталогов и ИИ и опыт разработчика о том, как AGENTS.md документирует эту структуру для помощников по коду.

Базовое использование (Flight::path())

Предположим, у нас есть следующее дерево каталогов:

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

Вы могли заметить, что это похоже на типичное дерево приложения (сам сайт документации использует структурированную раскладку). Нижний регистр controllers здесь — допустимый выбор; просто это не текущее значение по умолчанию для скелетона.

Вы можете указать каждый каталог для загрузки следующим образом:


/**
 * public/index.php
 */

// Добавляем путь в автозагрузчик
Flight::path(__DIR__.'/../app/controllers/');
Flight::path(__DIR__.'/../app/utils/');


/**
 * app/controllers/MyController.php
 */

// пространство имён не требуется

// Все автозагружаемые классы рекомендуется именовать в Pascal Case (каждое слово с заглавной буквы, без пробелов)
class MyController {

    public function index() {
        // что-то делаем
    }
}

Пространства имён с Flight::path()

Если у вас есть пространства имён, это реализуется очень легко. Вам следует использовать метод Flight::path() для указания корневого каталога (не корня документа и не папки public/) вашего приложения.


/**
 * public/index.php
 */

// Добавляем путь в автозагрузчик
Flight::path(__DIR__.'/../');

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

/**
 * app/controllers/MyController.php
 */

// пространства имён обязательны
// пространства имён совпадают со структурой каталогов
// пространства имён должны совпадать по регистру со структурой каталогов
// пространства имён и каталоги не могут содержать подчёркивания (если не задано Loader::setV2ClassLoading(false))
namespace app\controllers;

// Все автозагружаемые классы рекомендуется именовать в Pascal Case (каждое слово с заглавной буквы, без пробелов)
// Начиная с 3.7.2, вы можете использовать Pascal_Snake_Case для имён классов, выполнив Loader::setV2ClassLoading(false);
class MyController {

    public function index() {
        // что-то делаем
    }
}

Если вы хотите автозагрузить класс в вашем каталоге utils, вы делаете в основном то же самое:


/**
 * app/UTILS/ArrayHelperUtil.php
 */

// пространство имён должно соответствовать структуре каталогов и регистру (обратите внимание, что каталог UTILS заглавными буквами
//     как в дереве файлов выше)
namespace app\UTILS;

class ArrayHelperUtil {

    public function changeArrayCase(array $array) {
        // что-то делаем
    }
}

Пространство имён в стиле скелетона (те же правила, другой регистр)

/**
 * app/Controller/MyController.php
 */
namespace App\Controller;

class MyController {
    // ...
}

Правило не изменилось — изменился только выбранный скелетоном регистр каталогов/пространств имён. Какой бы регистр вы ни использовали для папок, строка namespace должна ему соответствовать.

Подчёркивания в именах классов

Начиная с 3.7.2, вы можете использовать Pascal_Snake_Case для имён классов, выполнив Loader::setV2ClassLoading(false);. Это позволяет использовать подчёркивания в именах классов. Это не рекомендуется, но доступно тем, кому это нужно.

use flight\core\Loader;

/**
 * public/index.php
 */

// Добавляем путь в автозагрузчик
Flight::path(__DIR__.'/../app/controllers/');
Flight::path(__DIR__.'/../app/utils/');
Loader::setV2ClassLoading(false);

/**
 * app/controllers/My_Controller.php
 */

// пространство имён не требуется

class My_Controller {

    public function index() {
        // что-то делаем
    }
}

Смотрите также

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

Класс не найден (автозагрузка не работает)

Причин этой проблемы может быть несколько. Ниже приведены некоторые примеры.

Неправильное имя файла

Самая распространённая причина — имя класса не совпадает с именем файла.

Если у вас есть класс с именем MyClass, файл должен называться MyClass.php. Если у вас есть класс MyClass, а файл называется myclass.php, автозагрузчик не сможет его найти.

Неправильное пространство имён или регистр папки

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

// ...код...

// если ваш MyController находится в app/Controller (скелетон) и пространство имён App\Controller
// это не будет работать:
Flight::route('/hello', 'MyController->hello');

// Стиль скелетона:
use App\Controller\MyController;
Flight::route('/hello', [ MyController::class, 'hello' ]);

// Старая раскладка с нижним регистром (только если ваши папки на самом деле app/controllers):
use app\controllers\MyController;
Flight::route('/hello', [ MyController::class, 'hello' ]);
// или полностью определённое имя:
Flight::route('/hello', [ 'App\Controller\MyController', 'hello' ]);

path() не определён (код приложения без Composer)

Если вы полагаетесь на Flight::path() вместо Composer для классов приложения, определите путь до маршрутов, которые ссылаются на эти классы (часто в начале загрузчика / public/index.php):

// Добавляем путь в автозагрузчик (корень проекта для приложений с пространствами имён)
Flight::path(__DIR__.'/../');

Официальный скелетон в основном использует Composer PSR-4 для App\, поэтому обычно вам не понадобится Flight::path() для контроллеров и моделей там.

История изменений

Learn/uploaded_file

Обработчик загруженного файла

Обзор

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

Понимание

Когда пользователь загружает файл через форму, PHP сохраняет информацию о файле в суперглобальной переменной $_FILES. В Flight вы редко взаимодействуете с $_FILES напрямую. Вместо этого объект Request Flight (доступный через Flight::request()) предоставляет метод getUploadedFiles(), который возвращает массив объектов UploadedFile, делая обработку файлов гораздо более удобной и надежной.

Класс UploadedFile предоставляет методы для:

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

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

Доступ к загруженным файлам из запроса

Рекомендуемый способ доступа к загруженным файлам — через объект запроса:

Flight::route('POST /upload', function() {
    // Для поля формы <input type="file" name="myFile">
    $uploadedFiles = Flight::request()->getUploadedFiles();
    $file = $uploadedFiles['myFile'];

    // Теперь вы можете использовать методы UploadedFile
    if ($file->getError() === UPLOAD_ERR_OK) {
        $file->moveTo('/path/to/uploads/' . $file->getClientFilename());
        echo "Файл успешно загружен!";
    } else {
        echo "Загрузка не удалась: " . $file->getError();
    }
});

Обработка множественной загрузки файлов

Если ваша форма использует name="myFiles[]" для множественной загрузки, вы получите массив объектов UploadedFile:

Flight::route('POST /upload', function() {
    // Для поля формы <input type="file" name="myFiles[]">
    $uploadedFiles = Flight::request()->getUploadedFiles();
    foreach ($uploadedFiles['myFiles'] as $file) {
        if ($file->getError() === UPLOAD_ERR_OK) {
            $file->moveTo('/path/to/uploads/' . $file->getClientFilename());
            echo "Загружено: " . $file->getClientFilename() . "<br>";
        } else {
            echo "Не удалось загрузить: " . $file->getClientFilename() . "<br>";
        }
    }
});

Создание экземпляра UploadedFile вручную

Обычно вы не создаете UploadedFile вручную, но это возможно при необходимости:

use flight\net\UploadedFile;

$file = new UploadedFile(
  $_FILES['myfile']['name'],
  $_FILES['myfile']['type'],
  $_FILES['myfile']['size'],
  $_FILES['myfile']['tmp_name'],
  $_FILES['myfile']['error']
);

Доступ к информации о файле

Вы можете легко получить детали о загруженном файле:

echo $file->getClientFilename();   // Оригинальное имя файла с компьютера пользователя
echo $file->getClientMediaType();  // Тип MIME (например, image/png)
echo $file->getSize();             // Размер файла в байтах
echo $file->getTempName();         // Временный путь к файлу на сервере
echo $file->getError();            // Код ошибки загрузки (0 означает отсутствие ошибки)

Перемещение загруженного файла

После валидации файла переместите его в постоянное расположение:

try {
  $file->moveTo('/path/to/uploads/' . $file->getClientFilename());
  echo "Файл успешно загружен!";
} catch (Exception $e) {
  echo "Загрузка не удалась: " . $e->getMessage();
}

Метод moveTo() вызовет исключение, если что-то пойдет не так (например, ошибка загрузки или проблема с правами доступа).

Обработка ошибок загрузки

Если во время загрузки возникла проблема, вы можете получить читаемое сообщение об ошибке:

if ($file->getError() !== UPLOAD_ERR_OK) {
  // Вы можете использовать код ошибки или поймать исключение от moveTo()
  echo "Произошла ошибка при загрузке файла.";
}

См. также

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

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

Guides/unit_testing

Модульное тестирование в Flight PHP с PHPUnit

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

Зачем нужно модульное тестирование?

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

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

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

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

Настройка PHPUnit

Сначала настройте PHPUnit в вашем проекте Flight PHP с помощью Composer для удобного тестирования. См. Руководство по началу работы с PHPUnit для подробностей.

  1. В каталоге вашего проекта выполните:

    composer require --dev phpunit/phpunit

    Это установит последнюю версию PHPUnit как зависимость для разработки.

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

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

    // остальное содержимое composer.json
    "scripts": {
        "test": "phpunit --configuration phpunit.xml"
    }
  4. Создайте файл phpunit.xml в корне:

    <?xml version="1.0" encoding="UTF-8"?>
    <phpunit bootstrap="vendor/autoload.php">
        <testsuites>
            <testsuite name="Flight Tests">
                <directory>tests</directory>
            </testsuite>
        </testsuites>
    </phpunit>

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

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

Начнем с простого маршрута, который проверяет ввод email пользователя. Мы протестируем его поведение: возврат сообщения об успехе для корректных email и ошибки для некорректных. Для проверки email мы используем filter_var.

// index.php
$app->route('POST /register', [ UserController::class, 'register' ]);

// UserController.php
class UserController {
    protected $app;

    public function __construct(flight\Engine $app) {
        $this->app = $app;
    }

    public function register() {
        $email = $this->app->request()->data->email;
        $responseArray = [];
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            $responseArray = ['status' => 'error', 'message' => 'Invalid email'];
        } else {
            $responseArray = ['status' => 'success', 'message' => 'Valid email'];
        }

        $this->app->json($responseArray);
    }
}

Для тестирования создайте файл теста. См. Модульное тестирование и принципы SOLID для получения дополнительной информации о структуре тестов:

// tests/UserControllerTest.php
use PHPUnit\Framework\TestCase;
use Flight;
use flight\Engine;

class UserControllerTest extends TestCase {

    public function testValidEmailReturnsSuccess() {
        $app = new Engine();
        $request = $app->request();
        $request->data->email = 'test@example.com'; // Имитация POST-данных
        $UserController = new UserController($app);
        $UserController->register($request->data->email);
        $response = $app->response()->getBody();
        $output = json_decode($response, true);
        $this->assertEquals('success', $output['status']);
        $this->assertEquals('Valid email', $output['message']);
    }

    public function testInvalidEmailReturnsError() {
        $app = new Engine();
        $request = $app->request();
        $request->data->email = 'invalid-email'; // Имитация POST-данных
        $UserController = new UserController($app);
        $UserController->register($request->data->email);
        $response = $app->response()->getBody();
        $output = json_decode($response, true);
        $this->assertEquals('error', $output['status']);
        $this->assertEquals('Invalid email', $output['message']);
    }
}

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

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

Использование внедрения зависимостей для тестируемых контроллеров

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

Давайте использовать flight\database\SimplePdo вместо сырого PDO. Этот помощник гораздо проще имитировать и тестировать (и он предпочтительнее устаревшего PdoWrapper).

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

use flight\database\SimplePdo;

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

    public function __construct(Engine $app, SimplePdo $db, MailerInterface $mailer) {
        $this->app = $app;
        $this->db = $db;
        $this->mailer = $mailer;
    }

    public function register() {
        $email = $this->app->request()->data->email;
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            // добавление return здесь помогает модульному тестированию остановить выполнение
            return $this->app->jsonHalt(['status' => 'error', 'message' => 'Invalid email']);
        }

        $this->db->runQuery('INSERT INTO users (email) VALUES (?)', [$email]);
        $this->mailer->sendWelcome($email);

        return $this->app->json(['status' => 'success', 'message' => 'User registered']);
    }
}

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

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

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

// tests/UserControllerDICTest.php
use flight\database\SimplePdo;
use PHPUnit\Framework\TestCase;

class UserControllerDICTest extends TestCase {
    public function testValidEmailSavesAndSendsEmail() {

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

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

    public function testInvalidEmailSkipsSaveAndEmail() {
         $mockDb = new class() extends SimplePdo {
            // Пустой конструктор обходит родительский конструктор
            public function __construct() {}
            public function runQuery(string $sql, array $params = []): PDOStatement {
                throw new Exception('Should not be called');
            }
        };
        $mockMailer = new class implements MailerInterface {
            public $sentEmail = null;
            public function sendWelcome($email): bool {
                throw new Exception('Should not be called');
            }
        };
        $app = new Engine();
        $app->request()->data->email = 'invalid-email';

        // Необходимо сопоставить jsonHalt, чтобы избежать выхода
        $app->map('jsonHalt', function($data) use ($app) {
            $app->json($data, 400);
        });
        $controller = new UserControllerDIC($app, $mockDb, $mockMailer);
        $controller->register();
        $response = $app->response()->getBody();
        $result = json_decode($response, true);
        $this->assertEquals('error', $result['status']);
        $this->assertEquals('Invalid email', $result['message']);
    }
}

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

Чрезмерная имитация

Будьте осторожны, не имитируйте слишком большую часть вашего кода. Приведу пример, почему это может быть плохо, на основе нашего UserController. Мы изменим проверку на метод isEmailValid (используя filter_var), а остальные новые добавления в отдельный метод registerUser.

use flight\database\SimplePdo;
use flight\Engine;

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

    public function __construct(Engine $app, SimplePdo $db, MailerInterface $mailer) {
        $this->app = $app;
        $this->db = $db;
        $this->mailer = $mailer;
    }

    public function register() {
        $email = $this->app->request()->data->email;
        if (!$this->isEmailValid($email)) {
            // добавление return здесь помогает модульному тестированию остановить выполнение
            return $this->app->jsonHalt(['status' => 'error', 'message' => 'Invalid email']);
        }

        $this->registerUser($email);

        $this->app->json(['status' => 'success', 'message' => 'User registered']);
    }

    protected function isEmailValid($email) {
        return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
    }

    protected function registerUser($email) {
        $this->db->runQuery('INSERT INTO users (email) VALUES (?)', [$email]);
        $this->mailer->sendWelcome($email);
    }
}

А теперь пере-имитированный модульный тест, который на самом деле ничего не тестирует:

use PHPUnit\Framework\TestCase;

class UserControllerTest extends TestCase {
    public function testValidEmailSavesAndSendsEmail() {
        $app = new Engine();
        $app->request()->data->email = 'test@example.com';
        // мы пропускаем дополнительное внедрение зависимостей здесь, потому что это "легко"
        $controller = new class($app) extends UserControllerDICV2 {
            protected $app;
            // Обходим зависимости в конструкторе
            public function __construct($app) {
                $this->app = $app;
            }

            // Просто принудительно сделаем это валидным.
            protected function isEmailValid($email) {
                return true; // Всегда возвращает true, обходя реальную проверку
            }

            // Обходим реальные вызовы БД и почтового сервиса
            protected function registerUser($email) {
                return false;
            }
        };
        $controller->register();
        $response = $app->response()->getBody();
        $result = json_decode($response, true);
        $this->assertEquals('success', $result['status']);
        $this->assertEquals('User registered', $result['message']);
    }
}

Ура, у нас есть модульные тесты, и они проходят! Но подождите, что если я действительно изменю внутреннюю работу isEmailValid или registerUser? Мои тесты все равно будут проходить, потому что я имитировал все функциональности. Позвольте показать, что я имею в виду.

// UserControllerDICV2.php
class UserControllerDICV2 {

    // ... другие методы ...

    protected function isEmailValid($email) {
        // Измененная логика
        $validEmail = filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
        // Теперь должен быть только определенный домен
        $validDomain = strpos($email, '@example.com') !== false; 
        return $validEmail && $validDomain;
    }
}

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

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

Полный пример проекта Flight PHP с модульными тестами можно найти на GitHub: n0nag0n/flight-unit-tests-guide. Для более глубокого понимания см. Модульное тестирование и принципы SOLID.

Частые ошибки

Масштабирование с помощью модульных тестов

Модульные тесты особенно полезны в крупных проектах или при возвращении к коду спустя месяцы. Они документируют поведение и выявляют регрессии, избавляя вас от необходимости заново изучать приложение. Для разработчиков-одиночек тестируйте критические пути (например, регистрацию пользователей, обработку платежей). Для команд тесты обеспечивают согласованное поведение при внесении изменений. См. Зачем нужны фреймворки? для получения дополнительной информации о преимуществах использования фреймворков и тестов.

Внесите свои собственные советы по тестированию в репозиторий документации Flight PHP!

Автор: n0nag0n 2025

Guides/blog

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

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

Предварительные требования

Шаг 1: Настройка проекта

Начните с создания нового каталога проекта и установки Flight через Composer.

  1. Создайте каталог:

    mkdir flight-blog
    cd flight-blog
  2. Установите Flight:

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

    mkdir public
  4. Базовый index.php: Создайте public/index.php с простым маршрутом «hello world»:

    <?php
    require '../vendor/autoload.php';
    
    Flight::route('/', function () {
        echo 'Hello, Flight!';
    });
    
    Flight::start();
  5. Запустите встроенный сервер: Проверьте настройку с помощью встроенного сервера разработки PHP:

    php -S localhost:8000 -t public/

    Перейдите по адресу http://localhost:8000, чтобы увидеть «Hello, Flight!».

Шаг 2: Организация структуры проекта

Для аккуратной настройки структурируйте проект следующим образом:

flight-blog/
├── app/
│   ├── config/
│   └── views/
├── data/
├── public/
│   └── index.php
├── vendor/
└── composer.json

Шаг 3: Установка и настройка Latte

Latte — это легкий шаблонизатор, который хорошо интегрируется с Flight.

  1. Установите Latte:

    composer require latte/latte
  2. Настройте Latte во Flight: Обновите public/index.php, чтобы зарегистрировать Latte в качестве шаблонизатора представлений:

    <?php
    require '../vendor/autoload.php';
    
    use Latte\Engine;
    
    Flight::register('view', Engine::class, [], function ($latte) {
        $latte->setTempDirectory(__DIR__ . '/../cache/');
        $latte->setLoader(new \Latte\Loaders\FileLoader(__DIR__ . '/../app/views/'));
    });
    
    Flight::route('/', function () {
        Flight::view()->render('home.latte', ['title' => 'My Blog']);
    });
    
    Flight::start();
  3. Создайте шаблон макета: В app/views/layout.latte:

    <!DOCTYPE html>
    <html>
    <head>
        <title>{$title}</title>
    </head>
    <body>
        <header>
            <h1>My Blog</h1>
            <nav>
                <a href="/">Home</a> | 
                <a href="/create">Create a Post</a>
            </nav>
        </header>
        <main>
            {block content}{/block}
        </main>
        <footer>
            <p>&copy; {date('Y')} Flight Blog</p>
        </footer>
    </body>
    </html>
  4. Создайте домашний шаблон: В app/views/home.latte:

    {extends 'layout.latte'}
    
     {block content}
         <h2>{$title}</h2>
         <ul>
         {foreach $posts as $post}
             <li><a href="/post/{$post['slug']}">{$post['title']}</a></li>
         {/foreach}
         </ul>
     {/block}

    Перезапустите сервер, если вы его остановили, и перейдите по адресу http://localhost:8000, чтобы увидеть отрендеренную страницу.

  5. Создайте файл данных: Используйте JSON-файл для имитации базы данных для простоты.

    В data/posts.json:

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

Шаг 4: Определение маршрутов

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

  1. Создайте routes.php: В app/config/routes.php:

    <?php
    Flight::route('/', function () {
        Flight::view()->render('home.latte', ['title' => 'My Blog']);
    });
    
    Flight::route('/post/@slug', function ($slug) {
        Flight::view()->render('post.latte', ['title' => 'Post: ' . $slug, 'slug' => $slug]);
    });
    
    Flight::route('GET /create', function () {
        Flight::view()->render('create.latte', ['title' => 'Create a Post']);
    });
  2. Обновите index.php: Подключите файл маршрутов:

    <?php
    require '../vendor/autoload.php';
    
    use Latte\Engine;
    
    Flight::register('view', Engine::class, [], function ($latte) {
        $latte->setTempDirectory(__DIR__ . '/../cache/');
        $latte->setLoader(new \Latte\Loaders\FileLoader(__DIR__ . '/../app/views/'));
    });
    
    require '../app/config/routes.php';
    
    Flight::start();

Шаг 5: Сохранение и получение записей блога

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

  1. Добавьте метод Posts: В index.php добавьте метод для загрузки записей:

    Flight::map('posts', function () {
        $file = __DIR__ . '/../data/posts.json';
        return json_decode(file_get_contents($file), true);
    });
  2. Обновите маршруты: Измените app/config/routes.php, чтобы использовать записи:

    <?php
    Flight::route('/', function () {
        $posts = Flight::posts();
        Flight::view()->render('home.latte', [
            'title' => 'My Blog',
            'posts' => $posts
        ]);
    });
    
    Flight::route('/post/@slug', function ($slug) {
        $posts = Flight::posts();
        $post = array_filter($posts, fn($p) => $p['slug'] === $slug);
        $post = reset($post) ?: null;
        if (!$post) {
            Flight::notFound();
            return;
        }
        Flight::view()->render('post.latte', [
            'title' => $post['title'],
            'post' => $post
        ]);
    });
    
    Flight::route('GET /create', function () {
        Flight::view()->render('create.latte', ['title' => 'Create a Post']);
    });

Шаг 6: Создание шаблонов

Обновите шаблоны для отображения записей.

  1. Страница записи (app/views/post.latte):

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

Шаг 7: Добавление создания записей

Обработайте отправку формы для добавления новых записей.

  1. Создайте форму (app/views/create.latte):

    {extends 'layout.latte'}
    
     {block content}
         <h2>{$title}</h2>
         <form method="POST" action="/create">
             <div class="form-group">
                 <label for="title">Title:</label>
                 <input type="text" name="title" id="title" required>
             </div>
             <div class="form-group">
                 <label for="content">Content:</label>
                 <textarea name="content" id="content" required></textarea>
             </div>
             <button type="submit">Save Post</button>
         </form>
     {/block}
  2. Добавьте POST-маршрут: В app/config/routes.php:

    Flight::route('POST /create', function () {
        $request = Flight::request();
        $title = $request->data['title'];
        $content = $request->data['content'];
        $slug = strtolower(str_replace(' ', '-', $title));
    
        $posts = Flight::posts();
        $posts[] = ['slug' => $slug, 'title' => $title, 'content' => $content];
        file_put_contents(__DIR__ . '/../../data/posts.json', json_encode($posts, JSON_PRETTY_PRINT));
    
        Flight::redirect('/');
    });
  3. Проверьте:

    • Перейдите по адресу http://localhost:8000/create.
    • Отправьте новую запись (например, «Second Post» с некоторым содержимым).
    • Проверьте главную страницу, чтобы увидеть её в списке.

Шаг 8: Улучшение с помощью обработки ошибок

Переопределите метод notFound для лучшего отображения ошибки 404.

В index.php:

Flight::map('notFound', function () {
    Flight::view()->render('404.latte', ['title' => 'Page Not Found']);
});

Создайте app/views/404.latte:

{extends 'layout.latte'}

{block content}
    <h2>404 - {$title}</h2>
    <p>Sorry, that page doesn't exist!</p>
{/block}

Следующие шаги

Заключение

Вы создали простой блог с помощью Flight PHP! Это руководство демонстрирует основные возможности, такие как маршрутизация, шаблонизация с Latte и обработка отправки форм — всё это остается легковесным. Изучите документацию Flight, чтобы узнать о более продвинутых функциях и развить ваш блог дальше!

License

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

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

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

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

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

About

Flight PHP Framework

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

Почему стоит выбрать Flight?

Обзор в видео

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

Быстрый старт

Для быстрой минимальной установки используйте Composer:

composer require flightphp/core

Или скачайте zip-архив репозитория здесь. После этого у вас будет базовый файл index.php:

<?php

// если установлено через composer
require 'vendor/autoload.php';
// или если установлено вручную через zip-архив
// require 'flight/Flight.php';

Flight::route('/', function() {
  echo 'hello world!';
});

Flight::route('/json', function() {
  Flight::json([
    'hello' => 'world'
  ]);
});

Flight::start();

Вот и всё! У вас есть базовое приложение на Flight. Теперь можно запустить файл командой php -S localhost:8000 и открыть http://localhost:8000 в браузере, чтобы увидеть результат.

Короткие примеры с Flight:: удобны для обучения и микроприложений. Для полноценной структуры проекта, удобной как человеку, так и AI-инструментам, используйте скелет ниже.

Скелет/Boilerplate-приложение

Есть официальный стартовый шаблон, который поможет начать любой новый проект на Flight. Он сразу задаёт структуру, конфигурацию, Composer-скрипты и инструкции для AI.

Ознакомьтесь с flightphp/skeleton, чтобы получить готовый проект, или посетите страницу примеров для вдохновения. Хотите узнать о AI-подходе? Изучите AI и опыт разработчика.

Что вы получите (основное):

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

Всё очень просто!

# Создаём новый проект
composer create-project flightphp/skeleton my-project/
# Переходим в каталог нового проекта
cd my-project/
# Запускаем локальный сервер разработки
composer start

Скрипт создаст структуру проекта, скопирует config_sample.phpconfig.php.env.example.env, если присутствует), и вы готовы к работе. Опциональные тестовые данные:

php runway migrate
# затем откройте /posts и /api/posts

Высокая производительность

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

Смотрите сравнение ниже с другими популярными PHP-фреймворками.

Фреймворк Обычные запросы/сек JSON запросы/сек
Flight 190,421 182,491
Yii 145,749 131,434
Fat-Free 139,238 133,952
Slim 89,588 87,348
Phalcon 95,911 87,675
Symfony 65,053 63,237
Lumen 40,572 39,700
Laravel 26,657 26,901
CodeIgniter 20,628 19,901

Flight и AI

Хотите узнать, как Flight работает с LLM? Узнайте, как AGENTS.md, команды Runway ai:* и структура скелета помогают ассистентам оставаться «в рельсах».

Стабильность и обратная совместимость

Мы ценим ваше время. Мы все видели фреймворки, которые полностью переосмысливают себя каждые пару лет, ломая код разработчиков и требуя дорогостоящих миграций. Flight другой. Flight v3 создан как эволюция v2, что означает — знакомый и любимый API не исчез. На самом деле большинство проектов на v2 будут работать без изменений в v3.

Мы стремимся сохранять Flight стабильным, чтобы вы могли сосредоточиться на создании приложения, а не на исправлении фреймворка. Скелет может быть более «мнениеориентированным» для новых проектов; основные API остаются привычными для всех остальных.

Сообщество

Мы на Matrix Chat

Matrix

И Discord

Участие в разработке

Есть два способа внести свой вклад в Flight:

  1. Участвовать в разработке ядра фреймворка, посетив репозиторий ядра.
  2. Помочь улучшить документацию! Этот сайт документации размещён на Github. Если вы заметили ошибку или хотите что-то улучшить, отправляйте pull request. Мы ценим обновления и новые идеи — особенно связанные с AI и новыми технологиями!

Требования

Flight требует PHP 7.4 или выше.

Примечание: PHP 7.4 поддерживается, поскольку на момент написания (2024) PHP 7.4 — это версия по умолчанию для некоторых LTS-дистрибутивов 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) {

    // Это гарантирует, что кэш используется только в режиме production
    // 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 seconds

// or
$data = $cache->get('simple-cache-test');
if(empty($data)) {
    $data = date("H:i:s");
    $cache->set('simple-cache-test', $data, 10); // 10 seconds
}

Сохранение значения кэша

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

Flight::cache()->set('simple-cache-test', 'my cached data', 10); // 10 seconds

Удаление значения кэша

Вы используете метод 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
// or
$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 для просмотра кода.

Awesome-plugins/permissions

FlightPHP/Permissions

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

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

Установка

Выполните composer require flightphp/permissions и вы готовы к работе!

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

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

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

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

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

// some code 

// then you probably have something that tells you who the current role is of the person
// likely you have something where you pull the current role
// from a session variable which defines this
// after someone logs in, otherwise they will have a 'guest' or 'public' role.
$current_role = 'admin';

// setup permissions
$permission = new \flight\Permission($current_role);
$permission->defineRule('loggedIn', function($current_role) {
    return $current_role !== 'guest';
});

// You'll probably want to persist this object in Flight somewhere
Flight::set('permission', $permission);

Затем где-нибудь в контроллере у вас может быть что-то подобное.

<?php

// some controller
class SomeController {
    public function someAction() {
        $permission = Flight::get('permission');
        if ($permission->has('loggedIn')) {
            // do something
        } else {
            // do something else
        }
    }
}

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

$current_role = 'admin';

// setup permissions
$permission = new \flight\Permission($current_role);
$permission->defineRule('post', function($current_role) {
    if($current_role === 'admin') {
        $permissions = ['create', 'read', 'update', 'delete'];
    } else if($current_role === 'editor') {
        $permissions = ['create', 'read', 'update'];
    } else if($current_role === 'author') {
        $permissions = ['create', 'read'];
    } else if($current_role === 'contributor') {
        $permissions = ['create'];
    } else {
        $permissions = [];
    }
    return $permissions;
});
Flight::set('permission', $permission);

Затем где-нибудь в контроллере...

class PostController {
    public function create() {
        $permission = Flight::get('permission');
        if ($permission->can('post.create')) {
            // do something
        } else {
            // do something else
        }
    }
}

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

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

Замыкания

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

// in your controller file
public function createOrder() {
    $MyDependency = Flight::myDependency();
    $permission = Flight::get('permission');
    if ($permission->can('order.create', $MyDependency)) {
        // do something
    } else {
        // do something else
    }
}

Классы

namespace MyApp;

class Permissions {

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

Сокращение для установки прав доступа с помощью классов

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

<?php

// bootstrap code
$Permissions = new \flight\Permission($current_role);
$Permissions->defineRule('order', 'MyApp\Permissions->order');

// myapp/Permissions.php
namespace MyApp;

class Permissions {

    public function order(string $current_role, int $user_id) {
        // Assuming you set this up beforehand
        /** @var \flight\database\SimplePdo $db */
        $db = Flight::db();
        $allowed_permissions = [ 'read' ]; // everyone can view an order
        if($current_role === 'manager') {
            $allowed_permissions[] = 'create'; // managers can create orders
        }
        $some_special_toggle_from_db = $db->fetchField('SELECT some_special_toggle FROM settings WHERE id = ?', [ $user_id ]);
        if($some_special_toggle_from_db) {
            $allowed_permissions[] = 'update'; // if the user has a special toggle, they can update orders
        }
        if($current_role === 'admin') {
            $allowed_permissions[] = 'delete'; // admins can delete orders
        }
        return $allowed_permissions;
    }
}

Крутая часть заключается в том, что есть также сокращение, которое вы можете использовать (которое также можно кэшировать!!!), где вы просто сообщаете классу прав доступа сопоставить все методы в классе с правами доступа. Таким образом, если у вас есть метод с именем order() и метод с именем company(), они будут автоматически сопоставлены, поэтому вы можете просто запустить $Permissions->has('order.read') или $Permissions->has('company.read'), и это будет работать. Определить это очень сложно, так что оставайтесь со мной здесь. Вам просто нужно сделать это:

Создайте класс прав доступа, который вы хотите сгруппировать вместе.

class MyPermissions {
    public function order(string $current_role, int $order_id = 0): array {
        // code to determine permissions
        return $permissions_array;
    }

    public function company(string $current_role, int $company_id): array {
        // code to determine permissions
        return $permissions_array;
    }
}

Затем сделайте права доступа обнаруживаемыми с помощью этой библиотеки.

$Permissions = new \flight\Permission($current_role);
$Permissions->defineRulesFromClassMethods(MyApp\Permissions::class);
Flight::set('permissions', $Permissions);

Наконец, вызовите разрешение в вашей кодовой базе, чтобы проверить, разрешено ли пользователю выполнять данное разрешение.

class SomeController {
    public function createOrder() {
        if(Flight::get('permissions')->can('order.create') === false) {
            die('You can\'t create an order. Sorry!');
        }
    }
}

Кэширование

Чтобы включить кэширование, см. простую библиотеку wruczak/phpfilecache. Пример включения этого приведен ниже.


// this $app can be part of your code, or
// you can just pass null and it will
// pull from Flight::app() in the constructor
$app = Flight::app();

// For now it accepts this as a file cache. Others can easily
// be added in the future. 
$Cache = new Wruczek\PhpFileCache\PhpFileCache;

$Permissions = new \flight\Permission($current_role, $app, $Cache);
$Permissions->defineRulesFromClassMethods(MyApp\Permissions::class, 3600); // 3600 is how many seconds to cache this for. Leave this off to not use caching

И вперед!

Awesome-plugins/simple_job_queue

Простой очередь задач

Простой очередь задач - это библиотека, которая может использоваться для обработки задач асинхронно. Она может быть использована с beanstalkd, MySQL/MariaDB, SQLite и PostgreSQL.

Установка

composer require n0nag0n/simple-job-queue

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

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

Добавление в Flight

Добавить это в Flight просто, и это делается с помощью метода register(). Ниже приведен пример того, как добавить это в Flight.

<?php
require 'vendor/autoload.php';

// Замените ['mysql'] на ['beanstalkd'], если хотите использовать beanstalkd
Flight::register('queue', n0nag0n\Job_Queue::class, ['mysql'], function($Job_Queue) {
    // если у вас уже есть соединение PDO на Flight::db();
    $Job_Queue->addQueueConnection(Flight::db());

    // или если вы используете beanstalkd/Pheanstalk
    $pheanstalk = Pheanstalk\Pheanstalk::create('127.0.0.1');
    $Job_Queue->addQueueConnection($pheanstalk);
});

Добавление новой задачи

Когда вы добавляете задачу, вам нужно указать конвейер (очередь). Это сравнимо с каналом в RabbitMQ или трубой в beanstalkd.

<?php
Flight::queue()->selectPipeline('send_important_emails');
Flight::queue()->addJob(json_encode([ 'something' => 'that', 'ends' => 'up', 'a' => 'string' ]));

Запуск рабочего процесса

Вот пример файла того, как запустить рабочего процесса.

<?php

require 'vendor/autoload.php';

$Job_Queue = new n0nag0n\Job_Queue('mysql');
// Соединение PDO
$PDO = new PDO('mysql:dbname=testdb;host=127.0.0.1', 'user', 'pass');
$Job_Queue->addQueueConnection($PDO);

// или если вы используете beanstalkd/Pheanstalk
$pheanstalk = Pheanstalk\Pheanstalk::create('127.0.0.1');
$Job_Queue->addQueueConnection($pheanstalk);

$Job_Queue->watchPipeline('send_important_emails');
while(true) {
    $job = $Job_Queue->getNextJobAndReserve();

    // настройте это так, как вам будет спокойнее (только для очередей базы данных, beanstalkd не нуждается в этом условии)
    if(empty($job)) {
        usleep(500000);
        continue;
    }

    echo "Обработка {$job['id']}\n";
    $payload = json_decode($job['payload'], true);

    try {
        $result = doSomethingThatDoesSomething($payload);

        if($result === true) {
            $Job_Queue->deleteJob($job);
        } else {
            // это убирает его из очереди готовых и помещает в другую очередь, которую можно будет забрать и «ударить» позже.
            $Job_Queue->buryJob($job);
        }
    } catch(Exception $e) {
        $Job_Queue->buryJob($job);
    }
}

Обработка длительных процессов с помощью Supervisord

Supervisord - это система управления процессами, которая обеспечивает постоянную работу ваших процессов рабочих. Вот более полное руководство по настройке его с вашим рабочим процессом Простой очереди задач:

Установка Supervisord

# На Ubuntu/Debian
sudo apt-get install supervisor

# На CentOS/RHEL
sudo yum install supervisor

# На macOS с Homebrew
brew install supervisor

Создание скрипта рабочего процесса

Сначала сохраните ваш код рабочего процесса в отдельный файл PHP:

<?php

require 'vendor/autoload.php';

$Job_Queue = new n0nag0n\Job_Queue('mysql');
// Соединение PDO
$PDO = new PDO('mysql:dbname=your_database;host=127.0.0.1', 'username', 'password');
$Job_Queue->addQueueConnection($PDO);

// Установите конвейер для наблюдения
$Job_Queue->watchPipeline('send_important_emails');

// Запись начала работы рабочего процесса
echo date('Y-m-d H:i:s') . " - Рабочий процесс запущен\n";

while(true) {
    $job = $Job_Queue->getNextJobAndReserve();

    if(empty($job)) {
        usleep(500000); // Спите 0,5 секунды
        continue;
    }

    echo date('Y-m-d H:i:s') . " - Обработка задачи {$job['id']}\n";
    $payload = json_decode($job['payload'], true);

    try {
        $result = doSomethingThatDoesSomething($payload);

        if($result === true) {
            $Job_Queue->deleteJob($job);
            echo date('Y-m-d H:i:s') . " - Задача {$job['id']} успешно завершена\n";
        } else {
            $Job_Queue->buryJob($job);
            echo date('Y-m-d H:i:s') . " - Задача {$job['id']} не удалась, похоронена\n";
        }
    } catch(Exception $e) {
        $Job_Queue->buryJob($job);
        echo date('Y-m-d H:i:s') . " - Исключение при обработке задачи {$job['id']}: {$e->getMessage()}\n";
    }
}

Настройка Supervisord

Создайте файл конфигурации для вашего рабочего процесса:

[program:email_worker]
command=php /path/to/worker.php
directory=/path/to/project
autostart=true
autorestart=true
startretries=3
stderr_logfile=/var/log/simple_job_queue_err.log
stdout_logfile=/var/log/simple_job_queue.log
user=www-data
numprocs=2
process_name=%(program_name)s_%(process_num)02d

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

Управление рабочими процессами с помощью Supervisorctl

После создания или изменения конфигурации:

# Перезагрузить конфигурацию супервайзера
sudo supervisorctl reread
sudo supervisorctl update

# Управление конкретными процессами рабочих
sudo supervisorctl start email_worker:*
sudo supervisorctl stop email_worker:*
sudo supervisorctl restart email_worker:*
sudo supervisorctl status email_worker:*

Запуск нескольких конвейеров

Для нескольких конвейеров создайте отдельные файлы рабочих процессов и конфигурации:

[program:email_worker]
command=php /path/to/email_worker.php
# ... другие настройки ...

[program:notification_worker]
command=php /path/to/notification_worker.php
# ... другие настройки ...

Мониторинг и журналы

Проверьте журналы для мониторинга активности рабочих процессов:

# Просмотр журналов
sudo tail -f /var/log/simple_job_queue.log

# Проверка состояния
sudo supervisorctl status

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

Awesome-plugins/jwt

Firebase JWT - Аутентификация с использованием JSON Web Token

JWT (JSON Web Tokens) — это компактный и безопасный для URL способ представления утверждений между вашим приложением и клиентом. Они идеально подходят для аутентификации stateless API — без необходимости хранения сессий на сервере! Это руководство показывает, как интегрировать Firebase JWT с Flight для безопасной аутентификации на основе токенов.

Посетите репозиторий на Github для полной документации и деталей.

Что такое JWT?

JSON Web Token — это строка, содержащая три части:

  1. Заголовок: Метаданные о токене (алгоритм, тип)
  2. Полезная нагрузка: Ваши данные (ID пользователя, роли, срок истечения и т.д.)
  3. Подпись: Криптографическая подпись для проверки подлинности

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

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

Установка

Установите через Composer:

composer require firebase/php-jwt

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

Вот быстрый пример создания и проверки JWT:

use Firebase\JWT\JWT;
use Firebase\JWT\Key;

// Ваш секретный ключ (ХРАНИТЕ ЕГО В БЕЗОПАСНОМ МЕСТЕ!)
$secretKey = 'your-256-bit-secret-key-here-keep-it-safe';

// Создание токена
$payload = [
    'user_id' => 123,
    'username' => 'johndoe',
    'role' => 'admin',
    'iat' => time(),              // Выдан в
    'exp' => time() + 3600        // Истекает через 1 час
];

$jwt = JWT::encode($payload, $secretKey, 'HS256');
echo "Token: " . $jwt;

// Проверка и декодирование токена
try {
    $decoded = JWT::decode($jwt, new Key($secretKey, 'HS256'));
    echo "User ID: " . $decoded->user_id;
} catch (Exception $e) {
    echo "Invalid token: " . $e->getMessage();
}

Middleware JWT для Flight (Рекомендуемый подход)

Наиболее распространенный и полезный способ использования JWT с Flight — это как middleware для защиты маршрутов API. Вот полный, готовый к производству пример:

Шаг 1: Создание класса JWT Middleware

// app/middleware/JwtMiddleware.php
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Firebase\JWT\ExpiredException;
use Firebase\JWT\SignatureInvalidException;
use flight\Engine;

class JwtMiddleware {

    protected Engine $app;
    protected string $secretKey;

    public function __construct(Engine $app) {
        $this->app = $app;
        // Храните ваш секретный ключ в app/config/config.php, НЕ хардкодьте!
        $this->secretKey = $app->get('config')['jwt_secret'];
    }

    public function before(array $params) {
        $authHeader = $this->app->request()->getHeader('Authorization');

        // Проверка наличия заголовка Authorization
        if (empty($authHeader)) {
            $this->app->jsonHalt(['error' => 'No authorization token provided'], 401);
        }

        // Извлечение токена из формата "Bearer <token>"
        if (!preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches)) {
            $this->app->jsonHalt(['error' => 'Invalid authorization format. Use: Bearer <token>'], 401);
        }

        $jwt = $matches[1];

        try {
            // Декодирование и проверка токена
            $decoded = JWT::decode($jwt, new Key($this->secretKey, 'HS256'));

            // Сохранение данных пользователя в запросе для использования в обработчиках маршрутов
            $this->app->request()->data->user = $decoded;

        } catch (ExpiredException $e) {
            $this->app->jsonHalt(['error' => 'Token has expired'], 401);
        } catch (SignatureInvalidException $e) {
            $this->app->jsonHalt(['error' => 'Invalid token signature'], 401);
        } catch (Exception $e) {
            $this->app->jsonHalt(['error' => 'Invalid token: ' . $e->getMessage()], 401);
        }
    }
}

Шаг 2: Регистрация JWT секрета в вашей конфигурации

// app/config/config.php
return [
    'jwt_secret' => getenv('JWT_SECRET') ?: 'your-fallback-secret-for-development'
];

// app/config/bootstrap.php или index.php
// убедитесь, что добавили эту строку, если хотите предоставить конфигурацию приложению
$app->set('config', $config);

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

Шаг 3: Защита ваших маршрутов с помощью Middleware

// Защита одного маршрута
Flight::route('GET /api/user/profile', function() {
    $user = Flight::request()->data->user; // Установлено middleware
    Flight::json([
        'user_id' => $user->user_id,
        'username' => $user->username,
        'role' => $user->role
    ]);
})->addMiddleware(JwtMiddleware::class);

// Защита всей группы маршрутов (более распространено!)
Flight::group('/api', function() {
    Flight::route('GET /users', function() { /* ... */ });
    Flight::route('GET /posts', function() { /* ... */ });
    Flight::route('POST /posts', function() { /* ... */ });
    Flight::route('DELETE /posts/@id', function($id) { /* ... */ });
}, [ JwtMiddleware::class ]); // Все маршруты в этой группе защищены!

Для более подробной информации о middleware см. документацию по middleware.

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

1. Эндпоинт входа (Генерация токена)

Создайте маршрут, который генерирует JWT после успешной аутентификации:

Flight::route('POST /api/login', function() {
    $data = Flight::request()->data;
    $username = $data->username ?? '';
    $password = $data->password ?? '';

    // Валидация учетных данных (пример — используйте свою логику!)
    $user = validateUserCredentials($username, $password);

    if (!$user) {
        Flight::jsonHalt(['error' => 'Invalid credentials'], 401);
    }

    // Генерация JWT
    $secretKey = Flight::get('config')['jwt_secret'];
    $payload = [
        'user_id' => $user->id,
        'username' => $user->username,
        'role' => $user->role,
        'iat' => time(),
        'exp' => time() + (60 * 60) // Истечение через 1 час
    ];

    $jwt = JWT::encode($payload, $secretKey, 'HS256');

    Flight::json([
        'success' => true,
        'token' => $jwt,
        'expires_in' => 3600
    ]);
});

function validateUserCredentials($username, $password) {
    // Ваш поиск в базе данных и проверка пароля здесь
    // Пример:
    $db = Flight::db();
    $user = $db->fetchRow("SELECT * FROM users WHERE username = ?", [$username]);

    if ($user && password_verify($password, $user['password_hash'])) {
        return (object) [
            'id' => $user['id'],
            'username' => $user['username'],
            'role' => $user['role']
        ];
    }
    return null;
}

2. Поток обновления токена

Реализуйте систему обновления токенов для долгоживущих сессий:

Flight::route('POST /api/login', function() {
    // ... валидация учетных данных ...

    $secretKey = Flight::get('config')['jwt_secret'];
    $refreshSecret = Flight::get('config')['jwt_refresh_secret'];

    // Короткоживущий токен доступа (15 минут)
    $accessToken = JWT::encode([
        'user_id' => $user->id,
        'type' => 'access',
        'iat' => time(),
        'exp' => time() + (15 * 60)
    ], $secretKey, 'HS256');

    // Долгоживущий токен обновления (7 дней)
    $refreshToken = JWT::encode([
        'user_id' => $user->id,
        'type' => 'refresh',
        'iat' => time(),
        'exp' => time() + (7 * 24 * 60 * 60)
    ], $refreshSecret, 'HS256');

    Flight::json([
        'access_token' => $accessToken,
        'refresh_token' => $refreshToken,
        'expires_in' => 900
    ]);
});

Flight::route('POST /api/refresh', function() {
    $refreshToken = Flight::request()->data->refresh_token ?? '';
    $refreshSecret = Flight::get('config')['jwt_refresh_secret'];

    try {
        $decoded = JWT::decode($refreshToken, new Key($refreshSecret, 'HS256'));

        // Проверка, что это токен обновления
        if ($decoded->type !== 'refresh') {
            Flight::jsonHalt(['error' => 'Invalid token type'], 401);
        }

        // Генерация нового токена доступа
        $secretKey = Flight::get('config')['jwt_secret'];
        $accessToken = JWT::encode([
            'user_id' => $decoded->user_id,
            'type' => 'access',
            'iat' => time(),
            'exp' => time() + (15 * 60)
        ], $secretKey, 'HS256');

        Flight::json([
            'access_token' => $accessToken,
            'expires_in' => 900
        ]);

    } catch (Exception $e) {
        Flight::jsonHalt(['error' => 'Invalid refresh token'], 401);
    }
});

3. Контроль доступа на основе ролей

Расширьте ваше middleware для проверки ролей пользователя:

class JwtRoleMiddleware {

    protected Engine $app;
    protected array $allowedRoles;

    public function __construct(Engine $app, array $allowedRoles = []) {
        $this->app = $app;
        $this->allowedRoles = $allowedRoles;
    }

    public function before(array $params) {
        // Предполагаем, что JwtMiddleware уже выполнился и установил данные пользователя
        $user = $this->app->request()->data->user ?? null;

        if (!$user) {
            $this->app->jsonHalt(['error' => 'Authentication required'], 401);
        }

        // Проверка наличия требуемой роли у пользователя
        if (!empty($this->allowedRoles) && !in_array($user->role, $this->allowedRoles)) {
            $this->app->jsonHalt(['error' => 'Insufficient permissions'], 403);
        }
    }
}

// Использование: Маршрут только для админов
Flight::route('DELETE /api/users/@id', function($id) {
    // Логика удаления пользователя
})->addMiddleware([
    JwtMiddleware::class,
    new JwtRoleMiddleware(Flight::app(), ['admin'])
]);

4. Публичный API с ограничением скорости по пользователю

Используйте JWT для отслеживания и ограничения скорости пользователей без сессий:

class RateLimitMiddleware {

    public function before(array $params) {
        $user = Flight::request()->data->user ?? null;
        $userId = $user ? $user->user_id : Flight::request()->ip;

        $cacheKey = "rate_limit:$userId";
        // Убедитесь, что вы настроили сервис кэша в app/config/services.php
        $requests = Flight::cache()->get($cacheKey, 0);

        if ($requests >= 100) { // 100 запросов в час
            Flight::jsonHalt(['error' => 'Rate limit exceeded'], 429);
        }

        Flight::cache()->set($cacheKey, $requests + 1, 3600);
    }
}

Лучшие практики безопасности

1. Используйте сильные секретные ключи

// Генерация безопасного секретного ключа (запустите один раз, сохраните в .env файл)
$secretKey = base64_encode(random_bytes(32));
echo $secretKey; // Сохраните это в вашем .env файле!

2. Храните секреты в переменных окружения

// Никогда не коммитьте секреты в контроль версий!
// Используйте .env файл и библиотеку вроде vlucas/phpdotenv

// .env файл:
// JWT_SECRET=your-base64-encoded-secret-here
// JWT_REFRESH_SECRET=another-base64-encoded-secret-here

// Вы также можете использовать файл app/config/config.php для хранения секретов
// просто убедитесь, что файл конфигурации не коммитится в контроль версий
// return [
//     'jwt_secret' => 'your-base64-encoded-secret-here',
//     'jwt_refresh_secret' => 'another-base64-encoded-secret-here',
// ];

// В вашем приложении:
$secretKey = getenv('JWT_SECRET');

3. Устанавливайте подходящие сроки истечения

// Хорошая практика: короткоживущие токены доступа
'exp' => time() + (15 * 60)  // 15 минут

// Для токенов обновления: более длинное истечение
'exp' => time() + (7 * 24 * 60 * 60)  // 7 дней

4. Используйте HTTPS в производстве

JWT всегда должны передаваться по HTTPS. Никогда не отправляйте токены по обычному HTTP в производстве!

5. Валидируйте утверждения токена

Всегда валидируйте утверждения, которые вас интересуют:

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

// Проверка истечения обрабатывается библиотекой автоматически
// Но вы можете добавить кастомные валидации:
if ($decoded->iat > time()) {
    throw new Exception('Token used before it was issued');
}

if (isset($decoded->nbf) && $decoded->nbf > time()) {
    throw new Exception('Token not yet valid');
}

6. Рассмотрите черный список токенов для выхода

Для дополнительной безопасности поддерживайте черный список недействительных токенов:

Flight::route('POST /api/logout', function() {
    $authHeader = Flight::request()->getHeader('Authorization');
    preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches);
    $jwt = $matches[1];

    // Извлечение срока истечения токена
    $decoded = Flight::request()->data->user;
    $ttl = $decoded->exp - time();

    // Сохранение в кэше/redis до истечения
    Flight::cache()->set("blacklist:$jwt", true, $ttl);

    Flight::json(['message' => 'Successfully logged out']);
});

// Добавьте в ваше JwtMiddleware:
public function before(array $params) {
    // ... извлечение JWT ...

    // Проверка черного списка
    if (Flight::cache()->get("blacklist:$jwt")) {
        $this->app->jsonHalt(['error' => 'Token has been revoked'], 401);
    }

    // ... проверка токена ...
}

Алгоритмы и типы ключей

Firebase JWT поддерживает несколько алгоритмов:

Симметричные алгоритмы (HMAC)

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

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

// Генерация ключей: openssl genrsa -out private.key 2048
// openssl rsa -in private.key -pubout -out public.key

$privateKey = file_get_contents('/path/to/private.key');
$publicKey = file_get_contents('/path/to/public.key');

// Кодирование с приватным ключом
$jwt = JWT::encode($payload, $privateKey, 'RS256');

// Декодирование с публичным ключом
$decoded = JWT::decode($jwt, new Key($publicKey, 'RS256'));

Когда использовать RSA: Используйте RSA, когда нужно распространить публичный ключ для проверки (например, микросервисы, интеграции с третьими сторонами). Для одного приложения HS256 проще и достаточно.

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

Ошибка "Expired token"

Утверждение exp вашего токена в прошлом. Выдайте новый токен или реализуйте обновление токена.

"Signature verification failed"

use Firebase\JWT\JWT;

JWT::$leeway = 60; // Разрешить 60 секунд разницы во времени
$decoded = JWT::decode($jwt, new Key($secretKey, 'HS256'));

Токен не отправляется в запросах

Убедитесь, что ваш клиент отправляет заголовок Authorization:

// Пример на JavaScript
fetch('/api/users', {
    headers: {
        'Authorization': 'Bearer ' + token
    }
});

Методы

Библиотека Firebase JWT предоставляет эти основные методы:

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

См. также

Лицензия

Библиотека Firebase JWT лицензирована по BSD 3-Clause License. См. репозиторий на Github для деталей.

Awesome-plugins/n0nag0n_wordpress

Интеграция с WordPress: n0nag0n/wordpress-integration-for-flight-framework

Хотите использовать Flight PHP внутри вашего сайта WordPress? Этот плагин делает это очень простым! С n0nag0n/wordpress-integration-for-flight-framework вы можете запустить полноценное приложение Flight прямо рядом с вашей установкой WordPress — идеально для создания пользовательских API, микросервисов или даже полноценных приложений, не выходя из комфорта WordPress.


Что он делает?

Установка

  1. Загрузите папку flight-integration в ваш каталог /wp-content/plugins/.
  2. Активируйте плагин в админ-панели WordPress (меню Плагины).
  3. Перейдите в Настройки > Flight Framework, чтобы настроить плагин.
  4. Укажите путь к установке Flight (или используйте Composer для установки Flight).
  5. Настройте путь к папке вашего приложения и создайте структуру папок (плагин может помочь с этим!).
  6. Начните создавать ваше приложение Flight!

Примеры использования

Пример базового маршрута

В вашем файле app/config/routes.php:

Flight::route('GET /api/hello', function() {
    Flight::json(['message' => 'Hello World!']);
});

Пример контроллера

Создайте контроллер в app/controllers/ApiController.php:

namespace app\controllers;

use Flight;

class ApiController {
    public function getUsers() {
        // Вы можете использовать функции WordPress внутри Flight!
        $users = get_users();
        $result = [];
        foreach($users as $user) {
            $result[] = [
                'id' => $user->ID,
                'name' => $user->display_name,
                'email' => $user->user_email
            ];
        }
        Flight::json($result);
    }
}

Затем, в вашем routes.php:

Flight::route('GET /api/users', [app\controllers\ApiController::class, 'getUsers']);

ЧАВО

В: Мне нужно знать Flight, чтобы использовать этот плагин?
О: Да, это для разработчиков, которые хотят использовать Flight в WordPress. Рекомендуется базовое знание маршрутизации и обработки запросов Flight.

В: Это замедлит мой сайт WordPress?
О: Нет! Плагин обрабатывает только запросы, которые соответствуют вашим маршрутам Flight. Все остальные запросы идут в WordPress как обычно.

В: Могу ли я использовать функции WordPress в своем приложении Flight?
О: Абсолютно! У вас есть полный доступ ко всем функциям, хукам и глобальным переменным WordPress из ваших маршрутов и контроллеров Flight.

В: Как создать пользовательские маршруты?
О: Определите свои маршруты в файле config/routes.php в папке вашего приложения. Посмотрите образец файла, созданный генератором структуры папок, для примеров.

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

1.0.0
Первоначальный релиз.


Для получения дополнительной информации посетите GitHub repo.

Awesome-plugins/ghost_session

Ghostff/Session

PHP Менеджер сессий (неблокирующий, 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/mcp

Сервер FlightPHP MCP

Сервер FlightPHP MCP предоставляет любому совместимому с MCP ИИ-ассистенту для кодирования мгновенный структурированный доступ ко всей документации FlightPHP — маршрутизации, middleware, плагинам, руководствам и многое другое. Вместо того чтобы ваш ИИ галлюцинировал детали API или угадывал сигнатуры методов, он запрашивает реальную документацию по требованию. Без API-ключей, без установки для хостинговой версии.

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

Быстрый старт

Сервер публично хостится и готов к использованию:

https://mcp.flightphp.com/mcp

Просто добавьте этот URL в ваше расширение для ИИ-кодирования. Без регистрации, без учетных данных. См. раздел Конфигурация IDE ниже для готовых конфигураций копирования-вставки для самых популярных инструментов.

Что он делает

После подключения ваш ИИ-ассистент может:

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

Конфигурация IDE / Расширения ИИ

Сервер использует транспорт Streamable HTTP. Выберите свое расширение ниже и вставьте конфигурацию.

Claude Code (CLI)

Выполните следующую команду, чтобы добавить его в ваш проект:

claude mcp add --transport http flightphp-docs https://mcp.flightphp.com/mcp

Или добавьте вручную в файл .mcp.json вашего проекта:

{
  "mcpServers": {
    "flightphp-docs": {
      "type": "http",
      "url": "https://mcp.flightphp.com/mcp"
    }
  }
}

GitHub Copilot (VS Code)

Добавьте в .vscode/mcp.json в вашем рабочем пространстве:

{
  "servers": {
    "flightphp-docs": {
      "type": "http",
      "url": "https://mcp.flightphp.com/mcp"
    }
  }
}

Kilo Code (VS Code)

Добавьте в settings.json вашего VS Code:

{
  "kilocode.mcpServers": {
    "flightphp-docs": {
      "url": "https://mcp.flightphp.com/mcp",
      "transport": "streamable-http"
    }
  }
}

Continue.dev (VS Code / JetBrains)

Добавьте в ~/.continue/config.json:

{
  "mcpServers": [
    {
      "name": "flightphp-docs",
      "transport": {
        "type": "http",
        "url": "https://mcp.flightphp.com/mcp"
      }
    }
  ]
}

Доступные инструменты

Сервер MCP предоставляет следующие инструменты вашему ИИ-ассистенту:

Инструмент Описание
list_docs_pages Перечисляет все доступные основные темы документации со слагами и описаниями
get_docs_page Получает страницу основных документов по слагам тем (например, routing, middleware, security)
list_guide_pages Перечисляет все доступные пошаговые руководства
get_guide_page Получает полное руководство по слагам (например, blog, unit-testing)
list_plugin_pages Перечисляет все доступные страницы плагинов и расширений
get_plugin_docs Получает полную документацию плагина по слагам (например, active-record, session, jwt)
search_docs Ищет по всем документам, руководствам и плагинам по ключевому слову или теме
fetch_url Получает любую страницу напрямую по полному URL docs.flightphp.com

Самохостинг

Предпочитаете запустить свою собственную инстанцию? Вам понадобится PHP >= 8.1 и Composer.

git clone https://github.com/flightphp/mcp.git
cd mcp
composer install
php server.php

Сервер запускается по умолчанию на http://0.0.0.0:8890/mcp. Обновите конфигурацию IDE, чтобы указать на ваш локальный адрес:

{
  "mcpServers": {
    "flightphp-docs": {
      "type": "http",
      "url": "http://localhost:8890/mcp"
    }
  }
}

Awesome-plugins/async

Async

Async — это небольшой пакет для фреймворка Flight, который позволяет запускать приложения Flight внутри асинхронных серверов и рантаймов, таких как Swoole, AdapterMan, ReactPHP, Amp, RoadRunner, Workerman и т.д. Из коробки он включает адаптеры для Swoole и AdapterMan.

Цель: разрабатывать и отлаживать с PHP-FPM (или встроенным сервером) и переключаться на Swoole (или другой асинхронный драйвер) для продакшена с минимальными изменениями.

Требования

Установка

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

composer require flightphp/async

Если вы планируете запускать с Swoole, установите расширение:

# используя pecl
pecl install swoole
# или openswoole
pecl install openswoole

# или с помощью менеджера пакетов (пример для Debian/Ubuntu)
sudo apt-get install php-swoole

Быстрый пример Swoole

Ниже приведена минимальная настройка, которая показывает, как поддерживать как PHP-FPM (или встроенный сервер), так и Swoole, используя один и тот же код.

Файлы, которые вам понадобятся в проекте:

index.php

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

// index.php
<?php

define('NOT_SWOOLE', true);

include 'swoole_server.php';

swoole_server.php

Этот файл инициализирует ваше приложение Flight и запустит драйвер Swoole, когда NOT_SWOOLE не определен.

// swoole_server.php
<?php

require_once __DIR__ . '/vendor/autoload.php';

$app = Flight::app();

$app->route('/', function() use ($app) {
    $app->json(['hello' => 'world']);
});

if (!defined('NOT_SWOOLE')) {
    // Require the SwooleServerDriver class when running in Swoole mode.
    require_once __DIR__ . '/SwooleServerDriver.php';

    Swoole\Runtime::enableCoroutine();
    $Swoole_Server = new SwooleServerDriver('127.0.0.1', 9501, $app);
    $Swoole_Server->start();
} else {
    $app->start();
}

SwooleServerDriver.php

Краткий драйвер, показывающий, как связывать запросы Swoole с Flight с использованием AsyncBridge и адаптеров Swoole.

// SwooleServerDriver.php
<?php

use flight\adapter\SwooleAsyncRequest;
use flight\adapter\SwooleAsyncResponse;
use flight\AsyncBridge;
use flight\Engine;
use Swoole\HTTP\Server as SwooleServer;
use Swoole\HTTP\Request as SwooleRequest;
use Swoole\HTTP\Response as SwooleResponse;

class SwooleServerDriver {
    protected $Swoole;
    protected $app;

    public function __construct(string $host, int $port, Engine $app) {
        $this->Swoole = new SwooleServer($host, $port);
        $this->app = $app;

        $this->setDefault();
        $this->bindWorkerEvents();
        $this->bindHttpEvent();
    }

    protected function setDefault() {
        $this->Swoole->set([
            'daemonize'             => false,
            'dispatch_mode'         => 1,
            'max_request'           => 8000,
            'open_tcp_nodelay'      => true,
            'reload_async'          => true,
            'max_wait_time'         => 60,
            'enable_reuse_port'     => true,
            'enable_coroutine'      => true,
            'http_compression'      => false,
            'enable_static_handler' => true,
            'document_root'         => __DIR__,
            'static_handler_locations' => ['/css', '/js', '/images', '/.well-known'],
            'buffer_output_size'    => 4 * 1024 * 1024,
            'worker_num'            => 4,
        ]);

        $app = $this->app;
        $app->map('stop', function (?int $code = null) use ($app) {
            if ($code !== null) {
                $app->response()->status($code);
            }
        });
    }

    protected function bindHttpEvent() {
        $app = $this->app;
        $AsyncBridge = new AsyncBridge($app);

        $this->Swoole->on('Start', function(SwooleServer $server) {
            echo "Swoole http server is started at http://127.0.0.1:9501\n";
        });

        $this->Swoole->on('Request', function (SwooleRequest $request, SwooleResponse $response) use ($AsyncBridge) {
            $SwooleAsyncRequest = new SwooleAsyncRequest($request);
            $SwooleAsyncResponse = new SwooleAsyncResponse($response);

            $AsyncBridge->processRequest($SwooleAsyncRequest, $SwooleAsyncResponse);

            $response->end();
            gc_collect_cycles();
        });
    }

    protected function bindWorkerEvents() {
        $createPools = function() {
            // create worker-specific connection pools here
        };
        $closePools = function() {
            // close pools / cleanup here
        };
        $this->Swoole->on('WorkerStart', $createPools);
        $this->Swoole->on('WorkerStop', $closePools);
        $this->Swoole->on('WorkerError', $closePools);
    }

    public function start() {
        $this->Swoole->start();
    }
}

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

Совет: Для продакшена используйте обратный прокси (Nginx) перед Swoole для обработки TLS, статических файлов и балансировки нагрузки.

Заметки по конфигурации

Драйвер Swoole предоставляет несколько опций конфигурации:

Настройте эти параметры в соответствии с ресурсами хоста и шаблонами трафика.

Обработка ошибок

AsyncBridge преобразует ошибки Flight в правильные HTTP-ответы. Вы также можете добавить обработку ошибок на уровне маршрута:

$app->route('/*', function() use ($app) {
    try {
        // route logic
    } catch (Exception $e) {
        $app->response()->status(500);
        $app->json(['error' => $e->getMessage()]);
    }
});

AdapterMan и другие рантаймы

AdapterMan поддерживается как альтернативный адаптер рантайма. Пакет спроектирован для адаптивности — добавление или использование других адаптеров в целом следует тому же шаблону: преобразование запроса/ответа сервера в запрос/ответ Flight через AsyncBridge и адаптеры, специфичные для рантайма.

Awesome-plugins/migrations

Миграции

Миграция для вашего проекта отслеживает все изменения базы данных, связанные с вашим проектом. byjg/php-migration — это действительно полезная основная библиотека, с которой вы можете начать.

Установка

PHP библиотека

Если вы хотите использовать только PHP библиотеку в вашем проекте:

composer require "byjg/migration"

Интерфейс командной строки

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

Вы можете установить его глобально и создать символическую ссылку.

composer require "byjg/migration-cli"

Пожалуйста, посетите byjg/migration-cli, чтобы получить больше информации о Migration CLI.

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

База данных Драйвер Строка соединения
Sqlite pdo_sqlite sqlite:///path/to/file
MySql/MariaDb pdo_mysql mysql://username:password@hostname:port/database
Postgres pdo_pgsql pgsql://username:password@hostname:port/database
Sql Server pdo_dblib, pdo_sysbase Linux dblib://username:password@hostname:port/database
Sql Server pdo_sqlsrv Windows sqlsrv://username:password@hostname:port/database

Как это работает?

Миграция базы данных использует ЧИСТЫЙ SQL для управления версионностью базы данных. Чтобы это заработало, вам необходимо:

SQL скрипты

Скрипты делятся на три группы:

Директория скриптов:

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

Многоразвивающая среда

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

В этом случае вы добавляете суффикс "-dev" после номера версии.

Смотрите сценарий:

В обоих случаях разработчики создадут файл под названием 43-dev.sql. Оба разработчика будут мигрировать UP и DOWN без проблем, и ваша локальная версия будет 43.

Но разработчик 1 объединил ваши изменения и создал окончательную версию 43.sql (git mv 43-dev.sql 43.sql). Если разработчик 2 обновит свою локальную ветку, он получит файл 43.sql (от 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/comment_template

CommentTemplate

CommentTemplate — это мощный шаблонный движок для PHP с компиляцией ресурсов, наследованием шаблонов и обработкой переменных. Он предоставляет простой и гибкий способ управления шаблонами с встроенной минификацией CSS/JS и кэшированием.

Особенности

Установка

Установите с помощью Composer.

composer require knifelemon/comment-template

Базовая конфигурация

Есть некоторые базовые опции конфигурации для начала работы. Вы можете прочитать больше о них в CommentTemplate Repo.

Метод 1: Использование функции обратного вызова

<?php
require_once 'vendor/autoload.php';

use KnifeLemon\CommentTemplate\Engine;

$app = Flight::app();

$app->register('view', Engine::class, [], function (Engine $engine) use ($app) {
    // Корневая директория (где находится index.php) — корень документов вашего веб-приложения
    $engine->setPublicPath(__DIR__);

    // Директория файлов шаблонов — поддерживает как относительные, так и абсолютные пути
    $engine->setSkinPath('views');             // Относительно пути public

    // Куда будут сохраняться скомпилированные ресурсы — поддерживает как относительные, так и абсолютные пути
    $engine->setAssetPath('assets');           // Относительно пути public

    // Расширение файлов шаблонов
    $engine->setFileExtension('.php');
});

$app->map('render', function(string $template, array $data) use ($app): void {
    echo $app->view()->render($template, $data);
});

Метод 2: Использование параметров конструктора

<?php
require_once 'vendor/autoload.php';

use KnifeLemon\CommentTemplate\Engine;

$app = Flight::app();

// __construct(string $publicPath = "", string $skinPath = "", string $assetPath = "", string $fileExtension = "")
$app->register('view', Engine::class, [
    __DIR__,                // publicPath — корневая директория (где находится index.php)
    'views',                // skinPath — путь к шаблонам (поддерживает относительные/абсолютные)
    'assets',               // assetPath — путь к скомпилированным ресурсам (поддерживает относительные/абсолютные)
    '.php'                  // fileExtension — расширение файлов шаблонов
]);

$app->map('render', function(string $template, array $data) use ($app): void {
    echo $app->view()->render($template, $data);
});

Конфигурация путей

CommentTemplate предоставляет интеллектуальную обработку путей как для относительных, так и для абсолютных путей:

Публичный путь

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

// Пример: если ваш index.php находится в /var/www/html/myapp/index.php
$template->setPublicPath('/var/www/html/myapp');  // Корневая директория

// Пример для Windows: если ваш index.php находится в C:\xampp\htdocs\myapp\index.php
$template->setPublicPath('C:\\xampp\\htdocs\\myapp');

Конфигурация пути к шаблонам

Путь к шаблонам поддерживает как относительные, так и абсолютные пути:

$template = new Engine();
$template->setPublicPath('/var/www/html/myapp');  // Корневая директория (где находится index.php)

// Относительные пути — автоматически объединяются с публичным путем
$template->setSkinPath('views');           // → /var/www/html/myapp/views/
$template->setSkinPath('templates/pages'); // → /var/www/html/myapp/templates/pages/

// Абсолютные пути — используются как есть (Unix/Linux)
$template->setSkinPath('/var/www/templates');      // → /var/www/templates/
$template->setSkinPath('/full/path/to/templates'); // → /full/path/to/templates/

// Абсолютные пути для Windows
$template->setSkinPath('C:\\www\\templates');     // → C:\www\templates\
$template->setSkinPath('D:/projects/templates');  // → D:/projects/templates/

// UNC-пути (сетевые ресурсы Windows)
$template->setSkinPath('\\\\server\\share\\templates'); // → \\server\share\templates\

Конфигурация пути к ресурсам

Путь к ресурсам также поддерживает как относительные, так и абсолютные пути:

// Относительные пути — автоматически объединяются с публичным путем
$template->setAssetPath('assets');        // → /var/www/html/myapp/assets/
$template->setAssetPath('static/files');  // → /var/www/html/myapp/static/files/

// Абсолютные пути — используются как есть (Unix/Linux)
$template->setAssetPath('/var/www/cdn');           // → /var/www/cdn/
$template->setAssetPath('/full/path/to/assets');   // → /full/path/to/assets/

// Абсолютные пути для Windows
$template->setAssetPath('C:\\www\\static');       // → C:\www\static\
$template->setAssetPath('D:/projects/assets');    // → D:/projects/assets/

// UNC-пути (сетевые ресурсы Windows)
$template->setAssetPath('\\\\server\\share\\assets'); // → \\server\share\assets\

Умное определение путей:

Как это работает:

Интеграция с Tracy Debugger

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

Comment Template Tracy

Установка

composer require tracy/tracy

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

<?php
use KnifeLemon\CommentTemplate\Engine;
use Tracy\Debugger;

// Включить Tracy (должно быть вызвано перед любым выводом)
Debugger::enable(Debugger::DEVELOPMENT);
Flight::set('flight.content_length', false);

// Переопределение шаблона
$app->register('view', Engine::class, [], function (Engine $builder) use ($app) {
    $builder->setPublicPath($app->get('flight.views.topPath'));
    $builder->setAssetPath($app->get('flight.views.assetPath'));
    $builder->setSkinPath($app->get('flight.views.path'));
    $builder->setFileExtension($app->get('flight.views.extension'));
});
$app->map('render', function(string $template, array $data) use ($app): void {
    echo $app->view()->render($template, $data);
});

$app->start();

Функции панели отладки

CommentTemplate добавляет пользовательскую панель в панель отладки Tracy с четырьмя вкладками:

Что логируется

Примечание: Нулевое влияние на производительность, когда Tracy не установлен или отключен.

См. полный рабочий пример с Flight PHP.

Директивы шаблонов

Наследование макетов

Используйте макеты для создания общей структуры:

layout/global_layout.php:

<!DOCTYPE html>
<html>
<head>
    <title>{$title}</title>
</head>
<body>
    <!--@contents-->
</body>
</html>

view/page.php:

<!--@layout(layout/global_layout)-->
<h1>{$title}</h1>
<p>{$content}</p>

Управление ресурсами

CSS-файлы

<!--@css(/css/styles.css)-->          <!-- Минифицировано и кэшировано -->
<!--@cssSingle(/css/critical.css)-->  <!-- Один файл, не минифицирован -->

JavaScript-файлы

CommentTemplate поддерживает различные стратегии загрузки JavaScript:

<!--@js(/js/script.js)-->             <!-- Минифицировано, загружается внизу -->
<!--@jsAsync(/js/analytics.js)-->     <!-- Минифицировано, загружается внизу с async -->
<!--@jsDefer(/js/utils.js)-->         <!-- Минифицировано, загружается внизу с defer -->
<!--@jsTop(/js/critical.js)-->        <!-- Минифицировано, загружается в head -->
<!--@jsTopAsync(/js/tracking.js)-->   <!-- Минифицировано, загружается в head с async -->
<!--@jsTopDefer(/js/polyfill.js)-->   <!-- Минифицировано, загружается в head с defer -->
<!--@jsSingle(/js/widget.js)-->       <!-- Один файл, не минифицирован -->
<!--@jsSingleAsync(/js/ads.js)-->     <!-- Один файл, не минифицирован, async -->
<!--@jsSingleDefer(/js/social.js)-->  <!-- Один файл, не минифицирован, defer -->

Директивы ресурсов в CSS/JS-файлах

CommentTemplate также обрабатывает директивы ресурсов внутри CSS- и JavaScript-файлов во время компиляции:

Пример CSS:

/* В ваших CSS-файлах */
@font-face {
    font-family: 'CustomFont';
    src: url('<!--@asset(fonts/custom.woff2)-->') format('woff2');
}

.background-image {
    background: url('<!--@asset(images/bg.jpg)-->');
}

.inline-icon {
    background: url('<!--@base64(icons/star.svg)-->');
}

Пример JavaScript:

/* В ваших JS-файлах */
const fontUrl = '<!--@asset(fonts/custom.woff2)-->';
const imageData = '<!--@base64(images/icon.png)-->';

Кодирование Base64

<!--@base64(images/logo.png)-->       <!-- Встраивается как data URI -->

Пример:

<!-- Встраивание маленьких изображений как data URI для более быстрой загрузки -->
<img src="<!--@base64(images/logo.png)-->" alt="Logo">
<div style="background-image: url('<!--@base64(icons/star.svg)-->');">
    Маленькая иконка как фон
</div>

Копирование ресурсов

<!--@asset(images/photo.jpg)-->       <!-- Копирование одного ресурса в публичную директорию -->
<!--@assetDir(assets)-->              <!-- Копирование всей директории в публичную директорию -->

Пример:

<!-- Копирование и ссылка на статические ресурсы -->
<img src="<!--@asset(images/hero-banner.jpg)-->" alt="Hero Banner">
<a href="<!--@asset(documents/brochure.pdf)-->" download>Скачать брошюру</a>

<!-- Копирование всей директории (шрифты, иконки и т.д.) -->
<!--@assetDir(assets/fonts)-->
<!--@assetDir(assets/icons)-->

Включение шаблонов

<!--@import(components/header)-->     <!-- Включение других шаблонов -->

Пример:

<!-- Включение повторно используемых компонентов -->
<!--@import(components/header)-->

<main>
    <h1>Добро пожаловать на наш сайт</h1>
    <!--@import(components/sidebar)-->

    <div class="content">
        <p>Основное содержимое здесь...</p>
    </div>
</main>

<!--@import(components/footer)-->

Обработка переменных

Базовые переменные

<h1>{$title}</h1>
<p>{$description}</p>

Фильтры переменных

{$title|upper}                       <!-- Преобразовать в верхний регистр -->
{$content|lower}                     <!-- Преобразовать в нижний регистр -->
{$html|striptag}                     <!-- Удалить HTML-теги -->
{$text|escape}                       <!-- Экранировать HTML -->
{$multiline|nl2br}                   <!-- Преобразовать переносы строк в <br> -->
{$html|br2nl}                        <!-- Преобразовать теги <br> в переносы строк -->
{$description|trim}                  <!-- Удалить пробелы -->
{$subject|title}                     <!-- Преобразовать в заглавный регистр -->

Команды переменных

{$title|default=Default Title}       <!-- Установить значение по умолчанию -->
{$name|concat= (Admin)}              <!-- Конкатенация текста -->

Команды переменных

{$content|striptag|trim|escape}      <!-- Цепочка нескольких фильтров -->

Комментарии

Комментарии шаблонов полностью удаляются из вывода и не появляются в итоговом HTML:

{* Это однострочный комментарий шаблона *}

{* 
   Это многострочный 
   комментарий шаблона 
   который занимает несколько строк
*}

<h1>{$title}</h1>
{* Отладочный комментарий: проверка работы переменной title *}
<p>{$content}</p>

Примечание: Комментарии шаблонов {* ... *} отличаются от HTML-комментариев <!-- ... -->. Комментарии шаблонов удаляются во время обработки и никогда не доходят до браузера.

Структура примера проекта

project/
├── source/
│   ├── layouts/
│   │   └── default.php
│   ├── components/
│   │   ├── header.php
│   │   └── footer.php
│   ├── css/
│   │   ├── bootstrap.min.css
│   │   └── custom.css
│   ├── js/
│   │   ├── app.js
│   │   └── bootstrap.min.js
│   └── homepage.php
├── public/
│   └── assets/           # Сгенерированные ресурсы
│       ├── css/
│       └── js/
└── vendor/

Awesome-plugins/easy_query

EasyQuery

knifelemon/easy-query — это легковесный, fluent SQL-запросостроитель, который генерирует SQL и параметры для подготовленных запросов. Работает с SimplePdo.

Особенности

Установка

composer require knifelemon/easy-query

Быстрый старт

use KnifeLemon\EasyQuery\Builder;

$q = Builder::table('users')
    ->select(['id', 'name', 'email'])
    ->where(['status' => 'active'])
    ->orderBy('created_at DESC')
    ->limit(10)
    ->build();

// Использование с SimplePdo Flight
$users = Flight::db()->fetchAll($q['sql'], $q['params']);

Понимание build()

Метод build() возвращает массив с sql и params. Это разделение обеспечивает безопасность вашей базы данных с использованием подготовленных запросов.

$q = Builder::table('users')
    ->where(['email' => 'user@example.com'])
    ->build();

// Возвращает:
// [
//     'sql' => 'SELECT * FROM users WHERE email = ?',
//     'params' => ['user@example.com']
// ]

Типы запросов

SELECT

// Выбор всех колонок
$q = Builder::table('users')->build();
// SELECT * FROM users

// Выбор конкретных колонок
$q = Builder::table('users')
    ->select(['id', 'name', 'email'])
    ->build();
// SELECT id, name, email FROM users

// С алиасом таблицы
$q = Builder::table('users')
    ->alias('u')
    ->select(['u.id', 'u.name'])
    ->build();
// SELECT u.id, u.name FROM users AS u

INSERT

$q = Builder::table('users')
    ->insert([
        'name' => 'John Doe',
        'email' => 'john@example.com',
        'status' => 'active'
    ])
    ->build();
// INSERT INTO users SET name = ?, email = ?, status = ?

Flight::db()->runQuery($q['sql'], $q['params']);
$userId = Flight::db()->lastInsertId();

UPDATE

$q = Builder::table('users')
    ->update(['status' => 'inactive', 'updated_at' => date('Y-m-d H:i:s')])
    ->where(['id' => 123])
    ->build();
// UPDATE users SET status = ?, updated_at = ? WHERE id = ?

Flight::db()->runQuery($q['sql'], $q['params']);

DELETE

$q = Builder::table('users')
    ->delete()
    ->where(['id' => 123])
    ->build();
// DELETE FROM users WHERE id = ?

Flight::db()->runQuery($q['sql'], $q['params']);

COUNT

$q = Builder::table('users')
    ->count()
    ->where(['status' => 'active'])
    ->build();
// SELECT COUNT(*) AS cnt FROM users WHERE status = ?

$count = Flight::db()->fetchField($q['sql'], $q['params']);

Условия WHERE

Простое равенство

$q = Builder::table('users')
    ->where(['id' => 123, 'status' => 'active'])
    ->build();
// WHERE id = ? AND status = ?

Операторы сравнения

$q = Builder::table('users')
    ->where([
        'age' => ['>=', 18],
        'score' => ['<', 100],
        'name' => ['!=', 'admin']
    ])
    ->build();
// WHERE age >= ? AND score < ? AND name != ?

LIKE

$q = Builder::table('users')
    ->where(['name' => ['LIKE', '%john%']])
    ->build();
// WHERE name LIKE ?

IN / NOT IN

// IN
$q = Builder::table('users')
    ->where(['id' => ['IN', [1, 2, 3, 4, 5]]])
    ->build();
// WHERE id IN (?, ?, ?, ?, ?)

// NOT IN
$q = Builder::table('users')
    ->where(['status' => ['NOT IN', ['banned', 'deleted']]])
    ->build();
// WHERE status NOT IN (?, ?)

BETWEEN

$q = Builder::table('products')
    ->where(['price' => ['BETWEEN', [100, 500]]])
    ->build();
// WHERE price BETWEEN ? AND ?

Условия OR

Используйте orWhere() для добавления сгруппированных условий OR:

$q = Builder::table('users')
    ->where(['status' => 'active'])
    ->orWhere([
        'role' => 'admin',
        'permissions' => ['LIKE', '%manage%']
    ])
    ->build();
// WHERE status = ? AND (role = ? OR permissions LIKE ?)

JOIN

INNER JOIN

$q = Builder::table('users')
    ->alias('u')
    ->select(['u.id', 'u.name', 'p.title'])
    ->innerJoin('posts', 'u.id = p.user_id', 'p')
    ->build();
// SELECT u.id, u.name, p.title FROM users AS u INNER JOIN posts AS p ON u.id = p.user_id

LEFT JOIN

$q = Builder::table('users')
    ->alias('u')
    ->select(['u.name', 'o.total'])
    ->leftJoin('orders', 'u.id = o.user_id', 'o')
    ->build();
// ... LEFT JOIN orders AS o ON u.id = o.user_id

Множественные JOIN

$q = Builder::table('orders')
    ->alias('o')
    ->select(['o.id', 'u.name AS customer', 'p.title AS product'])
    ->innerJoin('users', 'o.user_id = u.id', 'u')
    ->leftJoin('order_items', 'o.id = oi.order_id', 'oi')
    ->leftJoin('products', 'oi.product_id = p.id', 'p')
    ->where(['o.status' => 'completed'])
    ->build();

Сортировка, группировка и лимиты

ORDER BY

$q = Builder::table('users')
    ->orderBy('created_at DESC')
    ->build();
// ORDER BY created_at DESC

GROUP BY

$q = Builder::table('orders')
    ->select(['user_id', 'COUNT(*) as order_count'])
    ->groupBy('user_id')
    ->build();
// SELECT user_id, COUNT(*) as order_count FROM orders GROUP BY user_id

LIMIT и OFFSET

$q = Builder::table('users')
    ->limit(10)
    ->build();
// LIMIT 10

$q = Builder::table('users')
    ->limit(10, 20)  // limit, offset
    ->build();
// LIMIT 10 OFFSET 20

Сырые SQL-выражения

Используйте raw() когда нужны SQL-функции или выражения, которые не должны обрабатываться как привязанные параметры.

Базовый raw

$q = Builder::table('users')
    ->update([
        'login_count' => Builder::raw('login_count + 1'),
        'updated_at' => Builder::raw('NOW()')
    ])
    ->where(['id' => 123])
    ->build();
// SET login_count = login_count + 1, updated_at = NOW()

Raw с привязанными параметрами

$q = Builder::table('orders')
    ->update([
        'total' => Builder::raw('COALESCE(subtotal, ?) + ?', [0, 10])
    ])
    ->where(['id' => 1])
    ->build();
// SET total = COALESCE(subtotal, ?) + ?
// params: [0, 10, 1]

Raw в WHERE (подзапрос)

$q = Builder::table('products')
    ->where([
        'price' => ['>', Builder::raw('(SELECT AVG(price) FROM products)')]
    ])
    ->build();
// WHERE price > (SELECT AVG(price) FROM products)

Безопасные идентификаторы для пользовательского ввода

Когда имена колонок приходят от пользователя, используйте safeIdentifier() для предотвращения SQL-инъекций:

$sortColumn = $_GET['sort'];  // например, 'created_at'
$safeColumn = Builder::safeIdentifier($sortColumn);

$q = Builder::table('users')
    ->orderBy($safeColumn . ' DESC')
    ->build();

// Если пользователь пытается: "name; DROP TABLE users--"
// Выбрасывается InvalidArgumentException

rawSafe для имен колонок от пользователя

$userColumn = $_GET['aggregate_column'];

$q = Builder::table('orders')
    ->select([
        Builder::rawSafe('SUM({col})', ['col' => $userColumn])->value . ' AS total'
    ])
    ->build();
// Проверяет имя колонки, выбрасывает исключение если недействительно

Предупреждение: Никогда не конкатенируйте пользовательский ввод напрямую в raw(). Всегда используйте привязанные параметры или safeIdentifier().


Повторное использование Query Builder

Методы очистки

Очистите конкретные части для повторного использования билдера:

$query = Builder::table('users')
    ->select(['id', 'name'])
    ->where(['status' => 'active'])
    ->orderBy('created_at DESC');

// Первый запрос
$q1 = $query->limit(10)->build();

// Очистка и повторное использование
$query->clearWhere()->clearLimit();

// Второй запрос с другими условиями
$q2 = $query
    ->where(['status' => 'pending'])
    ->limit(5)
    ->build();

Доступные методы очистки

Метод Описание
clearWhere() Очистить условия WHERE и параметры
clearSelect() Сбросить колонки SELECT на значение по умолчанию '*'
clearJoin() Очистить все JOIN-клаузы
clearGroupBy() Очистить клаузу GROUP BY
clearOrderBy() Очистить клаузу ORDER BY
clearLimit() Очистить LIMIT и OFFSET
clearAll() Сбросить билдер в начальное состояние

Пример пагинации

$baseQuery = Builder::table('users')
    ->select(['id', 'name', 'email'])
    ->where(['status' => 'active'])
    ->orderBy('created_at DESC');

// Получить общее количество
$countQuery = clone $baseQuery;
$countResult = $countQuery->clearSelect()->count()->build();
$total = Flight::db()->fetchField($countResult['sql'], $countResult['params']);

// Получить пагинированные результаты
$page = 1;
$perPage = 20;
$listResult = $baseQuery->limit($perPage, ($page - 1) * $perPage)->build();
$users = Flight::db()->fetchAll($listResult['sql'], $listResult['params']);

Динамическое построение запросов

$query = Builder::table('products')->alias('p');

if (!empty($categoryId)) {
    $query->where(['p.category_id' => $categoryId]);
}

if (!empty($minPrice)) {
    $query->where(['p.price' => ['>=', $minPrice]]);
}

if (!empty($maxPrice)) {
    $query->where(['p.price' => ['<=', $maxPrice]]);
}

if (!empty($searchTerm)) {
    $query->where(['p.name' => ['LIKE', "%{$searchTerm}%"]]);
}

$result = $query->orderBy('p.created_at DESC')->limit(20)->build();
$products = Flight::db()->fetchAll($result['sql'], $result['params']);

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

use KnifeLemon\EasyQuery\Builder;

// Список пользователей с пагинацией
Flight::route('GET /users', function() {
    $page = (int) (Flight::request()->query['page'] ?? 1);
    $perPage = 20;

    $q = Builder::table('users')
        ->select(['id', 'name', 'email', 'created_at'])
        ->where(['status' => 'active'])
        ->orderBy('created_at DESC')
        ->limit($perPage, ($page - 1) * $perPage)
        ->build();

    $users = Flight::db()->fetchAll($q['sql'], $q['params']);
    Flight::json(['users' => $users, 'page' => $page]);
});

// Создание пользователя
Flight::route('POST /users', function() {
    $data = Flight::request()->data;

    $q = Builder::table('users')
        ->insert([
            'name' => $data->name,
            'email' => $data->email,
            'created_at' => Builder::raw('NOW()')
        ])
        ->build();

    Flight::db()->runQuery($q['sql'], $q['params']);
    Flight::json(['id' => Flight::db()->lastInsertId()]);
});

// Обновление пользователя
Flight::route('PUT /users/@id', function($id) {
    $data = Flight::request()->data;

    $q = Builder::table('users')
        ->update([
            'name' => $data->name,
            'email' => $data->email,
            'updated_at' => Builder::raw('NOW()')
        ])
        ->where(['id' => $id])
        ->build();

    Flight::db()->runQuery($q['sql'], $q['params']);
    Flight::json(['success' => true]);
});

// Удаление пользователя
Flight::route('DELETE /users/@id', function($id) {
    $q = Builder::table('users')
        ->delete()
        ->where(['id' => $id])
        ->build();

    Flight::db()->runQuery($q['sql'], $q['params']);
    Flight::json(['success' => true]);
});

Справочник API

Статические методы

Метод Описание
Builder::table(string $table) Создать новый экземпляр билдера для таблицы
Builder::raw(string $sql, array $bindings = []) Создать сырое SQL-выражение
Builder::rawSafe(string $expr, array $identifiers, array $bindings = []) Сырое выражение с безопасной заменой идентификаторов
Builder::safeIdentifier(string $identifier) Проверить и вернуть безопасное имя колонки/таблицы

Методы экземпляра

Метод Описание
alias(string $alias) Установить алиас таблицы
select(string\|array $columns) Установить колонки для выбора (по умолчанию: '*')
where(array $conditions) Добавить условия WHERE (AND)
orWhere(array $conditions) Добавить условия OR WHERE
join(string $table, string $condition, string $alias, string $type) Добавить клаузу JOIN
innerJoin(string $table, string $condition, string $alias) Добавить INNER JOIN
leftJoin(string $table, string $condition, string $alias) Добавить LEFT JOIN
groupBy(string $groupBy) Добавить клаузу GROUP BY
orderBy(string $orderBy) Добавить клаузу ORDER BY
limit(int $limit, int $offset = 0) Добавить LIMIT и OFFSET
count(string $column = '*') Установить запрос на COUNT
insert(array $data) Установить запрос на INSERT
update(array $data) Установить запрос на UPDATE
delete() Установить запрос на DELETE
build() Построить и вернуть ['sql' => ..., 'params' => ...]
get() Псевдоним для build()

Интеграция с Tracy Debugger

EasyQuery автоматически интегрируется с Tracy Debugger, если он установлен. Настройка не требуется!

composer require tracy/tracy
use Tracy\Debugger;

Debugger::enable();

// Все запросы автоматически логируются в панель Tracy
$q = Builder::table('users')->where(['status' => 'active'])->build();

Панель Tracy показывает:

Для полной документации посетите GitHub-репозиторий.

Awesome-plugins/twig

Twig

Twig — это гибкий, быстрый и безопасный шаблонизатор для PHP. Это язык шаблонов, используемый Symfony и многими другими проектами, что означает, что инструменты ИИ-кодирования и большинство PHP-разработчиков уже хорошо знакомы с его синтаксисом. Twig компилирует шаблоны в оптимизированный PHP, автоматически экранирует вывод по умолчанию (отлично для защиты от XSS) и легко расширяется фильтрами, функциями и расширениями.

Установка

Установите с помощью composer.

composer require twig/twig

Базовая настройка

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

require 'vendor/autoload.php';

$app = Flight::app();

$app->map('render', function(string $template, array $data): void {
    $loader = new \Twig\Loader\FilesystemLoader(Flight::get('flight.views.path'));
    $twig = new \Twig\Environment($loader, [
        // Где Twig хранит скомпилированные шаблоны
        'cache' => __DIR__ . '/../cache/twig',
        // Перекомпилировать шаблоны при изменении исходного кода (удобно при разработке)
        'auto_reload' => true,
    ]);

    echo $twig->render($template, $data);
});

Регистрация Twig как класса представления

Если вы предпочитаете повторно использовать одну среду Twig (рекомендуется для продакшена), зарегистрируйте её и укажите render на неё:

require 'vendor/autoload.php';

$app = Flight::app();

$app->register('view', \Twig\Environment::class, [
    new \Twig\Loader\FilesystemLoader($app->get('flight.views.path')),
    [
        'cache' => __DIR__ . '/../cache/twig',
        'auto_reload' => true,
    ],
]);

$app->map('render', function(string $template, array $data): void {
    echo Flight::view()->render($template, $data);
});

Пример простого макета

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

{# app/views/layout.twig #}
<!doctype html>
<html lang="en">
    <head>
        <title>{% if title %}{{ title }} - {% endif %}My App</title>
        <link rel="stylesheet" href="style.css">
    </head>
    <body>
        <header>
            <nav>
                {# ваши элементы навигации здесь #}
            </nav>
        </header>
        <div id="content">
            {# Вот здесь магия #}
            {% block content %}{% endblock %}
        </div>
        <div id="footer">
            &copy; Copyright
        </div>
    </body>
</html>

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

{# app/views/home.twig #}
{# Это говорит Twig, что этот файл "внутри" файла layout.twig #}
{% extends 'layout.twig' %}

{# Это содержимое, которое будет рендериться внутри макета в блоке content #}
{% block content %}
    <h1>Home Page</h1>
    <p>Welcome to my app!</p>
{% endblock %}

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

// простой маршрут
Flight::route('/', function () {
    Flight::render('home.twig', [
        'title' => 'Home Page'
    ]);
});

// или если вы используете контроллер
Flight::route('/', [HomeController::class, 'index']);

// HomeController.php
class HomeController
{
    public function index()
    {
        Flight::render('home.twig', [
            'title' => 'Home Page'
        ]);
    }
}

Смотрите документацию Twig для получения дополнительной информации о том, как использовать Twig на полную мощность!

Отладка

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

$app->register('view', \Twig\Environment::class, [
    new \Twig\Loader\FilesystemLoader($app->get('flight.views.path')),
    [
        'cache' => __DIR__ . '/../cache/twig',
        'debug' => true, // требуется для функции dump()
        'auto_reload' => true,
    ],
], function (\Twig\Environment $twig): void {
    $twig->addExtension(new \Twig\Extension\DebugExtension());
});

Затем в шаблоне:

{{ dump(user) }}

Вы также можете объединить Twig с Tracy для отладки на уровне PHP. Для метрик на уровне шаблона (время рендеринга, память, какие шаблоны/блоки выполнялись), используйте опциональную панель Twig в flightphp/tracy-extensions: передайте Twig\Profiler\Profile как twig_profile в TracyExtensionLoader. Опциональное TwigTracyExtension предоставляет {{ dump() }} / {{ bdump() }} / {{ dumpe() }} в шаблонах, когда Tracy включён.

Примечание по безопасности

Twig автоматически экранирует вывод по умолчанию, что помогает защититься от XSS-атак. Предпочтительно использовать {{ variable }} для текста. Используйте фильтр |raw только тогда, когда вы намеренно доверяете HTML-содержимому (например, очищенному markdown, который вы уже обработали на стороне сервера).

Awesome-plugins/session

FlightPHP 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

Runway

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

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

Команды скаффолдинга намеренно согласованы с официальным скелетом, поэтому инструменты кодирования ИИ и люди получают одинаковые пути, пространства имён и стиль конструкторской инъекции каждый раз.

Установка

Установите с помощью composer.

composer require flightphp/runway

Скелет уже зависит от Runway; используйте php runway из корня проекта.

Базовая конфигурация

При первом запуске Runway попытается найти конфигурацию runway в app/config/config.php через ключ 'runway'.

<?php
// app/config/config.php
return [
    'runway' => [
        'app_root' => 'app/',
        'public_root' => 'public/',
        // необязательно; скелет также использует index_root для публичной точки входа
        'index_root' => 'public/index.php',
    ],
];

ПРИМЕЧАНИЕ - Начиная с v1.2.0, .runway-config.json устарел в пользу app/config/config.php. Мигрируйте с помощью php runway config:migrate при обновлении старых проектов. Скелет может всё ещё создавать небольшой .runway-config.json при create-project для совместимости; предпочтите ключ runway в config.php в будущем.

Определение корня проекта

Runway достаточно умен, чтобы определить корень вашего проекта, даже если вы запускаете его из подкаталога. Он ищет такие индикаторы, как composer.json, .git или app/config/config.php, чтобы определить, где находится корень проекта. Это означает, что вы можете запускать команды Runway из любого места вашего проекта!

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

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

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

Список команд

Вы можете просмотреть список всех доступных команд, выполнив команду php runway.

php runway

Полагайтесь только на команды, которые действительно появляются в этом списке для вашей установки (основные команды Runway против специфичных для проекта, таких как migrate скелета).

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

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

php runway routes --help
php runway make:controller --help

Вот несколько примеров:

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

make:controller создаёт каркас контроллера, который соответствует макету официального скелета:

Путь app/Controller/{Name}.php
Пространство имён App\Controller
Стиль Конструкторская инъекция flight\Engine (без Flight:: в теле класса)
php runway make:controller MyController
# → app/Controller/MyController.php
#   namespace App\Controller;

Пример ожидаемой структуры (упрощённый):

<?php

declare(strict_types=1);

namespace App\Controller;

use flight\Engine;

class MyController
{
    protected Engine $app;

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

    public function index(): void
    {
        // например $this->app->render('…', […]);
    }
}

Зарегистрируйте его с помощью callable класса, чтобы Dice мог создать контроллер:

// app/config/routes.php
use App\Controller\MyController;

$router->get('/mine', [MyController::class, 'index']);

Почему такая структура? Регистр папки должен соответствовать пространству имён (Controller, а не controllers) для Composer PSR-4 на Linux — см. Автозагрузку. Тот же путь используется в корневых и scoped файлах AGENTS.md, которые указывают инструментам ИИ, что использовать, поэтому сгенерированные и написанные вручную контроллеры остаются идентичными.

Старые документации и общественные проекты иногда использовали app/controllers/ и app\controllers. Это остаётся допустимым, если ваше дерево всё ещё использует строчные папки. Новые проекты скелета и текущий вывод make:controller используют app/Controller/ + App\Controller.

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

Сначала убедитесь, что вы установили плагин Active Record.

php runway make:record users

В официальном скелете модели находятся в app/Model/ с пространством имён App\Model, а соединение с БД — SimplePdo (внедрите его или передайте в конструктор ActiveRecord). Имена файлов/пространств имён сгенерированных файлов следуют текущим значениям по умолчанию Runway и вашей конфигурации runway — предпочтите согласование новых моделей с App\Model, чтобы они соответствовали автозагрузке и AGENTS.md.

Пример модели, согласованной с демо постов скелета:

<?php

declare(strict_types=1);

namespace App\Model;

use flight\ActiveRecord;

/**
 * @property int $id
 * @property string $title
 * // …
 */
class Post extends ActiveRecord
{
    protected array $relations = [];

    public function __construct($databaseConnection)
    {
        parent::__construct($databaseConnection, 'posts');
    }
}

Если старый генератор всё ещё создаёт app/records / app\records, вы можете сохранить это соглашение в устаревших приложениях или переместить файлы в app/Model/ и обновить пространство имён в соответствии с регистром папки.

Миграции (скелет)

Официальный скелет поставляет проектную команду (обнаруженную из app/commands/), такую как:

php runway migrate

Миграции — это SQL-файлы в migrations/ (например YYYYMMDDHHMMSS_description.sql для SQLite и …_description.mysql.sql для MySQL), выбранные из конфигурации драйвера базы данных / env. Точные флаги и поведение определяются этой проектной командой — запустите php runway migrate --help в вашем приложении.

Помощники ИИ

Runway предоставляет команды, ориентированные на ИИ, используемые с ИИ и опытом разработчика:

php runway ai:init
php runway ai:generate-instructions

Они хранят учётные данные LLM и генерируют инструкции проекта (в первую очередь AGENTS.md). В скелете рассматривайте AGENTS.md (и scoped копии в app/) плюс SECURITY.md как источник истины для агентов.

Отображение всех маршрутов

Это отобразит все маршруты, которые в настоящее время зарегистрированы в Flight.

php runway routes

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

# Отобразить только GET маршруты
php runway routes --get

# Отобразить только POST маршруты
php runway routes --post

# и т.д.

Добавление пользовательских команд в Runway

Если вы создаёте пакет для Flight или хотите добавить свои собственные пользовательские команды в свой проект, вы можете сделать это, создав директорию src/commands/, flight/commands/, app/commands/ или commands/ для вашего проекта/пакета. Если вам нужна дальнейшая кастомизация, см. раздел ниже о Конфигурации.

В скелете проектные команды находятся в app/commands/ с пространством имён App\Command. Runway обнаруживает их по пути; синхронизируйте эту папку с classmap/PSR-4 Composer, как это делает ваш проект.

Чтобы создать команду, просто расширьте класс AbstractBaseCommand и реализуйте как минимум метод __construct и метод execute.

<?php

declare(strict_types=1);

namespace App\Command;

use flight\commands\AbstractBaseCommand;

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

    /**
     * Выполняет функцию
     *
     * @return void
     */
    public function execute()
    {
        $io = $this->app()->io();

        $io->info('Создание примера...');

        // Сделайте что-то здесь

        $io->ok('Пример создан!');
    }
}

См. Документацию adhocore/php-cli для получения дополнительной информации о том, как создавать свои собственные пользовательские команды в вашем приложении Flight!

Управление конфигурацией

Поскольку конфигурация была перенесена в app/config/config.php начиная с v1.2.0, есть несколько вспомогательных команд для управления конфигурацией.

Совет по скелету: Держите config.php как литеральные PHP-значения. Секреты должны находиться в .env. Избегайте выражений $_ENV[...] внутри config.phpconfig:set переписывает этот файл как статические данные и может встроить секреты в файл. См. Конфигурацию.

Миграция старой конфигурации

Если у вас есть старый файл .runway-config.json, вы можете легко перенести его в app/config/config.php с помощью следующей команды:

php runway config:migrate

Установка значения конфигурации

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

php runway config:set app_root "app/"

Получение значения конфигурации

Вы можете получить значение конфигурации с помощью команды config:get.

php runway config:get app_root

Все конфигурации Runway

Если вам нужно настроить конфигурацию для Runway, вы можете установить эти значения в app/config/config.php. Ниже приведены некоторые дополнительные конфигурации, которые вы можете установить:

<?php
// app/config/config.php
return [
    // ... другие значения конфигурации ...

    'runway' => [
        // Здесь находится директория вашего приложения
        'app_root' => 'app/',

        // Это директория, где находится ваш корневой index-файл
        'index_root' => 'public/',

        // Это пути к корням других проектов
        'root_paths' => [
            '/home/user/different-project',
            '/var/www/another-project'
        ],

        // Базовые пути, скорее всего, не нуждаются в настройке, но они здесь, если вам нужно
        'base_paths' => [
            '/includes/libs/vendor', // если у вас действительно уникальный путь к директории vendor или что-то подобное
        ],

        // Конечные пути — это места в проекте для поиска файлов команд
        'final_paths' => [
            'src/diff-path/commands',
            'app/module/admin/commands',
        ],

        // Если вы хотите просто добавить полный путь, вперёд (абсолютный или относительный к корню проекта)
        'paths' => [
            '/home/user/different-project/src/diff-path/commands',
            '/var/www/another-project/app/module/admin/commands',
            'app/my-unique-commands'
        ]
    ]
];

Доступ к конфигурации

Если вам нужно эффективно получить доступ к значениям конфигурации, вы можете получить к ним доступ через метод __construct или метод app(). Также важно отметить, что если у вас есть файл app/config/services.php, эти сервисы также будут доступны для вашей команды.

public function execute()
{
    $io = $this->app()->io();

    // Доступ к конфигурации
    $app_root = $this->config['runway']['app_root'];

    // Доступ к сервисам, например, к соединению с базой данных
    $database = $this->config['database']

    // ...
}

Обёртки помощников ИИ

Runway имеет некоторые обёртки помощников, которые облегчают генерацию команд ИИ. Вы можете использовать addOption и addArgument способом, который похож на Symfony Console. Это полезно, если вы используете инструменты ИИ для генерации ваших команд.

public function __construct(array $config)
{
    parent::__construct('make:example', 'Создать пример для документации', $config);

    // Аргумент mode может быть null и по умолчанию полностью необязателен
    $this->addOption('name', 'Название примера', null);
}

См. также

Awesome-plugins/tracy_extensions

Расширения панели Tracy для Flight

Это набор расширений, которые делают работу с Flight немного богаче.

Это особенно удобно с официальным скелетом, который по умолчанию использует Twig: тот же макет AI tools также четко отображается на панели Tracy.

Это панель

Flight Bar

И каждая панель отображает очень полезную информацию о вашем приложении!

Flight Data Flight Database Flight Request

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

Установка

Выполните composer require flightphp/tracy-extensions --dev и вы на пути!

Twig не является жесткой зависимостью пакета. Установите twig/twig только если хотите панель Twig (скелет уже делает это для представлений).

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

Для начала требуется очень мало конфигурации. Вам нужно будет инициализировать отладчик Tracy перед использованием этого https://tracy.nette.org/en/guide:

<?php

use Tracy\Debugger;
use flight\debug\tracy\TracyExtensionLoader;

// bootstrap code
require __DIR__ . '/vendor/autoload.php';

Debugger::enable();
// Возможно, вам нужно указать окружение с Debugger::enable(Debugger::DEVELOPMENT)

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

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

// еще код

Flight::start();

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

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

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


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

require 'vendor/autoload.php';

$app = Flight::app();

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

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

// маршруты и другие вещи...

Flight::start();

Панель Twig (опционально)

Если ваше приложение использует Twig (включая официальный скелет), вы можете показывать метрики шаблонов на панели Tracy. Создайте Profile Twig, прикрепите ProfilerExtension к вашему окружению, затем передайте этот профиль в загрузчик под ключом twig_profile. Прикрепляйте профилирование только в разработке.

<?php

use flight\debug\tracy\TracyExtensionLoader;
use flight\debug\tracy\TwigTracyExtension;
use Tracy\Debugger;
use Twig\Environment;
use Twig\Extension\ProfilerExtension;
use Twig\Loader\FilesystemLoader;
use Twig\Profiler\Profile;

$loader = new FilesystemLoader(__DIR__ . '/views');
$twig = new Environment($loader, [
    'debug' => true,
    'cache' => false,
]);

// Опционально: предоставить помощники дампа Tracy в шаблонах
// {{ dump(var) }}, {{ bdump(var) }}, {{ dumpe(var) }}
$twig->addExtension(new TwigTracyExtension());

$tracyConfig = [];
if (Debugger::$showBar === true) {
    $profile = new Profile();
    $twig->addExtension(new ProfilerExtension($profile));
    $tracyConfig['twig_profile'] = $profile;
}

if (Debugger::$showBar === true) {
    Flight::set('flight.content_length', false);
    new TracyExtensionLoader(Flight::app(), $tracyConfig);
}

// Связать Flight::render() с Twig (пример)
Flight::map('render', function (string $template, array $data = []) use ($twig) {
    if (substr($template, -5) !== '.twig') {
        $template .= '.twig';
    }
    echo $twig->render($template, $data);
});

Что показывает панель

Вкладка Twig скрыта, когда для запроса не рендерились шаблоны, или когда вы опускаете twig_profile (или не имеете Twig установленным)—другие панели Flight продолжают работать.

В services.php в стиле скелета создайте тот же $profile / ProfilerExtension, когда отладка включена, передайте twig_profile в TracyExtensionLoader и продолжайте использовать общее окружение Twig для $app->render().

Latte

Для этого раздела требуется PHP 8.1+.

Если у вас установлен Latte в вашем проекте, Tracy имеет нативную интеграцию с Latte для анализа ваших шаблонов. Вы просто регистрируете расширение с вашим экземпляром Latte (это собственный мост Tracy для Latte, а не панель Twig выше).


require 'vendor/autoload.php';

$app = Flight::app();

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

    // другие конфигурации...

    // добавить расширение только если панель отладки Tracy включена
    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.

FlightPHP APM

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

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

Установка

Начните с Composer:

composer require flightphp/apm

Вам потребуется:

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

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

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

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

Вот пошаговая инструкция для достижения крутых результатов с APM:

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

Добавьте следующий код в index.php или файл services.php, чтобы начать отслеживание:

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

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

// Если вы добавляете подключение к базе данных
// Рекомендуется использовать SimplePdo (или PdoQueryCapture из Tracy Extensions в режиме разработки).
// Включите отслеживание запросов APM через массив опций (5-й аргумент).
$pdo = new SimplePdo('mysql:host=localhost;dbname=example', 'user', 'pass', null, [
    'trackApmQueries' => 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

Что делает эта команда?

В процессе также будет предложено запустить миграции для данной настройки. Если вы настраиваете APM впервые, ответьте «да».

Зачем два хранилища? Сырые метрики быстро накапливаются (как нефильтрованные логи). Воркер обрабатывает их и сохраняет в структурированное хранилище для дашборда. Так всё остаётся аккуратным!

3. Обработка метрик с помощью воркера

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

php vendor/bin/runway apm:worker

Что он делает?

Постоянная работа Для работающих приложений вам потребуется непрерывная обработка. Вот ваши варианты:

Зачем это нужно? Без воркера ваш дашборд будет пустым. Он служит мостом между сырыми логами и полезными insights.

4. Запуск дашборда

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

php vendor/bin/runway apm:dashboard

Что делает эта команда?

Настройка:

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

Откройте URL в браузере и исследуйте!

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

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

Хотите другой дашборд?

Вы можете создать собственный дашборд! Посмотрите директорию vendor/flightphp/apm/src/apm/presenter для идей по отображению данных.

Возможности дашборда

Дашборд — это ваш APM-центр. Вот что вы увидите:

Дополнительно:

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

Добавление пользовательских событий

Отслеживайте что угодно — например, вызов API или процесс оплаты:

use flight\apm\CustomEvent;

$app->eventDispatcher()->trigger('apm.custom', new CustomEvent('api_call', [
    'endpoint' => 'https://api.example.com/users',
    'response_time' => 0.25,
    'status' => 200
]));

Где это отображается? В деталях запроса на дашборде, в разделе «Пользовательские события» — с возможностью раскрытия и красивого JSON-форматирования.

Пример использования:

$start = microtime(true);
$apiResponse = file_get_contents('https://api.example.com/data');
$app->eventDispatcher()->trigger('apm.custom', new CustomEvent('external_api', [
    'url' => 'https://api.example.com/data',
    'time' => microtime(true) - $start,
    'success' => $apiResponse !== false
]));

Теперь вы увидите, не тормозит ли ваше приложение из-за этого API!

Мониторинг базы данных

Отслеживайте PDO-запросы следующим образом:

use flight\database\SimplePdo;

$pdo = new SimplePdo('sqlite:/path/to/db.sqlite', null, null, null, [
    'trackApmQueries' => true, // обязательно для захвата запросов в APM
]);
$Apm->addPdoConnection($pdo);

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

Внимание:

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

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

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

Пример:

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

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

Request ID в приложении

Каждый запрос имеет уникальный идентификатор (request ID) для отслеживания. Вы можете использовать этот ID в приложении для сопоставления логов и метрик. Например, можно добавить request ID на страницу ошибки:

Flight::map('error', function($message) {
    // Получить request 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-база данных большая, миграции могут занять значительное время. Рекомендуется выполнять эту команду в непиковые часы.

Обновление с 0.4.3 -> 0.5.0

Если вы обновляетесь с версии 0.4.3 до 0.5.0, выполните следующую команду:

php vendor/bin/runway apm:config-migrate

Это мигрирует вашу конфигурацию из старого формата (используя .runway-config.json) в новый, где ключи/значения хранятся в файле config.php.

Очистка старых данных

Чтобы поддерживать базу данных в порядке, вы можете очищать старые данные. Это особенно полезно, если у вас нагруженное приложение и вы хотите контролировать размер базы данных. Выполните следующую команду:

php vendor/bin/runway apm:purge

Она удалит все данные старше 30 дней из базы. Вы можете изменить количество дней с помощью опции --days:

php vendor/bin/runway apm:purge --days 7

Это удалит все данные старше 7 дней.

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

Возникли проблемы? Попробуйте следующее:

Awesome-plugins/tracy

Tracy

Tracy — это потрясающий обработчик ошибок, который можно использовать с Flight. Он имеет ряд панелей, которые могут помочь вам отладить ваше приложение. Его также очень легко расширять и добавлять собственные панели. Команда Flight создала несколько панелей специально для проектов на Flight с помощью плагина flightphp/tracy-extensions (переменные Flight, запросы к БД, запросы, сессии и необязательная панель Twig, когда вы передаете профиль профилировщика — см. Расширения Tracy).

Установка

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

composer require tracy/tracy

Базовая конфигурация

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


require 'vendor/autoload.php';

use Tracy\Debugger;

// Включить Tracy
Debugger::enable();
// Debugger::enable(Debugger::DEVELOPMENT) // иногда вам нужно явно указать (также Debugger::PRODUCTION)
// Debugger::enable('23.75.345.200'); // вы также можете предоставить массив IP-адресов

// Здесь будут логироваться ошибки и исключения. Убедитесь, что эта директория существует и доступна для записи.
Debugger::$logDirectory = __DIR__ . '/../log/';
Debugger::$strictMode = true; // отображать все ошибки
// Debugger::$strictMode = E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED; // все ошибки, кроме устаревших уведомлений
if (Debugger::$showBar) {
    $app->set('flight.content_length', false); // если панель Debugger видима, то content-length не может быть установлен Flight

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

Полезные советы

При отладке вашего кода есть несколько очень полезных функций для вывода данных.

Awesome-plugins/active_record

Flight Active Record

Active Record — это сопоставление сущности базы данных с объектом PHP. Проще говоря, если у вас есть таблица users в базе данных, вы можете "перевести" строку в этой таблице в класс User и объект $user в вашем коде. См. базовый пример.

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

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

Предположим, у вас есть следующая таблица:

CREATE TABLE users (
    id INTEGER PRIMARY KEY, 
    name TEXT, 
    password TEXT 
);

Теперь вы можете настроить новый класс для представления этой таблицы:

/**
 * Класс ActiveRecord обычно используется в единственном числе
 * 
 * Настоятельно рекомендуется добавлять свойства таблицы в виде комментариев здесь
 * 
 * @property int    $id
 * @property string $name
 * @property string $password
 */ 
class User extends flight\ActiveRecord {
    public function __construct($database_connection)
    {
        // вы можете установить это таким образом
        parent::__construct($database_connection, 'users');
        // или таким образом
        parent::__construct($database_connection, null, [ 'table' => 'users']);
    }
}

Теперь наблюдайте за магией!

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

// для mysql
$database_connection = new PDO('mysql:host=localhost;dbname=test_db&charset=utf8bm4', 'username', 'password');

// или mysqli
$database_connection = new mysqli('localhost', 'username', 'password', 'test_db');
// или mysqli с созданием на основе не-объекта
$database_connection = mysqli_connect('localhost', 'username', 'password', 'test_db');

$user = new User($database_connection);
$user->name = 'Bobby Tables';
$user->password = password_hash('some cool password');
$user->insert();
// или $user->save();

echo $user->id; // 1

$user->name = 'Joseph Mamma';
$user->password = password_hash('some cool password again!!!');
$user->insert();
// здесь нельзя использовать $user->save(), иначе оно подумает, что это обновление!

echo $user->id; // 2

И было так просто добавить нового пользователя! Теперь, когда в базе данных есть строка пользователя, как вы её извлечёте?

$user->find(1); // найти id = 1 в базе данных и вернуть его.
echo $user->name; // 'Bobby Tables'

А что, если вы хотите найти всех пользователей?

$users = $user->findAll();

А с определённым условием?

$users = $user->like('name', '%mamma%')->findAll();

Видите, насколько это весело? Давайте установим его и начнём!

Установка

Просто установите с помощью Composer

composer require flightphp/active-record 

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

Это можно использовать как самостоятельную библиотеку или с фреймворком Flight PHP. Полностью на ваше усмотрение.

Автономно

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

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

$User = new User($pdo_connection);

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

Регистрация как метода в Flight

Если вы используете фреймворк Flight PHP, вы можете зарегистрировать класс ActiveRecord как сервис, но честно говоря, это не обязательно.

Flight::register('user', 'User', [ $pdo_connection ]);

// затем вы можете использовать его так в контроллере, функции и т.д.

Flight::user()->find(1);

Методы runway

runway — это CLI-инструмент для Flight, который имеет пользовательскую команду для этой библиотеки.

# Использование
php runway make:record database_table_name [class_name]

# Пример
php runway make:record users

Это создаст новый класс в директории app/records/ как UserRecord.php со следующим содержимым:

<?php

declare(strict_types=1);

namespace app\records;

/**
 * Класс ActiveRecord для таблицы users.
 * @link https://docs.flightphp.com/awesome-plugins/active-record
 *
 * @property int $id
 * @property string $username
 * @property string $email
 * @property string $password_hash
 * @property string $created_dt
 */
class UserRecord extends \flight\ActiveRecord
{
    /**
     * @var array $relations Установка отношений для модели
     *   https://docs.flightphp.com/awesome-plugins/active-record#relationships
     */
    protected array $relations = [
        // 'relation_name' => [ self::HAS_MANY, 'RelatedClass', 'foreign_key' ],
    ];

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

Функции CRUD

find($id = null) : boolean|ActiveRecord

Найти одну запись и присвоить её текущему объекту. Если вы передадите $id какого-либо рода, он выполнит поиск по первичному ключу с этим значением. Если ничего не передано, он просто найдёт первую запись в таблице.

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

// найти запись с некоторыми условиями заранее
$user->notNull('password')->orderBy('id DESC')->find();

// найти запись по конкретному id
$id = 123;
$user->find($id);

findAll(): array<int,ActiveRecord>

Находит все записи в указанной таблице.

$user->findAll();

isHydrated(): boolean (v0.4.0)

Возвращает true, если текущая запись была гидратирована (загружена из базы данных).

$user->find(1);
// если запись найдена с данными...
$user->isHydrated(); // true

insert(): boolean|ActiveRecord

Вставляет текущую запись в базу данных.

$user = new User($pdo_connection);
$user->name = 'demo';
$user->password = md5('demo');
$user->insert();
Первичные ключи на основе текста

Если у вас есть первичный ключ на основе текста (например, UUID), вы можете установить значение первичного ключа перед вставкой одним из двух способов.

$user = new User($pdo_connection, [ 'primaryKey' => 'uuid' ]);
$user->uuid = 'some-uuid';
$user->name = 'demo';
$user->password = md5('demo');
$user->insert(); // или $user->save();

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

class User extends flight\ActiveRecord {
    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users', [ 'primaryKey' => 'uuid' ]);
        // вы также можете установить primaryKey таким образом вместо массива выше.
        $this->primaryKey = 'uuid';
    }

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

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

update(): boolean|ActiveRecord

Обновляет текущую запись в базе данных.

$user->greaterThan('id', 0)->orderBy('id desc')->find();
$user->email = 'test@example.com';
$user->update();

save(): boolean|ActiveRecord

Вставляет или обновляет текущую запись в базу данных. Если запись имеет id, она обновится, иначе вставится.

$user = new User($pdo_connection);
$user->name = 'demo';
$user->password = md5('demo');
$user->save();

Примечание: Если у вас определены отношения в классе, он рекурсивно сохранит эти отношения, если они определены, инстанцированы и имеют изменённые данные для обновления. (v0.4.0 и выше)

delete(): boolean

Удаляет текущую запись из базы данных.

$user->gt('id', 0)->orderBy('id desc')->find();
$user->delete();

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

$user->like('name', 'Bob%')->delete();

dirty(array $dirty = []): ActiveRecord

Грязные данные относятся к данным, которые были изменены в записи.

$user->greaterThan('id', 0)->orderBy('id desc')->find();

// на данный момент ничего не "грязное".

$user->email = 'test@example.com'; // теперь email считается "грязным", поскольку оно изменилось.
$user->update();
// теперь нет данных, которые являются грязными, потому что они обновлены и сохранены в базе данных

$user->password = password_hash()'newpassword'); // теперь это грязное
$user->dirty(); // передача ничего очистит все грязные записи.
$user->update(); // ничего не обновится, потому что ничего не было захвачено как грязное.

$user->dirty([ 'name' => 'something', 'password' => password_hash('a different password') ]);
$user->update(); // и name, и password обновлены.

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

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

$user->copyFrom([ 'name' => 'something', 'password' => password_hash('a different password') ]);
$user->update(); // и name, и password обновлены.

isDirty(): boolean (v0.4.0)

Возвращает true, если текущая запись была изменена.

$user->greaterThan('id', 0)->orderBy('id desc')->find();
$user->email = 'test@email.com';
$user->isDirty(); // true

reset(bool $include_query_data = true): ActiveRecord

Сбрасывает текущую запись в её начальное состояние. Это очень полезно для циклов. Если вы передадите true, он также сбросит данные запроса, которые использовались для поиска текущего объекта (поведение по умолчанию).

$users = $user->greaterThan('id', 0)->orderBy('id desc')->find();
$user_company = new UserCompany($pdo_connection);

foreach($users as $user) {
    $user_company->reset(); // начать с чистого листа
    $user_company->user_id = $user->id;
    $user_company->company_id = $some_company_id;
    $user_company->insert();
}

getBuiltSql(): string (v0.4.1)

После выполнения метода find(), findAll(), insert(), update() или save() вы можете получить построенный SQL и использовать его для отладки.

Методы SQL-запросов

select(string $field1 [, string $field2 ... ])

Вы можете выбрать только несколько столбцов в таблице, если хотите (это более производительно для очень широких таблиц с многими столбцами)

$user->select('id', 'name')->find();

from(string $table)

Вы технически можете выбрать другую таблицу тоже! Почему бы и нет?!

$user->select('id', 'name')->from('user')->find();

join(string $table_name, string $join_condition)

Вы даже можете присоединить другую таблицу в базе данных.

$user->join('contacts', 'contacts.user_id = users.id')->find();

where(string $where_conditions)

Вы можете установить некоторые пользовательские аргументы where (вы не можете установить параметры в этом операторе where)

$user->where('id=1 AND name="demo"')->find();

Примечание по безопасности — Вас может соблазнить сделать что-то вроде $user->where("id = '{$id}' AND name = '{$name}'")->find();. Пожалуйста, НЕ ДЕЛАЙТЕ ЭТОГО!!! Это подвержено тому, что известно как атаки SQL-инъекций. В интернете много статей, пожалуйста, погуглите "sql injection attacks php" и вы найдёте много статей по этой теме. Правильный способ обработки этого с этой библиотекой — вместо этого метода where() вы бы сделали что-то вроде $user->eq('id', $id)->eq('name', $name)->find(); Если вам абсолютно необходимо это сделать, библиотека PDO имеет $pdo->quote($var) для экранирования. Только после использования quote() вы можете использовать его в операторе where().

group(string $group_by_statement)/groupBy(string $group_by_statement)

Группируйте ваши результаты по определённому условию.

$user->select('COUNT(*) as count')->groupBy('name')->findAll();

order(string $order_by_statement)/orderBy(string $order_by_statement)

Сортируйте возвращаемый запрос определённым образом.

$user->orderBy('name DESC')->find();

limit(string $limit)/limit(int $offset, int $limit)

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

Условия OR

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

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

// Метод 2
$user->eq('id', 1)->eq('name', 'demo', 'OR')->find();
// Это вычислится как `id = 1 OR name = 'demo'`

Отношения

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

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

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

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

        // обязательно
        // в зависимости от типа отношения
        // self::HAS_ONE = внешний ключ, который ссылается на соединение
        // self::HAS_MANY = внешний ключ, который ссылается на соединение
        // self::BELONGS_TO = локальный ключ, который ссылается на соединение
        'local_or_foreign_key',
        // просто 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; // это имя пользователя

Довольно круто, eh?

Жадная загрузка

Обзор

Жадная загрузка решает проблему N+1 запросов, загружая отношения заранее. Вместо выполнения отдельного запроса для отношений каждой записи, жадная загрузка извлекает все связанные данные всего в одном дополнительном запросе на отношение.

Примечание: Жадная загрузка доступна только для v0.7.0 и выше.

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

Используйте метод with() для указания, какие отношения жадно загружать:

// Загрузить пользователей с их контактами в 2 запроса вместо N+1
$users = $user->with('contacts')->findAll();
foreach ($users as $u) {
    foreach ($u->contacts as $contact) {
        echo $contact->email; // Нет дополнительного запроса!
    }
}

Несколько отношений

Загружайте несколько отношений одновременно:

$users = $user->with(['contacts', 'profile', 'settings'])->findAll();

Типы отношений

HAS_MANY
// Жадно загрузить все контакты для каждого пользователя
$users = $user->with('contacts')->findAll();
foreach ($users as $u) {
    // $u->contacts уже загружен как массив
    foreach ($u->contacts as $contact) {
        echo $contact->email;
    }
}
HAS_ONE
// Жадно загрузить один контакт для каждого пользователя
$users = $user->with('contact')->findAll();
foreach ($users as $u) {
    // $u->contact уже загружен как объект
    echo $u->contact->email;
}
BELONGS_TO
// Жадно загрузить родительских пользователей для всех контактов
$contacts = $contact->with('user')->findAll();
foreach ($contacts as $c) {
    // $c->user уже загружен
    echo $c->user->name;
}
С find()

Жадная загрузка работает как с findAll() , так и с find() :

$user = $user->with('contacts')->find(1);
// Пользователь и все их контакты загружены в 2 запроса

Преимущества производительности

Без жадной загрузки (проблема N+1):

$users = $user->findAll(); // 1 запрос
foreach ($users as $u) {
    $contacts = $u->contacts; // N запросов (по одному на пользователя!)
}
// Итого: 1 + N запросов

С жадной загрузкой:

$users = $user->with('contacts')->findAll(); // всего 2 запроса
foreach ($users as $u) {
    $contacts = $u->contacts; // 0 дополнительных запросов!
}
// Итого: 2 запроса (1 для пользователей + 1 для всех контактов)

Для 10 пользователей это уменьшает запросы с 11 до 2 — снижение на 82%!

Важные примечания

Ограничения

Установка пользовательских данных

Иногда вам может понадобиться прикрепить что-то уникальное к вашему ActiveRecord, например, пользовательский расчёт, который может быть проще прикрепить к объекту, который затем передаётся, скажем, в шаблон.

setCustomData(string $field, mixed $value)

Вы прикрепляете пользовательские данные с помощью метода setCustomData().

$user->setCustomData('page_view_count', $page_view_count);

А затем вы просто ссылаетесь на него как на обычное свойство объекта.

echo $user->page_view_count;

События

Ещё одна супер крутая функция этой библиотеки — это события. События срабатывают в определённые моменты на основе определённых методов, которые вы вызываете. Они очень полезны для автоматической настройки данных для вас.

onConstruct(ActiveRecord $ActiveRecord, array &config)

Это очень полезно, если вам нужно установить соединение по умолчанию или что-то вроде того.

// index.php или bootstrap.php
Flight::register('db', 'PDO', [ 'sqlite:test.db' ]);

//
//
//

// User.php
class User extends flight\ActiveRecord {

    protected function onConstruct(self $self, array &$config) { // не забудьте ссылку &
        // вы могли бы сделать это для автоматической установки соединения
        $config['connection'] = Flight::db();
        // или это
        $self->transformAndPersistConnection(Flight::db());

        // Вы также можете установить имя таблицы таким образом.
        $config['table'] = 'users';
    } 
}

beforeFind(ActiveRecord $ActiveRecord)

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

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function beforeFind(self $self) {
        // всегда выполняйте id >= 0, если это ваш стиль
        $self->gte('id', 0); 
    } 
}

afterFind(ActiveRecord $ActiveRecord)

Этот, вероятно, более полезен, если вам всегда нужно выполнять некоторую логику каждый раз, когда эта запись извлекается. Нужно ли вам расшифровать что-то? Нужно ли выполнять пользовательский запрос подсчёта каждый раз (не производительно, но ладно)?

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function afterFind(self $self) {
        // расшифровка чего-то
        $self->secret = yourDecryptFunction($self->secret, $some_key);

        // возможно, хранение чего-то пользовательского, как запрос???
        $self->setCustomData('view_count', $self->select('COUNT(*) count')->from('user_views')->eq('user_id', $self->id)['count']; 
    } 
}

beforeFindAll(ActiveRecord $ActiveRecord)

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

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function beforeFindAll(self $self) {
        // всегда выполняйте id >= 0, если это ваш стиль
        $self->gte('id', 0); 
    } 
}

afterFindAll(array<int,ActiveRecord> $results)

Похоже на afterFind(), но вы можете сделать это для всех записей!

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function afterFindAll(array $results) {

        foreach($results as $self) {
            // сделайте что-то крутое, как afterFind()
        }
    } 
}

beforeInsert(ActiveRecord $ActiveRecord)

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

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function beforeInsert(self $self) {
        // установите некоторые разумные значения по умолчанию
        if(!$self->created_date) {
            $self->created_date = gmdate('Y-m-d');
        }

        if(!$self->password) {
            $self->password = password_hash((string) microtime(true));
        }
    } 
}

afterInsert(ActiveRecord $ActiveRecord)

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

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function afterInsert(self $self) {
        // делайте, что хотите
        Flight::cache()->set('most_recent_insert_id', $self->id);
        // или что угодно....
    } 
}

beforeUpdate(ActiveRecord $ActiveRecord)

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

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function beforeInsert(self $self) {
        // установите некоторые разумные значения по умолчанию
        if(!$self->updated_date) {
            $self->updated_date = gmdate('Y-m-d');
        }
    } 
}

afterUpdate(ActiveRecord $ActiveRecord)

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

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function afterInsert(self $self) {
        // делайте, что хотите
        Flight::cache()->set('most_recently_updated_user_id', $self->id);
        // или что угодно....
    } 
}

beforeSave(ActiveRecord $ActiveRecord)/afterSave(ActiveRecord $ActiveRecord)

Это полезно, если вы хотите, чтобы события происходили как при вставках, так и при обновлениях. Я пощажу вас от длинного объяснения, но уверен, вы можете угадать, что это такое.

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function beforeSave(self $self) {
        $self->last_updated = gmdate('Y-m-d H:i:s');
    } 
}

beforeDelete(ActiveRecord $ActiveRecord)/afterDelete(ActiveRecord $ActiveRecord)

Не уверен, что вы захотите здесь сделать, но никаких суждений! Действуйте!

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function beforeDelete(self $self) {
        echo 'He was a brave soldier... :cry-face:';
    } 
}

Управление соединением с базой данных

При использовании этой библиотеки вы можете установить соединение с базой данных несколькими способами. Вы можете установить соединение в конструкторе, вы можете установить его через переменную конфигурации $config['connection'] или вы можете установить его через setDatabaseConnection() (v0.4.1).

$pdo_connection = new PDO('sqlite:test.db'); // для примера
$user = new User($pdo_connection);
// или
$user = new User(null, [ 'connection' => $pdo_connection ]);
// или
$user = new User();
$user->setDatabaseConnection($pdo_connection);

Если вы хотите избежать установки $database_connection каждый раз при вызове active record, есть способы обойти это!

// index.php или bootstrap.php
// Установите это как зарегистрированный класс в Flight
Flight::register('db', 'PDO', [ 'sqlite:test.db' ]);

// User.php
class User extends flight\ActiveRecord {

    public function __construct(array $config = [])
    {
        $database_connection = $config['connection'] ?? Flight::db();
        parent::__construct($database_connection, 'users', $config);
    }
}

// И теперь аргументы не требуются!
$user = new User();

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

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

Вклад

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

Настройка

При вкладе убедитесь, что вы запускаете composer test-coverage для поддержания 100% покрытия тестами (это не настоящее покрытие unit-тестами, больше как интеграционное тестирование).

Также убедитесь, что вы запускаете composer beautify и composer phpcs для исправления любых ошибок линтинга.

Лицензия

MIT

Awesome-plugins/latte

Latte

Latte — это полнофункциональный шаблонизатор, который очень прост в использовании и ближе к синтаксису PHP, чем Twig или Smarty. Его также очень легко расширять и добавлять собственные фильтры и функции.

Установка

Установите с помощью composer.

composer require latte/latte

Базовая настройка

Есть несколько базовых опций настройки для начала работы. Подробнее о них можно прочитать в Документации Latte.


require 'vendor/autoload.php';

$app = Flight::app();

$app->map('render', function(string $template, array $data, ?string $block): void {
    $latte = new Latte\Engine;

    // Где latte специально хранит свой кэш
    $latte->setTempDirectory(__DIR__ . '/../cache/');

    $finalPath = Flight::get('flight.views.path') . $template;

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

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

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

<!-- app/views/layout.latte -->
<!doctype html>
<html lang="en">
    <head>
        <title>{$title ? $title . ' - '}My App</title>
        <link rel="stylesheet" href="style.css">
    </head>
    <body>
        <header>
            <nav>
                <!-- 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, а другие представляют собой микро/облегченные библиотеки, которые помогут вам начать работу.

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

Flight можно сделать еще круче с помощью плагинов на базе ИИ.

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

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

Мониторинг Производительности Приложений (APM)

Мониторинг производительности приложений (APM) необходим для любого приложения. Он помогает понять, как работает ваше приложение и где находятся узкие места. Существует ряд инструментов APM, которые можно использовать с Flight.

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

Flight и так быстрый фреймворк, но установка турбины делает все еще веселее (и сложнее)!

Авторизация/Разрешения

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

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

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

Кэширование

Кэширование — отличный способ ускорить ваше приложение. Существует ряд библиотек кэширования, которые можно использовать с Flight.

CLI

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

Cookies

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

Отладка

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

Базы Данных

Базы данных — основа большинства приложений. Это то, как вы храните и извлекаете данные. Некоторые библиотеки баз данных — это просто обертки для написания запросов, а некоторые — полноценные ORM.

Шифрование

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

Очередь Задач

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

Сессии

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

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

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

Интеграция с WordPress

Хотите использовать Flight в вашем проекте WordPress? Для этого есть удобный плагин!

Участие

Есть плагин, которым вы хотели бы поделиться? Отправьте pull request, чтобы добавить его в список!

Media

Медиа

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

Статьи и обзоры

Видео и уроки

Что-то упущено?

Мы пропустили что-то, что вы написали или записали? Дайте нам знать с помощью issue или pull request!

Examples

Нужен быстрый старт?

У вас есть два варианта для начала работы с новым проектом Flight:

Примеры, внесённые сообществом:

Нужен ли вам некоторый вдохновение?

Хотя эти примеры не спонсируются официально командой Flight, они могут дать вам идеи о том, как структурировать свои собственные проекты, построенные на Flight!

Хотите поделиться своим собственным примером?

Если у вас есть проект, которым вы хотите поделиться, пожалуйста, отправьте pull request, чтобы добавить его в этот список!

Install/install

Инструкции по установке

Перед установкой Flight необходимо выполнить несколько базовых предварительных требований. А именно, вам понадобится:

  1. Установите PHP на вашей системе
  2. Установите Composer для лучшего опыта разработчика.

Базовая установка

Если вы используете Composer, вы можете выполнить следующую команду:

composer require flightphp/core

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

Вы также можете скачать файлы напрямую и распаковать их в вашу веб-директорию.

Базовая установка отлично подходит для обучения, микросервисных API и экспериментов с копированием и вставкой. Для полной структуры приложения, которой одинаково легко будут следовать люди и AI-инструменты для кодирования, используйте рекомендуемый скелет ниже.

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

Настоятельно рекомендуется начинать любой новый проект с приложения flightphp/skeleton. Установка проста как ветерок.

composer create-project flightphp/skeleton my-project/
cd my-project/
composer start
# необязательная демо БД + посты
php runway migrate

Этот шаг настраивает структуру проекта, автозагрузку Composer PSR-4, конфигурацию и такие инструменты, как Tracy, Tracy Extensions и Runway. Он также включает корневой файл AGENTS.md (и локальные копии в app/), чтобы AI-ассистенты работали с той же структурой, что и вы — см. AI и опыт разработчика.

Что даёт скелет

project-root/
├── AGENTS.md              # источник истины для AI / агентов
├── SECURITY.md            # ожидания по безопасности
├── .env.example           # секреты / оверлеи деплоя (копируется в .env)
├── public/index.php       # только веб-вход
├── app/
│   ├── config/            # загрузка, маршруты, сервисы, config_sample.php
│   ├── Controller/        # App\Controller\*  (папка в PascalCase!)
│   ├── Middleware/        # App\Middleware\*
│   ├── Model/             # App\Model\* (ActiveRecord)
│   ├── Utils/             # Config, Env, DatabaseFactory
│   ├── commands/          # команды CLI Runway
│   ├── views/             # шаблоны Twig (*.twig)
│   ├── cache/
│   └── log/
├── migrations/            # SQL-миграции (.sql / .mysql.sql)
└── tests/                 # PHPUnit

Пространства имён соответствуют регистру папок. Composer сопоставляет "App\\": "app/", поэтому:

Путь на диске Пространство имён
app/Controller/HomeController.php App\Controller\HomeController
app/Middleware/… App\Middleware\…
app/Model/… App\Model\…
app/Utils/… App\Utils\…

В Linux app/controller/ не то же самое, что app/Controller/. Автозагрузка чувствительна к регистру — используйте папки в PascalCase, как в скелете. Подробнее: Автозагрузка.

Стандартный стек (новые проекты): представления Twig, SimplePdo + ActiveRecord, Dice с внедрением Engine (предпочтительно без Flight:: внутри классов приложения), опционально SQLite после php runway migrate.

create-project обычно копирует app/config/config_sample.phpconfig.php и .env.example.env, если они присутствуют. Маршруты находятся в app/config/routes.php; сервисы и DI — в app/config/services.php.

Документация ↔ скелет: Эта документация описывает Flight API (часто с короткими примерами Flight::). Скелет определяет структуру приложения. При добавлении кода в app/ следуйте структуре скелета; используйте документацию для названий методов, опций и плагинов.

Настройка вашего веб-сервера

Встроенный веб-сервер PHP

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

php -S localhost:8000
# или с приложением-скелетом
composer start

Затем откройте браузер и перейдите по адресу http://localhost:8000.

Если вы хотите сделать корневую директорию вашего проекта иной (например, ваш проект находится в ~/myproject, а корневая директория — ~/myproject/public/), вы можете выполнить следующую команду, находясь в директории ~/myproject:

php -S localhost:8000 -t public/
# в приложении-скелете это уже настроено
composer start

Затем откройте браузер и перейдите по адресу http://localhost:8000.

Apache

Убедитесь, что Apache уже установлен в вашей системе. Если нет, поищите в Google, как установить Apache в вашей системе.

Для Apache отредактируйте ваш файл .htaccess, добавив следующее:

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [QSA,L]

Примечание: Если вам нужно использовать Flight в поддиректории, добавьте строку RewriteBase /subdir/ сразу после RewriteEngine On.

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

RewriteEngine On
RewriteRule ^(.*)$ index.php

Nginx

Убедитесь, что Nginx уже установлен в вашей системе. Если нет, поищите в Google, как установить Nginx в вашей системе.

Для Nginx добавьте следующее в объявление вашего сервера:

server {
  location / {
    try_files $uri $uri/ /index.php;
  }
}

Создайте ваш файл index.php

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

<?php

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

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

// Наконец, запустите фреймворк.
Flight::start();

В приложении-скелете публичная точка входа только запускает приложение. Маршруты регистрируются в app/config/routes.php (обычно в виде [App\Controller\…::class, 'method'], чтобы Dice мог внедрять зависимости). Сервисы, Twig, SimplePdo и контейнер настраиваются в app/config/services.php. Такая структура выбрана осознанно, чтобы AI-инструменты и люди редактировали одни и те же места каждый раз.

Установка PHP {#installing-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-файла для не многопоточной (Non-Thread-Safe) сборки.
  2. Распакуйте PHP:

    • Извлеките загруженный zip-файл в C:\php.
  3. Добавьте PHP в системный PATH:

    • Перейдите в Свойства системы > Переменные среды.
    • В разделе Системные переменные найдите Path и нажмите Изменить.
    • Добавьте путь C:\php (или туда, куда вы извлекли PHP).
    • Нажмите ОК, чтобы закрыть все окна.
  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, etc.)

Установка 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:

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