Learn/flight_vs_laravel
Flight vs Laravel
Что такое Laravel?
Laravel — это полнофункциональный фреймворк, который имеет все возможные функции и удивительную экосистему, ориентированную на разработчиков, но за счет производительности и сложности. Цель Laravel — обеспечить разработчику наивысший уровень производительности и сделать распространенные задачи простыми. Laravel — отличный выбор для разработчиков, которые хотят построить полнофункциональное корпоративное веб-приложение. Это влечет за собой некоторые компромиссы, в частности в плане производительности и сложности. Освоение основ Laravel может быть простым, но достижение мастерства в фреймворке может занять некоторое время.
Также существует множество модулей Laravel, из-за чего разработчики часто чувствуют, что единственный способ решить проблемы — это через эти модули, хотя на самом деле можно просто использовать другую библиотеку или написать свой собственный код.
Преимущества по сравнению с Flight
- Laravel имеет огромную экосистему разработчиков и модулей, которые можно использовать для решения распространенных проблем.
- Laravel имеет полнофункциональный ORM, который можно использовать для взаимодействия с вашей базой данных.
- Laravel имеет огромное количество документации и руководств, которые можно использовать для изучения фреймворка. Это может быть полезно для глубокого погружения в детали или плохо, потому что приходится проходить через слишком много материала.
- Laravel имеет встроенную систему аутентификации, которую можно использовать для обеспечения безопасности вашего приложения.
- Laravel имеет подкасты, конференции, встречи, видео и другие ресурсы, которые можно использовать для изучения фреймворка.
- Laravel ориентирован на опытного разработчика, который хочет построить полнофункциональное корпоративное веб-приложение.
Недостатки по сравнению с Flight
- Laravel имеет гораздо больше происходящего под капотом, чем Flight. Это влечет за собой драматические затраты в плане производительности. См. TechEmpower benchmarks для получения дополнительной информации.
- Flight ориентирован на разработчика, который хочет построить легковесное, быстрое и простое в использовании веб-приложение.
- Flight ориентирован на простоту и удобство использования.
- Одна из ключевых особенностей Flight — это то, что он старается поддерживать обратную совместимость. Laravel вызывает много раздражения между основными версиями.
- Flight предназначен для разработчиков, которые впервые вступают в мир фреймворков.
- Flight не имеет зависимостей, в то время как Laravel имеет отвратительное количество зависимостей
- Flight также может создавать приложения корпоративного уровня, но у него не так много шаблонного кода, как у Laravel. Кроме того, потребуется больше дисциплины со стороны разработчика, чтобы поддерживать порядок и хорошую структуру.
- Flight дает разработчику больше контроля над приложением, в то время как Laravel имеет множество магических функций за кулисами, которые могут раздражать.
Learn/migrating_to_v3
Миграция на v3
Обратная совместимость в основном сохранена, но есть некоторые изменения, о которых вы должны знать при миграции с v2 на v3. Некоторые изменения слишком сильно конфликтовали с шаблонами проектирования, поэтому пришлось внести корректировки.
Поведение буферизации вывода
v3.5.0
Буферизация вывода — это процесс, при котором вывод, генерируемый PHP-скриптом, хранится в буфере (внутреннем для PHP), прежде чем будет отправлен клиенту. Это позволяет модифицировать вывод перед его отправкой клиенту.
В MVC-приложении контроллер является "менеджером" и управляет тем, что делает представление. Генерация вывода вне контроллера (или в случае Flight иногда анонимной функцией) нарушает шаблон MVC. Это изменение сделано для большей соответствия шаблону MVC и чтобы сделать фреймворк более предсказуемым и удобным в использовании.
В v2 буферизация вывода обрабатывалась таким образом, что она не всегда последовательно закрывала свой буфер вывода, что затрудняло юнит-тестирование и потоковую передачу. Для большинства пользователей это изменение может не повлиять на вас. Однако, если вы выводите содержимое вне вызываемых функций и контроллеров (например, в хуке), вы, вероятно, столкнетесь с проблемами. Вывод содержимого в хуках и до фактического выполнения фреймворка мог работать в прошлом, но впредь работать не будет.
Где вы можете столкнуться с проблемами
// index.php
require 'vendor/autoload.php';
// just an example
define('START_TIME', microtime(true));
function hello() {
echo 'Hello World';
}
Flight::map('hello', 'hello');
Flight::after('hello', function(){
// this will actually be fine
echo '<p>This Hello World phrase was brought to you by the letter "H"</p>';
});
Flight::before('start', function(){
// things like this will cause an error
echo '<html><head><title>My Page</title></head><body>';
});
Flight::route('/', function(){
// this is actually just fine
echo 'Hello World';
// This should be just fine as well
Flight::hello();
});
Flight::after('start', function(){
// this will cause an error
echo '<div>Your page loaded in '.(microtime(true) - START_TIME).' seconds</div></body></html>';
});
Включение поведения рендеринга v2
Можете ли вы оставить свой старый код как есть, без переписывания для работы с v3? Да, можете! Вы можете включить поведение рендеринга v2, установив опцию конфигурации flight.v2.output_buffering в true. Это позволит вам продолжать использовать старое поведение рендеринга, но рекомендуется исправить это в будущем. В v4 фреймворка это будет удалено.
// index.php
require 'vendor/autoload.php';
Flight::set('flight.v2.output_buffering', true);
Flight::before('start', function(){
// Now this will be just fine
echo '<html><head><title>My Page</title></head><body>';
});
// more code
Изменения в диспетчере
v3.7.0
Если вы напрямую вызывали статические методы для Dispatcher, такие как Dispatcher::invokeMethod(), Dispatcher::execute() и т.д., вам нужно обновить свой код, чтобы не вызывать эти методы напрямую. Dispatcher был преобразован для большей объектно-ориентированности, чтобы контейнеры внедрения зависимостей можно было использовать проще. Если вам нужно вызвать метод аналогично тому, как это делал Dispatcher, вы можете вручную использовать что-то вроде $result = $class->$method(...$params); или call_user_func_array() вместо этого.
Изменения в halt() stop() redirect() и error()
v3.10.0
Поведение по умолчанию до 3.10.0 заключалось в очистке как заголовков, так и тела ответа. Это было изменено на очистку только тела ответа. Если вам также нужно очистить заголовки, вы можете использовать Flight::response()->clear().
Learn/configuration
Конфигурация
Обзор
Flight предоставляет простой способ настройки различных аспектов фреймворка в соответствии с потребностями вашего приложения. Некоторые настройки установлены по умолчанию, но вы можете переопределить их по мере необходимости. Вы также можете устанавливать собственные переменные для использования в вашем приложении.
Четкая многоуровневая конфигурация (значения по умолчанию в файле + секреты из окружения) также помогает инструментам ИИ для кодирования: агенты узнают одно место для литералов и одно место для секретов, вместо того чтобы изобретать чтение $_ENV внутри контроллеров.
Понимание
Вы можете настроить определенное поведение Flight, установив значения конфигурации через метод set.
Flight::set('flight.log_errors', true);
В структурированном приложении (включая skeleton) вы обычно загружаете настройки проекта из app/config/config.php, а затем применяете соответствующие ключи к Engine (например, flight.base_url, flight.views.path). Вы также можете внедрить небольшой объект конфигурации в контроллеры вместо чтения глобальных переменных повсюду — это удобнее для тестов и для агентов, следующих AGENTS.md.
Базовое использование
Параметры конфигурации Flight
Ниже приведен список всех доступных параметров конфигурации:
- flight.base_url
?string— переопределяет базовый URL запроса, если Flight работает в подкаталоге. (по умолчанию: null) - flight.case_sensitive
bool— чувствительное к регистру сопоставление URL. (по умолчанию: false) - flight.handle_errors
bool— разрешить Flight обрабатывать все ошибки внутренне. (по умолчанию: true) - flight.log_errors
bool— сохранять ошибки в файл журнала ошибок веб-сервера. (по умолчанию: false)- Если у вас установлен Tracy, Tracy будет логировать ошибки в соответствии с настройками Tracy, а не этой конфигурацией.
- flight.debug
bool— выводить подробную информацию об ошибке (сообщение исключения, код и трассировку стека) в браузере при возникновении ошибки. (по умолчанию: false)- Никогда не включайте это в продакшене — это раскрывает внутренние детали приложения. Используйте только для локальной разработки или промежуточного окружения.
- При значении
falseвместо этого показывается общий ответ500 Internal Server Error. Используйте вместе сflight.log_errorsдля регистрации ошибок на стороне сервера.
- flight.allow_method_override
bool— разрешить переопределение HTTP-метода через заголовок запросаX-HTTP-Method-Overrideили поле_methodв теле POST. (по умолчанию: true)- Рекомендуется установить значение
falseдля приложений, которым не нужна подмена методов на основе HTML-форм, поскольку это предотвращает подделку клиентами запросовDELETEилиPUTчерез стандартную POST-форму. - См. Безопасность для получения дополнительной информации.
- Рекомендуется установить значение
- flight.views.path
string— каталог, содержащий файлы шаблонов представлений. (по умолчанию: ./views) - flight.views.extension
string— расширение файла шаблона представления. (по умолчанию:.php; официальный skeleton устанавливает.twigпри использовании Twig) - flight.content_length
bool— устанавливать заголовокContent-Length. (по умолчанию: true)- Если вы используете Tracy, это значение должно быть false, чтобы Tracy мог корректно отображать содержимое.
- flight.v2.output_buffering
bool— использовать устаревшую буферизацию вывода. См. миграцию на v3. (по умолчанию: false)
Конфигурация загрузчика
Существует также дополнительный параметр конфигурации для загрузчика. Он позволяет автоматически загружать классы с символом _ в имени класса.
// Включить загрузку классов с подчеркиваниями
// По умолчанию true
Loader::$v2ClassLoading = false;
Помните, что автозагрузка также зависит от регистра папок, соответствующего вашим пространствам имен — особенно с макетом App\ + app/Controller/ в skeleton.
Конфигурация проекта и .env (паттерн skeleton)
Ядро Flight не требует файлов .env. Многие приложения используют только массив конфигурации PHP. Официальный skeleton наслаивает конфигурацию так, чтобы секреты не попадали в git, а Runway мог безопасно перезаписывать литеральную конфигурацию:
.env/ реальное окружение — секреты и переопределения для деплоя (игнорируется git).app/config/config.php— литеральные значения PHP-массива по умолчанию (скопированы изconfig_sample.php). Предпочтительно не использовать выражения$_ENV[...]внутри этого файла: такие инструменты, какrunway config:set, могут перезаписать его статическими значениями и встроить секреты в файл.- Слияние при загрузке — env побеждает для сопоставленных ключей; код приложения читает объект конфигурации или
$app->get(), а не$_ENVв контроллерах.
Пример структуры config_sample.php / config.php (упрощенно):
<?php
// Только литералы — секреты принадлежат .env для рабочего процесса skeleton
return [
'app' => [
'env' => 'development',
'debug' => true,
'base_url' => '/',
'timezone' => 'UTC',
],
'database' => [
'driver' => 'sqlite', // или mysql, или '' для отключения
'host' => 'localhost',
'dbname' => '',
'user' => '',
'password' => '',
'file_path' => __DIR__ . '/../../database.sqlite',
],
// ...
];
# .env.example → .env (skeleton)
APP_ENV=development
APP_DEBUG=true
FLIGHT_BASE_URL=/
DB_DRIVER=sqlite
# DB_PASSWORD=...
Это разделение намеренно для проектов, дружественных к ИИ: инструкции могут говорить «значения по умолчанию в 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 () {
// Обработка не найденного
});
См. также
- Установка — Конфигурация skeleton,
.envи структура загрузки. - Автозагрузка — Пространства имен и регистр папок.
- Расширение Flight — Как расширять и настраивать основные функции Flight.
- Модульное тестирование — Как писать модульные тесты для вашего приложения Flight.
- ИИ и опыт разработчика —
AGENTS.mdи последовательные инструкции для проекта. - Tracy — Плагин для расширенной обработки ошибок и отладки.
- Расширения Tracy — Расширения для интеграции Tracy с Flight.
- APM — Плагин для мониторинга производительности приложений и отслеживания ошибок.
- Безопасность — Флаги усиления защиты и обработка секретов.
Устранение неполадок
- Если у вас возникли проблемы с поиском всех значений вашей конфигурации, вы можете выполнить
var_dump(Flight::get()); - Если Runway или инструменты развертывания перезаписали
config.php, убедитесь, что секреты не были закоммичены — храните их в.envили в реальном окружении при использовании паттерна skeleton.
Журнал изменений
- Документация — документировано наслоение конфигурации в стиле skeleton /
.envи расширение представлений Twig по умолчанию для новых проектов. - v3.18.1 — Добавлены параметры конфигурации
flight.debugиflight.allow_method_override. - v3.5.0 — Добавлена конфигурация
flight.v2.output_bufferingдля поддержки устаревшего поведения буферизации вывода. - v2.0 — Добавлены основные конфигурации.
Learn/ai
AI и опыт разработчика с Flight
Обзор
Flight разработан для работы с AI-инструментами кодирования, а не против них. Небольшой предсказуемый API, понятная структура приложения в официальном скелете и файлы с инструкциями для конкретного проекта означают, что ассистенты вроде GitHub Copilot, Cursor, Windsurf, Claude Code и Gemini могут следовать тем же паттернам, которые вы пишете вручную.
Встроенные команды Runway для подключения к LLM-провайдерам и генерации инструкций проекта помогают вам и вашей команде получать последовательную и релевантную помощь, не вставляя один и тот же контекст в каждый чат.
Понимание
AI-ассистенты наиболее полезны, когда они понимают контекст, соглашения и цели вашего проекта. AI-помощники Flight позволяют вам:
- Подключать ваш проект к популярным LLM-провайдерам (OpenAI, Grok, Claude и т.д.)
- Генерировать и обновлять инструкции для конкретного проекта, чтобы все получали одни и те же рекомендации
- Сохранять единую структуру для кода, написанного вручную и сгенерированного AI (особенно со скелетом)
Эти функции поставляются с основным CLI Flight (через Runway) и уже подключены в официальном стартовом проекте flightphp/skeleton.
Что скелет включает для AI
Официальный стартовый проект рассматривает AGENTS.md как источник истины для AI-инструментов:
| Файл | Роль |
|---|---|
AGENTS.md (корень проекта) |
Глобальные правила, поток загрузки, пространства имен, DI, «чего не делать» |
Локальные AGENTS.md в app/, migrations/, tests/ и т.д. |
Легкие подсказки для конкретной папки при работе в этом дереве |
SECURITY.md |
Секреты, заголовки, XSS/SQL, отчеты — безопасность остается осознанной и отдельной |
В скелете нет отдельного файла стиля для Copilot / Cursor / Gemini / Windsurf. Укажите вашему ассистенту на корневой AGENTS.md (и позвольте ему переходить по ссылкам на локальные файлы). Люди могут полностью игнорировать эти файлы и использовать README; структура в любом случае одинакова.
Документация учит API; скелет учит структуре. Короткие примеры
Flight::в этих документах отлично подходят для обучения. В приложении на скелете предпочитайте классыApp\…, внедрение зависимостей через конструктор и$this->appвместо статического фасада в контроллерах. См. Установка и Автозагрузка.
Базовое использование
Настройка учетных данных LLM
Команда ai:init проведет вас через подключение проекта к LLM-провайдеру.
php runway ai:init
Вам будет предложено:
- Выбрать провайдера (OpenAI, Grok, Claude и т.д.)
- Ввести ваш API-ключ
- Установить базовый URL и имя модели
Это создает учетные данные, используемые для последующих LLM-запросов (например, для генерации инструкций).
Пример:
Добро пожаловать в 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-провайдера для генерации инструкций и записывает их в первую очередь в:
AGENTS.mdв корне проекта (не зависит от инструмента; именно этого ожидают официальный скелет и большинство современных агентов)
В зависимости от версии CLI и параметров команда также может записывать копии для конкретных инструментов в старых рабочих процессах (например, файлы правил Copilot, Cursor, Windsurf или Gemini). Для новых проектов на основе скелета рассматривайте AGENTS.md (а также любые локальные файлы AGENTS.md, которые вы храните в app/) как единственный источник истины — не поддерживайте пять расходящихся файлов инструкций вручную.
Пример:
Опишите, для чего ваш проект? Мой потрясающий API
Какую базу данных вы планируете использовать? MySQL
Какой HTML-шаблонизатор вы планируете использовать (если есть)? twig
Является ли безопасность важным элементом этого проекта? (y/n) y
...
AI-инструкции успешно обновлены.
Теперь AI-инструменты могут предлагать код, соответствующий вашему реальному стеку и структуре, а не общему учебнику по PHP.
Продвинутое использование
- Настройте учетные данные или пути вывода с помощью параметров команд (см.
--helpдля каждой команды). - Помощники работают с любым LLM-провайдером, поддерживающим OpenAI-совместимый API.
- Повторно запускайте
ai:generate-instructionsпо мере развития проекта, чтобы агенты оставались в синхронизации. - В скелете храните политику безопасности в
SECURITY.md, а структуру кода — вAGENTS.md, чтобы ни один документ не превращался в свалку. - Предпочитайте docs.flightphp.com и Flight MCP сервер, когда агентам нужны детали API; проверяйте выдуманные методы на соответствие
vendor/flightphp/core.
Смотрите также
- Flight Skeleton – Официальный стартовый проект с
AGENTS.md, Twig, SimplePdo и Dice, настроенный для AI-дружественной структуры - Установка – Рекомендуемая структура
create-project - Автозагрузка – Регистр папок соответствует пространствам имен (
App\Controller↔app/Controller/) - Runway CLI – CLI, обеспечивающий работу команд
ai:*и генерацию каркаса - Безопасность – Безопасные настройки по умолчанию, которые агенты (и люди) не должны ослаблять
Устранение неполадок
- Если вы видите «Missing .runway-creds.json», сначала запустите
php runway ai:init. - Убедитесь, что ваш API-ключ действителен и имеет доступ к выбранной модели.
- Если инструкции не обновляются, проверьте права доступа к файлам в каталоге проекта.
- Если агент выдумывает API Flight или неправильную структуру папок, укажите ему на корневой
AGENTS.mdи этот сайт документации; структура скелета является определяющей для кода вapp/.
Журнал изменений
- v3.18.4 –
ai:generate-instructionsзаписывает инструкции проекта вAGENTS.mdв корне проекта. - v3.16.0 – Добавлены команды CLI
ai:initиai:generate-instructionsдля интеграции с AI.
Learn/unit_testing_and_solid_principles
Эта статья была 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 каждый раз, когда мы вызываем функцию, связанную с датой/временем. Вот некоторые рекомендуемые настройки:
- date.timezone - выберите из списка поддерживаемых временных зон
- session.savepath - если мы используем файлы для сессий и не какой-либо другой обработчик сохранения, установите это вне /tmp. Оставление этого как /tmp может быть рискованным в среде общего хостинга, поскольку /tmp_ обычно имеет широкие разрешения. Даже с установленным битом sticky, любой, у кого есть доступ к перечислению содержимого этой директории, может узнать все ваши активные идентификаторы сессий.
- session.cookie_secure - очевидно, включите это, если вы обслуживаете код PHP по HTTPS.
- session.cookie_httponly - установите это, чтобы предотвратить доступ к cookie сессий PHP через JavaScript
- Ещё... используйте инструмент вроде iniscan, чтобы проверить вашу конфигурацию на наличие распространенных уязвимостей
1.3 Расширения
Также хорошей идеей является отключение (или хотя бы не включение) расширений, которые вы не будете использовать, как драйверы баз данных. Чтобы увидеть, что включено, запустите команду phpinfo() или перейдите в командную строку и запустите это.
$ php -i
Информация та же, но phpinfo() добавляет HTML-форматирование. Версия CLI проще для перенаправления в grep для поиска конкретной информации. Прим.
$ php -i | grep error_log
Один нюанс этого метода: возможно, что разные настройки PHP применяются к веб-ориентированной версии и версии CLI.
2 Используйте Composer
Это может показаться сюрпризом, но одна из лучших практик для написания современного PHP - писать меньше кода. Хотя правда, что один из лучших способов освоить программирование - делать это, существует множество уже решенных проблем в пространстве PHP, таких как маршрутизация, базовые библиотеки проверки ввода, преобразование единиц, слои абстракции баз данных и т.д... Просто перейдите на Packagist и посмотрите. Вы, вероятно, обнаружите, что значительная часть проблемы, которую вы пытаетесь решить, уже написана и протестирована.
Хотя 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 тесты делят много следующих характеристик:
- Быстрые - должны запускаться в миллисекундах.
- Без доступа к сети - должны уметь отключить беспроводную связь/вытащить шнур и все тесты все равно проходят.
- Ограниченный доступ к файловой системе - это добавляет к скорости и гибкости при развертывании кода в другие среды.
- Без доступа к базе данных - избегает costly setup и teardown деятельности.
- Тестировать только одну вещь за раз - unit тест должен иметь только одну причину для неудачи.
- Хорошо названные - см. 5.2 выше.
- В основном фейковые объекты - единственные "реальные" объекты в unit тестах должны быть объектом, который мы тестируем, и простыми объектами значений. Остальное должно быть какой-то формой test double
Есть причины пойти против некоторых из них, но как общие рекомендации они послужат вам хорошо.
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.
Понимание
Существует ряд распространённых угроз безопасности, о которых следует знать при создании веб-приложений. Некоторые из наиболее распространённых угроз включают:
- Межсайтовая подделка запросов (CSRF)
- Межсайтовый скриптинг (XSS)
- SQL-инъекции
- Совместное использование ресурсов между источниками (CORS)
Шаблоны помогают с 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);
// Это выведет: <script>alert("XSS")</script>
// 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); // этот выведет всех пользователей в базе данных, а не только одно имя пользователя
Секреты и конфигурация
- Помещайте секреты в
.env(или в реальное окружение), а не в коммитируемые образцыconfig.php. - Правило скелета: буквальные значения по умолчанию в
config.php; объединяйте окружение в bootstrap; не читайте$_ENVвнутри контроллеров — вместо этого внедряйте конфигурацию. См. Конфигурация. - Никогда не коммитьте API-ключи, пароли БД или ключи шифрования сессий. Направляйте ИИ-инструменты на
SECURITY.md, чтобы они не придумывали небезопасные обходные пути.
Проверка 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 секунд
});
Смотрите также
- Сессии — Как безопасно управлять пользовательскими сессиями.
- Шаблоны — Twig/Latte авто-экранирование и XSS.
- SimplePdo — Помощники для работы с БД с подготовленными запросами.
- PdoWrapper — Устарел; используйте SimplePdo для нового кода.
- Middleware — Как использовать middleware для упрощения процесса добавления заголовков безопасности.
- Конфигурация —
.envпротив буквальной конфигурации, production-флаги. - AI и опыт разработчика — Держите политику безопасности в
SECURITY.mdдля агентов. - Ответы — Как настраивать HTTP-ответы с безопасными заголовками.
- Запросы — Как обрабатывать и санитизировать пользовательский ввод.
- filter_var — PHP-функция для санитизации ввода.
- password_hash — PHP-функция для безопасного хеширования паролей.
- password_verify — PHP-функция для проверки хешированных паролей.
Устранение неполадок
- Обратитесь к разделу «Смотрите также» выше для получения информации по устранению неполадок, связанных с компонентами Flight Framework.
- Если CSP блокирует ваши скрипты, добавьте nonce (паттерн скелета) или добавьте конкретные источники в белый список — не устанавливайте
script-src *без плана.
Журнал изменений
- Документация — скелет
App\Middleware, заметки по Twig CSRF/XSS, SimplePdo, секреты/.envиSECURITY.mdдля AI-ориентированных проектов. - v3.18.1 — Добавлен раздел усиления конфигурации Flight, охватывающий
flight.allow_method_override,flight.debugи проверку JSONP-колбэка. - v3.1.0 — Добавлены разделы о CORS, обработке ошибок, санитизации ввода, хешировании паролей и ограничении частоты запросов.
- v2.0 — Добавлено экранирование для стандартных представлений для предотвращения XSS.
Learn/routing
Маршрутизация
Обзор
Маршрутизация во Flight PHP сопоставляет URL-шаблоны с функциями обратного вызова или методами классов, обеспечивая быструю и простую обработку запросов. Она спроектирована с минимальными накладными расходами, удобна для начинающих и расширяема без внешних зависимостей.
Понимание
Маршрутизация — это основной механизм, который связывает HTTP-запросы с логикой вашего приложения во Flight. Определяя маршруты, вы указываете, как разные URL-адреса запускают конкретный код — через функции, методы классов или действия контроллеров. Система маршрутизации Flight гибкая: она поддерживает базовые шаблоны, именованные параметры, регулярные выражения и расширенные возможности, такие как внедрение зависимостей и ресурсная маршрутизация. Такой подход поддерживает организованность и простоту сопровождения кода, оставаясь быстрым и простым для новичков и расширяемым для продвинутых пользователей.
Примечание: Хотите узнать больше о маршрутизации? Посмотрите страницу "зачем нужен фреймворк?" для более подробного объяснения.
Базовое использование
Определение простого маршрута
Базовая маршрутизация во Flight выполняется путём сопоставления URL-шаблона с функцией обратного вызова или массивом из класса и метода.
Flight::route('/', function(){
echo 'hello world!';
});
Маршруты сопоставляются в порядке их определения. Будет вызван первый маршрут, соответствующий запросу.
Использование функций в качестве обратных вызовов
Обратный вызов может быть любым вызываемым объектом. Таким образом, вы можете использовать обычную функцию:
function hello() {
echo 'hello world!';
}
Flight::route('/', 'hello');
Использование классов и методов в качестве контроллера
Вы также можете использовать метод класса (статический или обычный):
class GreetingController {
public function hello() {
echo 'hello world!';
}
}
Flight::route('/', [ 'GreetingController','hello' ]);
// или
Flight::route('/', [ GreetingController::class, 'hello' ]); // предпочтительный способ
// или
Flight::route('/', [ 'GreetingController::hello' ]);
// или
Flight::route('/', [ 'GreetingController->hello' ]);
Или создав объект сначала, а затем вызвав метод:
use flight\Engine;
// GreetingController.php
class GreetingController
{
protected Engine $app
public function __construct(Engine $app) {
$this->app = $app;
$this->name = 'John Doe';
}
public function hello() {
echo "Hello, {$this->name}!";
}
}
// index.php
$app = Flight::app();
$greeting = new GreetingController($app);
Flight::route('/', [ $greeting, 'hello' ]);
Примечание: По умолчанию, когда контроллер вызывается внутри фреймворка, класс
flight\Engineвсегда внедряется, если вы не укажете иное через контейнер внедрения зависимостей.
Маршрутизация по конкретным методам
По умолчанию шаблоны маршрутов сопоставляются со всеми методами запросов. Вы можете отвечать на конкретные методы, размещая идентификатор перед URL.
Flight::route('GET /', function () {
echo 'I received a GET request.';
});
Flight::route('POST /', function () {
echo 'I received a POST request.';
});
// Вы не можете использовать Flight::get() для маршрутов, так как это метод
// для получения переменных, а не для создания маршрута.
Flight::post('/', function() { /* код */ });
Flight::patch('/', function() { /* код */ });
Flight::put('/', function() { /* код */ });
Flight::delete('/', function() { /* код */ });
Вы также можете сопоставить несколько методов с одним обратным вызовом, используя разделитель |:
Flight::route('GET|POST /', function () {
echo 'I received either a GET or a POST request.';
});
Особая обработка HEAD и OPTIONS запросов
Flight предоставляет встроенную обработку для HTTP-запросов HEAD и OPTIONS:
HEAD-запросы
- HEAD-запросы обрабатываются так же, как
GET-запросы, но Flight автоматически удаляет тело ответа перед отправкой клиенту. - Это означает, что вы можете определить маршрут для
GET, и HEAD-запросы к тому же URL будут возвращать только заголовки (без содержимого), как того требуют стандарты HTTP.
Flight::route('GET /info', function() {
echo 'This is some info!';
});
// HEAD-запрос к /info вернёт те же заголовки, но без тела.
OPTIONS-запросы
OPTIONS-запросы автоматически обрабатываются Flight для любого определённого маршрута.
- Когда получен OPTIONS-запрос, Flight отвечает со статусом
204 No Contentи заголовкомAllow, перечисляющим все поддерживаемые HTTP-методы для этого маршрута. - Вам не нужно определять отдельный маршрут для OPTIONS.
// Для маршрута, определённого как:
Flight::route('GET|POST /users', function() { /* ... */ });
// OPTIONS-запрос к /users ответит:
//
// Статус: 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');
});
Просмотр информации о маршруте
Если вы хотите просмотреть информацию о сопоставленном маршруте, есть два способа:
- Используйте свойство
executedRouteна объектеFlight::router(). - Вы можете запросить передачу объекта маршрута в ваш обратный вызов, передав
trueв качестве третьего параметра в методе маршрута. Объект маршрута всегда будет последним параметром, переданным в вашу функцию обратного вызова.
executedRoute
Flight::route('/', function() {
$route = Flight::router()->executedRoute;
// Сделать что-то с $route
// Массив сопоставленных HTTP-методов
$route->methods;
// Массив именованных параметров
$route->params;
// Соответствующее регулярное выражение
$route->regex;
// Содержит содержимое любого '*', использованного в URL-шаблоне
$route->splat;
// Показывает путь URL... если вам действительно это нужно
$route->pattern;
// Показывает, какое промежуточное ПО назначено этому маршруту
$route->middleware;
// Показывает псевдоним, назначенный этому маршруту
$route->alias;
});
Примечание: Свойство
executedRouteбудет установлено только после выполнения маршрута. Если вы попытаетесь получить к нему доступ до выполнения маршрута, оно будетNULL. Вы также можете использоватьexecutedRouteв промежуточном ПО!
Передача true в определение маршрута
Flight::route('/', function(\flight\net\Route $route) {
// Массив сопоставленных HTTP-методов
$route->methods;
// Массив именованных параметров
$route->params;
// Соответствующее регулярное выражение
$route->regex;
// Содержит содержимое любого '*', использованного в URL-шаблоне
$route->splat;
// Показывает путь URL... если вам действительно это нужно
$route->pattern;
// Показывает, какое промежуточное ПО назначено этому маршруту
$route->middleware;
// Показывает псевдоним, назначенный этому маршруту
$route->alias;
}, true); // <-- Этот параметр true обеспечивает это
Группировка маршрутов и промежуточное ПО
Могут быть случаи, когда вы хотите сгруппировать связанные маршруты (например, /api/v1).
Это можно сделать с помощью метода group:
Flight::group('/api/v1', function () {
Flight::route('/users', function () {
// Соответствует /api/v1/users
});
Flight::route('/posts', function () {
// Соответствует /api/v1/posts
});
});
Вы даже можете вкладывать группы в группы:
Flight::group('/api', function () {
Flight::group('/v1', function () {
// Flight::get() получает переменные, а не устанавливает маршрут! Смотрите объектный контекст ниже
Flight::route('GET /users', function () {
// Соответствует GET /api/v1/users
});
Flight::post('/posts', function () {
// Соответствует POST /api/v1/posts
});
Flight::put('/posts/1', function () {
// Соответствует PUT /api/v1/posts
});
});
Flight::group('/v2', function () {
// Flight::get() получает переменные, а не устанавливает маршрут! Смотрите объектный контекст ниже
Flight::route('GET /users', function () {
// Соответствует GET /api/v2/users
});
});
});
Группировка с объектным контекстом
Вы всё ещё можете использовать группировку маршрутов с объектом Engine следующим образом:
$app = Flight::app();
$app->group('/api/v1', function (Router $router) {
// используем переменную $router
$router->get('/users', function () {
// Соответствует GET /api/v1/users
});
$router->post('/posts', function () {
// Соответствует POST /api/v1/posts
});
});
Примечание: Это предпочтительный способ определения маршрутов и групп с объектом
$router.
Группировка с промежуточным ПО
Вы также можете назначить промежуточное ПО группе маршрутов:
Flight::group('/api/v1', function () {
Flight::route('/users', function () {
// Соответствует /api/v1/users
});
}, [ MyAuthMiddleware::class ]); // или [ new MyAuthMiddleware() ], если вы хотите использовать экземпляр
Подробнее на странице промежуточное ПО для групп.
Ресурсная маршрутизация
Вы можете создать набор маршрутов для ресурса с помощью метода resource. Это создаст набор маршрутов для ресурса, следующих RESTful-соглашениям.
Чтобы создать ресурс, сделайте следующее:
Flight::resource('/users', UsersController::class);
И что произойдёт в фоновом режиме, так это создание следующих маршрутов:
[
'index' => 'GET /users',
'create' => 'GET /users/create',
'store' => 'POST /users',
'show' => 'GET /users/@id',
'edit' => 'GET /users/@id/edit',
'update' => 'PUT /users/@id',
'destroy' => 'DELETE /users/@id'
]
Ваш контроллер будет использовать следующие методы:
class UsersController
{
public function index(): void
{
}
public function show(string $id): void
{
}
public function create(): void
{
}
public function store(): void
{
}
public function edit(string $id): void
{
}
public function update(string $id): void
{
}
public function destroy(string $id): void
{
}
}
Примечание: Вы можете просмотреть добавленные маршруты с помощью
runway, выполнивphp runway routes.
Настройка ресурсных маршрутов
Есть несколько параметров для настройки ресурсных маршрутов.
Базовый псевдоним (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
]);
Смотрите также
- Промежуточное ПО — Использование промежуточного ПО с маршрутами для аутентификации, логирования и т. д.
- Внедрение зависимостей — Упрощение создания и управления объектами в маршрутах.
- Зачем нужен фреймворк? — Понимание преимуществ использования такого фреймворка, как Flight.
- Расширение — Как расширить Flight с помощью собственных функций, включая метод
notFound. - php.net: preg_match — PHP-функция для сопоставления регулярных выражений.
Устранение неполадок
- Параметры маршрута сопоставляются по порядку, а не по имени. Убедитесь, что порядок параметров в функции обратного вызова соответствует определению маршрута.
- Использование
Flight::get()не определяет маршрут; для маршрутизации используйтеFlight::route('GET /...')или объект Router в группах (например,$router->get(...)). - Свойство
executedRouteустанавливается только после выполнения маршрута; до выполнения оно равноNULL. - Потоковая передача требует отключения устаревшей функциональности буферизации вывода Flight (
flight.v2.output_buffering = false). - Для внедрения зависимостей только некоторые определения маршрутов поддерживают создание через контейнер.
404 Not Found или неожиданное поведение маршрута
Если вы видите ошибку 404 Not Found (но вы готовы поклясться, что маршрут действительно существует и это не опечатка), на самом деле это может быть проблемой того, что вы возвращаете значение в конечной точке маршрута вместо простого вывода. Причина этого намеренная, но может застать врасплох некоторых разработчиков.
Flight::route('/hello', function(){
// Это может вызвать ошибку 404 Not Found
return 'Hello World';
});
// Вероятно, вам нужно это
Flight::route('/hello', function(){
echo 'Hello World';
});
Причина в том, что в роутер встроен специальный механизм, который интерпретирует возвращаемый вывод как сигнал «перейти к следующему маршруту». Это поведение задокументировано в разделе Маршрутизация.
История изменений
- v3: Добавлены ресурсная маршрутизация, псевдонимы маршрутов и поддержка потоковой передачи, группы маршрутов и поддержка промежуточного ПО.
- v1: Доступно подавляющее большинство базовых функций.
Learn/learn
Узнайте о Flight
Flight — это быстрый, простой и расширяемый фреймворк для PHP. Он довольно универсален и может использоваться для создания любых веб-приложений. Он создан с учётом простоты и написан так, чтобы его было легко понимать и использовать — как людям, так и ИИ-помощникам для программирования.
Примечание: Вы увидите примеры, в которых используется
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 это означает проверку того, как ваши маршруты, контроллеры и логика реагируют на различные входные данные — без зависимости от глобального состояния или реальных внешних сервисов.
Ключевые принципы:
- Тестируйте поведение, а не реализацию: Сосредоточьтесь на том, что делает ваш код, а не на том, как он это делает.
- Избегайте глобального состояния: Используйте внедрение зависимостей вместо
Flight::set()илиFlight::get(). - Мокайте внешние сервисы: Заменяйте такие вещи, как базы данных или почтовые сервисы, двойниками тестов.
- Поддерживайте тесты быстрыми и сфокусированными: Модульные тесты не должны обращаться к реальным базам данных или API.
Базовое использование
Настройка PHPUnit
- Установите PHPUnit через Composer:
composer require --dev phpunit/phpunit - Создайте каталог
testsв корне вашего проекта. - Добавьте тестовый скрипт в ваш
composer.json:"scripts": { "test": "phpunit --configuration phpunit.xml" } - Создайте файл
phpunit.xml:<?xml version="1.0" encoding="UTF-8"?> <phpunit bootstrap="vendor/autoload.php"> <testsuites> <testsuite name="Flight Tests"> <directory>tests</directory> </testsuite> </testsuites> </phpunit>
Теперь вы можете запускать тесты с помощью composer test.
Тестирование простого обработчика маршрута
Предположим, у вас есть маршрут, который проверяет 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']);
}
}
Советы:
- Имитируйте POST-данные с помощью
$app->request()->data. - Избегайте использования статических методов
Flight::в тестах — используйте экземпляр$app.
Использование внедрения зависимостей для тестируемых контроллеров
Внедряйте зависимости (например, базу данных или почтовый сервис) в ваши контроллеры, чтобы их можно было легко мокать в тестах:
use flight\database\SimplePdo;
class UserController {
protected $app;
protected $db;
protected $mailer;
public function __construct($app, $db, $mailer) {
$this->app = $app;
$this->db = $db;
$this->mailer = $mailer;
}
public function register() {
$email = $this->app->request()->data->email;
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
return $this->app->json(['status' => 'error', 'message' => 'Invalid email']);
}
$this->db->runQuery('INSERT INTO users (email) VALUES (?)', [$email]);
$this->mailer->sendWelcome($email);
return $this->app->json(['status' => 'success', 'message' => 'User registered']);
}
}
И тест с моками:
use PHPUnit\Framework\TestCase;
class UserControllerDICTest extends TestCase {
public function testValidEmailSavesAndSendsEmail() {
$mockDb = $this->createMock(flight\database\SimplePdo::class);
$mockDb->method('runQuery')->willReturn(true);
$mockMailer = new class {
public $sentEmail = null;
public function sendWelcome($email) { $this->sentEmail = $email; return true; }
};
$app = new flight\Engine();
$app->request()->data->email = 'test@example.com';
$controller = new UserController($app, $mockDb, $mockMailer);
$controller->register();
$response = $app->response()->getBody();
$result = json_decode($response, true);
$this->assertEquals('success', $result['status']);
$this->assertEquals('User registered', $result['message']);
$this->assertEquals('test@example.com', $mockMailer->sentEmail);
}
}
Продвинутое использование
- Мокание: Используйте встроенные моки PHPUnit или анонимные классы для замены зависимостей.
- Прямое тестирование контроллеров: Создавайте экземпляры контроллеров с новым
Engineи мокайте зависимости. - Избегайте чрезмерного мокания: Позволяйте реальной логике выполняться там, где это возможно; мокайте только внешние сервисы.
Смотрите также
- Руководство по модульному тестированию — Полное руководство по лучшим практикам модульного тестирования.
- Контейнер внедрения зависимостей — Как использовать DIC для управления зависимостями и улучшения тестируемости.
- Расширение — Как добавить собственные хелперы или переопределить основные классы.
- SimplePdo — Упрощает взаимодействие с базой данных и упрощает мокание в тестах.
- Запросы — Обработка HTTP-запросов во Flight.
- Ответы — Отправка ответов пользователям.
- Модульное тестирование и принципы SOLID — Узнайте, как принципы SOLID могут улучшить ваши модульные тесты.
Устранение неполадок
- Избегайте использования глобального состояния (
Flight::set(),$_SESSIONи т.д.) в вашем коде и тестах. - Если ваши тесты выполняются медленно, возможно, вы пишете интеграционные тесты — мокайте внешние сервисы, чтобы модульные тесты оставались быстрыми.
- Если настройка тестов сложна, рассмотрите рефакторинг кода для использования внедрения зависимостей.
История изменений
- v3.15.0 — Добавлены примеры для внедрения зависимостей и мокания.
Learn/flight_vs_symfony
Сравнение Flight и Symfony
Что такое Symfony?
Symfony - набор многоразовых компонентов PHP и фреймворк PHP для веб-проектов.
Стандартный фундамент, на котором строятся лучшие приложения на PHP. Выберите любые из 50 доступных автономных компонентов для ваших собственных приложений.
Ускорьте создание и поддержку ваших веб-приложений на PHP. Заканчивайте повторяющиеся задачи по кодированию и наслаждайтесь возможностью контролировать ваш код.
Преимущества по сравнению с Flight
- Symfony имеет огромную экосистему разработчиков и модулей, которые могут использоваться для решения общих проблем.
- Symfony имеет полнофункциональный ORM (Doctrine), который можно использовать для взаимодействия с вашей базой данных.
- Symfony имеет большое количество документации и учебных пособий, которые могут быть использованы для изучения фреймворка.
- Symfony имеет подкасты, конференции, встречи, видео и другие ресурсы, которые можно использовать для изучения фреймворка.
- Symfony ориентирован на опытного разработчика, который стремится создать полнофункциональное корпоративное веб-приложение.
Недостатки по сравнению с Flight
- Symfony имеет гораздо больше происходящего "под капотом", чем Flight. Это происходит за счет радикальных затрат в терминах производительности. См. бенчмарки TechEmpower для более подробной информации
- Flight ориентирован на разработчика, который стремится создать легкое, быстрое и простое в использовании веб-приложение.
- Flight ориентирован на простоту и удобство использования.
- Одной из основных особенностей Flight является то, что он делает все возможное для поддержания обратной совместимости.
- У Flight нет зависимостей, в то время как у Symfony есть целый ряд зависимостей
- Flight предназначен для разработчиков, которые впервые погружаются в мир фреймворков.
- Flight также может обрабатывать корпоративные приложения, но у него не так много примеров и учебных пособий, как у Symfony. Также для поддержания порядка и хорошей структуры разработчику потребуется больше дисциплины.
- Flight дает разработчику больший контроль над приложением, в то время как Symfony может скрыто использовать магию за кулисами.
Learn/flight_vs_another_framework
Сравнение Flight с другим фреймворком
Если вы переходите с другого фреймворка, такого как Laravel, Slim, Fat-Free или Symfony, на Flight, эта страница поможет вам понять различия между ними.
Laravel
Laravel - это полнофункциональный фреймворк со всеми плюшками и удивительной экосистемой, сосредоточенной на разработчике, но за счет производительности и сложности.
Slim
Slim - это микро-фреймворк, похожий на Flight. Он разработан с упором на легкость использования, но может быть немного сложнее, чем Flight.
Fat-Free
Fat-Free - это полностековый фреймворк в намного меньшем объеме. Хотя в нем есть все необходимые инструменты, его архитектура данных может усложнить некоторые проекты более, чем это необходимо.
Symfony
Symfony - модульный фреймворк корпоративного уровня, разработанный для гибкости и масштабируемости. Для меньших проектов или новых разработчиков Symfony может быть немного подавляющим.
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();
});
См. также
- Collections - Узнайте, как использовать класс Collection для легкого доступа к данным.
Устранение неисправностей
- Если вы получаете ошибку о соединении с базой данных, проверьте ваш DSN, имя пользователя, пароль и опции.
- Все строки возвращаются как Collections — если вам нужен простой массив, используйте
$collection->getData(). - Для запросов
IN (?)убедитесь, что вы передаете массив или строку, разделенную запятыми.
Журнал изменений
- v3.2.0 - Первоначальный выпуск PdoWrapper с базовыми методами запросов и получения.
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, но оно выполняет свою задачу с теми же преимуществами!
Смотрите также
- Установка — Структура скелета и где находится
services.php. - Автозагрузка — Пространства имён
App\и регистр папок. - Расширение Flight — Узнайте, как добавить внедрение зависимостей в ваши собственные классы, расширяя фреймворк.
- Конфигурация — Узнайте, как настроить Flight для вашего приложения.
- Маршрутизация — Узнайте, как определять маршруты для вашего приложения и как внедрение зависимостей работает с контроллерами.
- Промежуточное ПО — Узнайте, как создавать промежуточное ПО для вашего приложения и как внедрение зависимостей работает с ним.
- Модульное тестирование — Почему внедрение через конструктор лучше глобальных
Flight::. - ИИ и опыт разработчика — Единый шаблон DI для людей и агентов.
- SimplePdo — Предпочтительный помощник для работы с базой данных при внедрении.
Устранение неполадок
- Если у вас возникают проблемы с контейнером, убедитесь, что вы передаёте в контейнер правильные имена классов.
- Контроллеры, которые указывают тип
Engine, но получают «пустое» приложение: добавьте подстановку Engine (см. выше). Dice не должен создавать второй Engine черезnew. - Класс не найден для
App\Controller\…: проверьте регистр папки вapp/Controller/— см. Автозагрузка. - Обработчик должен возвращать созданный объект из
registerContainerHandler(не вызывайтеFlight::make()безreturn).
Журнал изменений
- Документация — Описание скелета Dice + подстановки Engine, SimplePdo и структуры
App\Controllerдля проектов, дружественных к ИИ. - v3.7.0 — Добавлена возможность регистрации обработчика DIC во Flight.
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 аутентификации, и вы хотите перенаправить пользователя на страницу входа, если он не аутентифицирован. У вас есть несколько вариантов:
- Вы можете вернуть false из функции middleware, и Flight автоматически вернёт ошибку 403 Forbidden, но без настройки.
- Вы можете перенаправить пользователя на страницу входа с помощью
Flight::redirect(). - Вы можете создать пользовательскую ошибку внутри middleware и остановить выполнение маршрута.
Простой и прямолинейный
Вот простой пример return false; :
class MyMiddleware {
public function before($params) {
$hasUserKey = Flight::session()->exists('user');
if ($hasUserKey === false) {
return false;
}
// поскольку это 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.']);
}
}
}
См. также
- Маршрутизация - Как сопоставлять маршруты с контроллерами и рендерить представления.
- Запросы - Понимание того, как обрабатывать входящие запросы.
- Ответы - Как настраивать HTTP-ответы.
- Внедрение зависимостей - Упрощение создания и управления объектами в маршрутах.
- Почему фреймворк? - Понимание преимуществ использования фреймворка вроде Flight.
- Пример стратегии выполнения middleware
Устранение неисправностей
- Если у вас есть перенаправление в middleware, но ваше приложение не перенаправляется, убедитесь, что вы добавили инструкцию
exit;в middleware.
Журнал изменений
- v3.1: Добавлена поддержка middleware.
Learn/filtering
Фильтрация
Обзор
Flight позволяет вам фильтровать отображенные методы до и после их вызова.
Понимание
Нет предопределенных хуков, которые вам нужно запоминать. Вы можете фильтровать любой из стандартных методов фреймворка, а также любые пользовательские методы, которые вы отобразили.
Функция фильтра выглядит так:
/**
* @param array $params Параметры, переданные в фильтруемый метод.
* @param string $output (только буферизация вывода v2) Вывод фильтруемого метода.
* @return bool Верните true/void или не возвращайте ничего, чтобы продолжить цепочку, false, чтобы прервать цепочку.
*/
function (array &$params, string &$output): bool {
// Код фильтра
}
Используя переданные переменные, вы можете манипулировать входными параметрами и/или выводом.
Вы можете запустить фильтр перед методом, сделав следующее:
Flight::before('start', function (array &$params, string &$output): bool {
// Сделайте что-то
});
Вы можете запустить фильтр после метода, сделав следующее:
Flight::after('start', function (array &$params, string &$output): bool {
// Сделайте что-то
});
Вы можете добавить столько фильтров, сколько хотите, к любому методу. Они будут вызваны в порядке их объявления.
Вот пример процесса фильтрации:
// Отобразите пользовательский метод
Flight::map('hello', function (string $name) {
return "Hello, $name!";
});
// Добавьте фильтр 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 для получения дополнительной информации.
См. также
Устранение неисправностей
- Убедитесь, что вы возвращаете
falseиз ваших функций фильтра, если хотите, чтобы цепочка остановилась. Если вы ничего не возвращаете, цепочка продолжится.
Журнал изменений
- v2.0 - Первое издание.
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'], если он существует.
Свойства объекта запроса
Объект запроса предоставляет следующие свойства:
- body - Сырое тело HTTP-запроса
- url - Запрашиваемый URL
- base - Родительская поддиректория URL
- method - Метод запроса (GET, POST, PUT, DELETE)
- referrer - URL реферера
- ip - IP-адрес клиента
- ajax - Является ли запрос AJAX-запросом
- scheme - Протокол сервера (http, https)
- user_agent - Информация о браузере
- type - Тип содержимого
- length - Длина содержимого
- query - Параметры строки запроса
- data - Данные POST или JSON-данные
- cookies - Данные cookie
- files - Загруженные файлы
- secure - Является ли соединение безопасным
- accept - Параметры HTTP accept
- proxy_ip - IP-адрес прокси клиента. Сканирует массив
$_SERVERна наличиеHTTP_CLIENT_IP,HTTP_X_FORWARDED_FOR,HTTP_X_FORWARDED,HTTP_X_CLUSTER_CLIENT_IP,HTTP_FORWARDED_FOR,HTTP_FORWARDEDв этом порядке. - host - Имя хоста запроса
- servername - SERVER_NAME из
$_SERVER
Вспомогательные методы
Есть несколько вспомогательных методов для сборки частей URL или работы с определенными заголовками.
Полный URL
Вы можете получить доступ к полному URL запроса с помощью метода getFullUrl():
$url = Flight::request()->getFullUrl();
// https://example.com/some/path?foo=bar
Базовый URL
Вы можете получить доступ к базовому URL с помощью метода getBaseUrl():
// http://example.com/path/to/something/cool?query=yes+thanks
$url = Flight::request()->getBaseUrl();
// https://example.com
// Notice, no trailing slash.
Разбор запроса
Вы можете передать URL методу parseQuery(), чтобы разобрать строку запроса в ассоциативный массив:
$query = Flight::request()->parseQuery('https://example.com/some/path?foo=bar');
// ['foo' => 'bar']
Переговоры по типам содержимого Accept
v3.17.2
Вы можете использовать метод negotiateContentType(), чтобы определить лучший тип содержимого для ответа на основе заголовка Accept, отправленного клиентом.
// Example Accept header: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
// The below defines what you support.
$availableTypes = ['application/json', 'application/xml'];
$typeToServe = Flight::request()->negotiateContentType($availableTypes);
if ($typeToServe === 'application/json') {
// Serve JSON response
} elseif ($typeToServe === 'application/xml') {
// Serve XML response
} else {
// Default to something else or throw an error
}
Примечание: Если ни один из доступных типов не найден в заголовке
Accept, метод вернетnull. Если заголовокAcceptне определен, метод вернет первый тип в массиве$availableTypes.
См. также
- Routing - См., как сопоставлять маршруты с контроллерами и рендерить представления.
- Responses - Как настраивать HTTP-ответы.
- Why a Framework? - Как запросы вписываются в общую картину.
- Collections - Работа с коллекциями данных.
- Uploaded File Handler - Обработка загрузки файлов.
Устранение неисправностей
request()->ipиrequest()->proxy_ipмогут отличаться, если ваш веб-сервер находится за прокси, балансировщиком нагрузки и т.д.
Журнал изменений
- v3.17.2 - Добавлен negotiateContentType()
- v3.12.0 - Добавлена возможность обработки загрузки файлов через объект запроса.
- v1.0 - Первое выпущение.
Learn/why_frameworks
Почему фреймворк?
Некоторые программисты решительно против использования фреймворков. Они утверждают, что фреймворки избыточны, медленны и сложны в изучении. Они говорят, что фреймворки не нужны, и что можно писать лучший код без них. Конечно, есть несколько обоснованных аргументов против использования фреймворков. Однако, есть также много преимуществ в использовании фреймворков.
Причины использования фреймворка
Вот несколько причин, почему вам может захотеться рассмотреть использование фреймворка:
- Быстрая разработка: Фреймворки предоставляют много функциональности из коробки. Это означает, что вы можете создавать веб-приложения быстрее. Вам не нужно писать столько кода, потому что фреймворк предоставляет много функциональности, которая вам нужна.
- Согласованность: Фреймворки предоставляют последовательный способ сделать вещи. Это облегчает понимание работы кода и упрощает другим разработчикам понимание вашего кода. Если у вас есть скрипт за скриптом, вы можете потерять согласованность между скриптами, особенно если вы работаете с командой разработчиков.
- Безопасность: Фреймворки предоставляют функции безопасности, которые помогают защитить ваши веб-приложения от распространенных угроз безопасности. Это означает, что вам не нужно беспокоиться о безопасности, потому что фреймворк заботится о большей части этого за вас.
- Сообщество: У фреймворков есть большие сообщества разработчиков, которые вносят свой вклад во фреймворк. Это означает, что вы можете получить помощь от других разработчиков, когда у вас возникают вопросы или проблемы. Это также означает, что доступно множество ресурсов для помощи в изучении использования фреймворка.
- Лучшие практики: Фреймворки созданы с использованием лучших практик. Это означает, что вы можете учиться у фреймворка и использовать те же лучшие практики в своем собственном коде. Это может помочь вам стать лучшим программистом. Иногда вы не знаете, чего не знаете, и это может вам плохо повлиять в конечном итоге.
- Расширяемость: Фреймворки разработаны для расширения. Это означает, что вы можете добавлять свою собственную функциональность в фреймворк. Это позволяет создавать веб-приложения, которые соответствуют вашим конкретным потребностям.
Flight - это микрофреймворк. Это означает, что он небольшой и легкий. Он не предоставляет так много функциональности, как более крупные фреймворки, такие как Laravel или Symfony. Однако он предоставляет много функциональности, которая вам нужна для создания веб-приложений. Его также легко изучить и использовать. Это делает его хорошим выбором для быстрого и простого создания веб-приложений. Если вы новичок в фреймворках, Flight - отличный фреймворк для начала. Он поможет вам узнать о преимуществах использования фреймворков, не перегружая вас слишком сложностью. После того как у вас будет опыт работы с Flight, будет легче перейти на более сложные фреймворки, такие как Laravel или Symfony, однако Flight все равно может создать успешное надежное приложение.
Что такое маршрутизация?
Маршрутизация является основой фреймворка Flight, но что это такое? Маршрутизация - это процесс принятия URL и сопоставления его с определенной функцией в вашем коде.
Таким образом вы можете заставить ваш веб-сайт делать разные вещи в зависимости от запрошенного URL. Например, вы могли бы показать профиль пользователя, когда
они посещают /user/1234, но показать список всех пользователей, когда они посещают /users. Все это делается через маршрутизацию.
Это может работать так:
- Пользователь заходит в ваш браузер и вводит
http://example.com/user/1234. - Сервер получает запрос, смотрит на URL и передает его в ваш код приложения Flight.
- Предположим, в вашем коде Flight у вас есть что-то вроде
Flight::route('/user/@id', [ 'UserController', 'viewUserProfile' ]);. Ваш код приложения Flight смотрит на URL и видит, что он соответствует определенному маршруту, затем выполняет код, который вы определили для этого маршрута. - Маршрутизатор Flight затем запустится и вызовет метод
viewUserProfile($id)в классеUserController, передавая1234в аргумент$idметода. - Код в вашем методе
viewUserProfile()затем будет выполняться и делать то, что вы ему сказали делать. Вы можете закончить выводом некоторого HTML для страницы профиля пользователя, или если это RESTful API, вы можете вывести JSON-ответ с информацией о пользователе. - Flight завернет это в красивый бантик, сгенерирует заголовки ответа и отправит обратно в браузер пользователя.
- Пользователь будет полон радости и даст себе теплый объятие!
И зачем это важно?
Иметь правильный централизованный маршрутизатор может действительно сильно облегчить вашу жизнь! Просто сначала это может быть трудно увидеть. Вот несколько причин:
- Централизованная маршрутизация: Вы можете хранить все свои маршруты в одном месте. Это позволяет легче видеть, какие маршруты у вас есть и что они делают. Также это упрощает их изменение, если вам это понадобится.
- Параметры маршрута: Вы можете использовать параметры маршрута, чтобы передавать данные в ваши методы маршрутов. Это отличный способ держать ваш код чистым и организованным.
- Группировка маршрутов: Вы можете группировать маршруты вместе. Это отлично для организации вашего кода и для применения middleware к группе маршрутов.
- Псевдонимы маршрутов: Вы можете присвоить псевдоним маршруту, чтобы URL мог динамически генерироваться позже в вашем коде (например, как шаблон). Например, вместо хардкода
/user/1234в вашем коде, вы можете обратиться к псевдонимуuser_viewи передатьidкак параметр. Это замечательно в случае, если вы решите изменить его на/admin/user/1234позднее. Вам не придется изменять все свои жестко закодированные URL, просто URL, привязанный к маршруту. - Промежуточное ПО маршрута: Вы можете добавлять промежуточное программное обеспечение к вашим маршрутам. Промежуточное программное обеспечение невероятно мощно в добавлении определенных поведений к вашему приложению, таких как аутентификация того, что определенный пользователь может получить доступ к маршруту или группе маршрутов.
Я уверен, что вы знакомы со способом создания веб-сайта, описанным скрипт за скриптом. Может быть у вас есть файл под названием index.php, который содержит множество условных операторов if
для проверки URL, а затем запуска определенной функции на основе URL. Это форма маршрутизации, но она не очень организована и может
быстро выйти из-под контроля. Система маршрутизации Flight - это гораздо более организованный и мощный способ управления маршрутами.
Это?
// /user/view_profile.php?id=1234
if ($_GET['id']) {
$id = $_GET['id'];
viewUserProfile($id);
}
// /user/edit_profile.php?id=1234
if ($_GET['id']) {
$id = $_GET['id'];
editUserProfile($id);
}
// и так далее...
или это?
// index.php
Flight::route('/user/@id', [ 'UserController', 'viewUserProfile' ]);
Flight::route('/user/@id/edit', [ 'UserController', 'editUserProfile' ]);
// Возможно, в вашем app/controllers/UserController.php
class UserController {
public function viewUserProfile($id) {
// сделать что-то
}
public function editUserProfile($id) {
// сделать что-то
}
}
Надеюсь, теперь вы начинаете видеть преимущества использования централизованной системы маршрутизации. Это намного проще управлять и понимать в долгосрочной перспективе!
Запросы и ответы
Flight предоставляет простой и легкий способ обработки запросов и ответов. Это ядро функционала веб-фреймворка. Он принимает запрос от браузера пользователя, обрабатывает его, а затем отправляет ответ. Именно так вы можете создавать веб-приложения, которые показывают профиль пользователя, позволяют пользователю войти в систему или опубликовать новый блог.
Запросы
Запрос - это то, что браузер пользователя отправляет на ваш сервер при посещении вашего веб-сайта. Этот запрос содержит информацию о том, что хочет сделать пользователь. Например, он может содержать информацию о том, какой URL пользователь хочет посетить, какие данные пользователь хочет отправить на ваш сервер, или какие данные пользователь хочет получить от вашего сервера. Важно знать, что запрос только для чтения. Вы не можете изменить запрос, но можете читать его.
Flight предоставляет простой способ получить доступ к информации о запросе. Вы можете получить доступ к информации о запросе, используя метод Flight::request()
Метод возвращает объект Request, который содержит информацию о запросе. Вы можете использовать этот объект для доступа к информации о запросе, такой как URL, метод или данные,
которые пользователь отправил на ваш сервер.
Ответы
Ответ - это то, что ваш сервер отправляет обратно на браузер пользователя при посещении вашего веб-сайта. Этот ответ содержит информацию о том, что ваш сервер хочет сделать. Например, он может содержать информацию о том, какие данные ваш сервер хочет отправить пользователю, какие данные ваш сервер хочет получить от пользователя, или какие данные ваш сервер хочет сохранить на компьютере пользователя.
Flight предоставляет простой способ отправить ответ браузеру пользователя. Вы можете отправить ответ, используя метод Flight::response()
Метод принимает объект Response в качестве аргумента и отправляет ответ браузеру пользователя. Вы можете использовать этот объект, чтобы отправить ответ браузеру пользователя,
такой как HTML, JSON или файл. Flight помогает автоматически генерировать некоторые части ответа, чтобы сделать вещи легкими, но в конечном итоге у вас есть
контроль над тем, что вы отправляете обратно пользователю.
Learn/responses
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');
});
См. также
- Маршрутизация — Как сопоставлять маршруты с контроллерами и рендерить представления.
- Запросы — Понимание того, как обрабатывать входящие запросы.
- Middleware — Использование middleware с маршрутами для аутентификации, логирования и т.д.
- Почему фреймворк? — Понимание преимуществ использования фреймворка вроде Flight.
- Расширение — Как расширять Flight своей собственной функциональностью.
Устранение неисправностей
- Если у вас проблемы с перенаправлениями, которые не работают, убедитесь, что вы добавили
return;в метод. stop()иhalt()— это не одно и то же.halt()остановит выполнение немедленно, в то время какstop()позволит выполнению продолжаться.
Журнал изменений
- v3.17.1 — Добавлен
$fileNameв методdownloadFile(). - v3.12.0 — Добавлен вспомогательный метод downloadFile.
- v3.10.0 — Добавлен
jsonHalt. - v1.0 — Первое выпущение.
Learn/events
Менеджер событий
начиная с v3.15.0
Обзор
События позволяют регистрировать и вызывать пользовательское поведение в вашем приложении. С добавлением Flight::onEvent() и Flight::triggerEvent() вы теперь можете подключаться к ключевым моментам жизненного цикла вашего приложения или определять свои собственные события (например, уведомления и emails), чтобы сделать ваш код более модульным и расширяемым. Эти методы являются частью mappable methods в Flight, что означает, что вы можете переопределить их поведение в соответствии с вашими потребностями.
Понимание
События позволяют разделять разные части вашего приложения, чтобы они не зависели друг от друга слишком сильно. Это разделение — часто называемое decoupling — делает ваш код проще для обновления, расширения или отладки. Вместо того чтобы писать всё в одном большом блоке, вы можете разделить логику на меньшие, независимые части, которые реагируют на конкретные действия (события).
Представьте, что вы строите приложение для блога:
- Когда пользователь публикует комментарий, вы можете захотеть:
- Сохранить комментарий в базу данных.
- Отправить email владельцу блога.
- Записать действие для безопасности.
Без событий вы бы запихнули всё это в одну функцию. С событиями вы можете разделить: одна часть сохраняет комментарий, другая вызывает событие вроде 'comment.posted', а отдельные слушатели обрабатывают email и логирование. Это делает ваш код чище и позволяет добавлять или удалять функции (например, уведомления) без касания основной логики.
Распространенные случаи использования
В основном события хороши для вещей, которые являются опциональными, но не абсолютной основной частью вашей системы. Например, следующие вещи хорошо иметь, но если они по какой-то причине не сработают, ваше приложение всё равно должно работать:
- Логирование: Записывать действия вроде входов или ошибок без засорения основного кода.
- Уведомления: Отправлять emails или оповещения, когда что-то происходит.
- Обновления кэша: Обновлять кэш или уведомлять другие системы об изменениях.
Однако, предположим, у вас есть функция "забыл пароль". Это должно быть частью вашей основной функциональности, а не событием, потому что если этот email не уйдёт, пользователь не сможет сбросить пароль и использовать ваше приложение.
Базовое использование
Система событий Flight построена вокруг двух основных методов: Flight::onEvent() для регистрации слушателей событий и Flight::triggerEvent() для вызова событий. Вот как вы можете их использовать:
Регистрация слушателей событий
Чтобы слушать событие, используйте Flight::onEvent(). Этот метод позволяет определить, что должно происходить, когда событие происходит.
Flight::onEvent(string $event, callable $callback): void
$event: Имя для вашего события (например,'user.login').$callback: Функция, которая выполняется, когда событие вызывается.
Вы "подписываетесь" на событие, сообщая Flight, что делать, когда оно происходит. Callback может принимать аргументы, переданные от вызова события.
Система событий Flight синхронная, что означает, что каждый слушатель события выполняется последовательно, один за другим. Когда вы вызываете событие, все зарегистрированные слушатели для этого события выполнятся до завершения, прежде чем ваш код продолжится. Это важно понимать, поскольку это отличается от асинхронных систем событий, где слушатели могут выполняться параллельно или позже.
Простой пример
Flight::onEvent('user.login', function ($username) {
echo "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
$event: Имя события, которое вы вызываете (должно соответствовать зарегистрированному событию)....$args: Опциональные аргументы для отправки слушателям (может быть любое количество аргументов).
Простой пример
$username = 'alice';
Flight::triggerEvent('user.login', $username);
Это вызывает событие 'user.login' и отправляет 'alice' слушателю, который мы определили ранее, что выведет: Welcome back, alice!.
- Если слушатели не зарегистрированы, ничего не происходит — ваше приложение не сломается.
- Используйте spread operator (
...) для гибкой передачи нескольких аргументов.
Остановка событий
Если слушатель возвращает 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();
- Плюсы: Держит
index.phpсосредоточенным на маршрутизации, организует события логично, легко найти и редактировать. - Минусы: Добавляет немного структуры, что может показаться излишним для очень маленьких приложений.
Вариант 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
- Начните просто: Для крошечных приложений размещайте события в
index.php. Это быстро и соответствует минимализму Flight. - Растите умно: По мере расширения приложения (например, больше 5-10 событий) используйте файл
app/config/events.php. Это естественный шаг вверх, как организация маршрутов, и держит код аккуратным без добавления сложных фреймворков. - Избегайте переусложнения: Не создавайте полноценный класс "event manager" или директорию, если приложение не огромно — 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.";
});
Почему полезно: Код редактирования не заботится о кэшировании — он просто сигнализирует об обновлении. Другие части приложения могут реагировать по необходимости.
Лучшие практики
- Называйте события ясно: Используйте конкретные имена вроде
'user.login'или'page.updated', чтобы было очевидно, что они делают. - Держите слушателей простыми: Не размещайте медленные или сложные задачи в слушателях — держите приложение быстрым.
- Тестируйте события: Вызывайте их вручную, чтобы убедиться, что слушатели работают как ожидается.
- Используйте события разумно: Они отличны для decoupling, но слишком много может сделать код сложным для отслеживания — используйте их, когда это имеет смысл.
Система событий в Flight PHP с Flight::onEvent() и Flight::triggerEvent() даёт вам простой, но мощный способ строить гибкие приложения. Позволяя разным частям приложения общаться друг с другом через события, вы можете держать код организованным, переиспользуемым и простым для расширения. Будь то логирование действий, отправка уведомлений или управление обновлениями, события помогают делать это без запутывания логики. Плюс, с возможностью переопределения этих методов, у вас есть свобода адаптировать систему под ваши нужды. Начните с одного события и посмотрите, как оно трансформирует структуру вашего приложения!
Встроенные события
Flight PHP поставляется с несколькими встроенными событиями, которые вы можете использовать для подключения к жизненному циклу фреймворка. Эти события вызываются в конкретных точках цикла запрос/ответ, позволяя выполнять пользовательскую логику, когда происходят определённые действия.
Список встроенных событий
- flight.request.received:
function(Request $request)Вызывается, когда запрос получен, разобраны и обработан. - flight.error:
function(Throwable $exception)Вызывается, когда происходит ошибка во время жизненного цикла запроса. - flight.redirect:
function(string $url, int $status_code)Вызывается, когда инициируется перенаправление. - flight.cache.checked:
function(string $cache_key, bool $hit, float $executionTime)Вызывается, когда кэш проверяется для конкретного ключа и попадание или промах в кэш. - flight.middleware.before:
function(Route $route)Вызывается после выполнения middleware before. - flight.middleware.after:
function(Route $route)Вызывается после выполнения middleware after. - flight.middleware.executed:
function(Route $route, $middleware, string $method, float $executionTime)Вызывается после выполнения любого middleware - flight.route.matched:
function(Route $route)Вызывается, когда маршрут совпадает, но ещё не запущен. - flight.route.executed:
function(Route $route, float $executionTime)Вызывается после выполнения и обработки маршрута.$executionTime— время, потраченное на выполнение маршрута (вызов контроллера и т.д.). - flight.view.rendered:
function(string $template_file_path, float $executionTime)Вызывается после рендеринга вида.$executionTime— время, потраченное на рендеринг шаблона. Примечание: Если вы переопределите методrender, вам нужно будет повторно вызвать это событие. - flight.response.sent:
function(Response $response, float $executionTime)Вызывается после отправки ответа клиенту.$executionTime— время, потраченное на построение ответа.
См. также
- Extending Flight - Как расширять и кастомизировать основную функциональность Flight.
- Cache - Пример использования событий для очистки кэша при обновлении страницы.
Устранение неисправностей
- Если вы не видите, что ваши слушатели событий вызываются, убедитесь, что регистрируете их перед вызовом событий. Порядок регистрации имеет значение.
Журнал изменений
- v3.15.0 - Добавлены события в Flight.
Learn/templates
HTML-представления и шаблоны
Обзор
Flight по умолчанию предоставляет базовую функциональность HTML-шаблонизации. Шаблонизация — это очень эффективный способ отделить логику приложения от уровня представления. Выделенный движок (Twig, Latte и т.д.) также даёт инструментам ИИ для написания кода знакомый ограниченный синтаксис, поэтому они с меньшей вероятностью будут встраивать бизнес-логику прямо в ваш HTML.
Понимание
При создании приложения у вас, скорее всего, будет HTML, который нужно отдавать конечному пользователю. PHP сам по себе является языком шаблонов, но очень легко добавить бизнес-логику, например вызовы базы данных, API и т.д., прямо в HTML-файл, что делает тестирование и разделение кода очень сложным процессом. Передавая данные в шаблон и позволяя шаблону отображать себя, становится гораздо проще разделять и юнит-тестировать код. Вы скажете нам спасибо, если будете использовать шаблоны!
Базовое использование
Flight позволяет заменить стандартный движок представлений, просто сопоставив render (или зарегистрировав класс представления). Прокрутите вниз, чтобы узнать о Twig, Latte, Smarty, Blade и других.
Параметр скелета по умолчанию: официальный flightphp/skeleton использует только Twig в каталоге
app/views/(*.twig). Контроллеры вызывают$this->app->render('welcome', $data)(расширение необязательно). Это выбор приложения для новых проектов, а не требование ядра Flight. Latte и другие движки полностью поддерживаются.
Twig
по умолчанию в скелете
Twig — это гибкий, быстрый и безопасный шаблонизатор, используемый в Symfony и многих других PHP-проектах. Инструменты ИИ для написания кода особенно хорошо знают Twig, и он по умолчанию автоматически экранирует вывод, что помогает защититься от XSS.
Установка
composer require twig/twig
(Уже включён, если вы выполнили composer create-project flightphp/skeleton.)
Базовая конфигурация
Переопределите метод render, чтобы использовать Twig вместо стандартного PHP-рендерера:
// переопределяем метод render, чтобы использовать Twig вместо стандартного PHP-рендерера
Flight::map('render', function(string $template, array $data): void {
$loader = new \Twig\Loader\FilesystemLoader(Flight::get('flight.views.path'));
$twig = new \Twig\Environment($loader, [
// Где Twig хранит скомпилированные шаблоны
'cache' => __DIR__ . '/../cache/twig',
'auto_reload' => true,
]);
// Разрешаем "welcome" или "welcome.twig"
if (substr($template, -5) !== '.twig') {
$template .= '.twig';
}
echo $twig->render($template, $data);
});
В скелете эта настройка находится в app/config/services.php (общее окружение Twig, путь к кэшу, глобальные переменные, такие как base_url / nonce для CSP). Предпочтительнее внедрять Engine и вызывать $app->render() из контроллеров, чтобы код оставался удобным для ИИ и тестирования.
Использование Twig во Flight
Теперь, когда вы можете рендерить с помощью Twig, можно сделать, например, следующее:
{# app/views/home.twig #}
<html>
<head>
<title>{% if title %}{{ title }} - {% endif %}My App</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Hello, {{ name }}!</h1>
</body>
</html>
// routes.php
Flight::route('/@name', function ($name) {
Flight::render('home.twig', [
'title' => 'Home Page',
'name' => $name
]);
});
Когда вы откроете /Bob в браузере, результат будет следующим:
<html>
<head>
<title>Home Page - My App</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Hello, Bob!</h1>
</body>
</html>
Дополнительное чтение
Более полный пример использования Twig с макетами приведён в разделе 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!
Смотрите также
- Установка — Макет скелета (
app/views/*.twig) для новых проектов. - Расширение — Как переопределить метод
renderдля использования другого шаблонизатора. - Маршрутизация — Как сопоставлять маршруты с контроллерами и отображать представления.
- Ответы — Как настраивать HTTP-ответы.
- Безопасность — Автоматическое экранирование и XSS.
- ИИ и опыт разработчика — Почему один шаблонизатор по умолчанию помогает агентам кода.
- Зачем нужен фреймворк? — Как шаблоны вписываются в общую картину.
Устранение неполадок
- Если в вашем middleware есть редирект, но ваше приложение, похоже, не перенаправляет, убедитесь, что вы добавили оператор
exit;в ваш middleware. - Если Twig не может найти шаблон, проверьте
flight.views.pathи что файл существует по этому пути с ожидаемым расширением (скелет:app/views/).
Журнал изменений
- Документация — Twig описан как официальный шаблонизатор по умолчанию для скелета; Latte остаётся полноценной альтернативой.
- v2.0 — Первоначальный выпуск.
Learn/simple_pdo
Класс помощника SimplePdo PDO
Обзор
Класс SimplePdo в Flight — это современный, функциональный помощник для работы с базами данных с использованием PDO. Он расширяет PdoWrapper и добавляет удобные методы-помощники для распространённых операций с базой данных, таких как insert(), update(), delete() и транзакции. Он упрощает задачи работы с базой данных, возвращает результаты в виде Collections для лёгкого доступа и поддерживает логирование запросов и мониторинг производительности приложений (APM) для продвинутых сценариев использования.
Понимание
Класс SimplePdo предназначен для упрощения работы с базами данных в PHP. Вместо манипуляций с подготовленными выражениями, режимами выборки и многословными SQL-операциями вы получаете чистые, простые методы для распространённых задач. Каждая строка возвращается как Collection, так что вы можете использовать нотацию массива ($row['name']) и нотацию объекта ($row->name).
Этот класс является суперкомплектом PdoWrapper, то есть включает всю функциональность PdoWrapper плюс дополнительные методы-помощники, которые делают ваш код чище и удобнее в поддержке. Если вы сейчас используете PdoWrapper, переход на SimplePdo будет простым, поскольку он расширяет PdoWrapper.
Вы можете зарегистрировать SimplePdo как общую службу в Flight, а затем использовать её в любом месте вашего приложения через Flight::db().
Основное использование
Регистрация SimplePdo
Сначала зарегистрируйте класс SimplePdo в Flight:
Flight::register('db', \flight\database\SimplePdo::class, [
'mysql:host=localhost;dbname=cool_db_name', 'user', 'pass', [
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'utf8mb4\'',
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_STRINGIFY_FETCHES => false,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
]
]);
ПРИМЕЧАНИЕ
Если вы не укажете
PDO::ATTR_DEFAULT_FETCH_MODE,SimplePdoавтоматически установит его вPDO::FETCH_ASSOCза вас.
Теперь вы можете использовать Flight::db() в любом месте для получения соединения с базой данных.
Выполнение запросов
runQuery()
function runQuery(string $sql, array $params = []): PDOStatement
Используйте это для INSERT, UPDATE или когда вы хотите вручную получить результаты:
$db = Flight::db();
$statement = $db->runQuery("SELECT * FROM users WHERE status = ?", ['active']);
while ($row = $statement->fetch()) {
// $row 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 будет простой:
-
Обновите вашу регистрацию:
// Old Flight::register('db', \flight\database\PdoWrapper::class, [ /* ... */ ]); // New Flight::register('db', \flight\database\SimplePdo::class, [ /* ... */ ]); -
Все существующие методы
PdoWrapperработают вSimplePdo— Нет разрушительных изменений. Ваш существующий код продолжит работать. -
Опционально используйте новые методы-помощники — Начните использовать
insert(),update(),delete()иtransaction()для упрощения вашего кода.
См. также
- Collections — Узнайте, как использовать класс Collection для лёгкого доступа к данным.
- PdoWrapper — Устаревший класс помощника PDO (устарел).
Устранение неисправностей
- Если вы получаете ошибку о соединении с базой данных, проверьте ваш DSN, имя пользователя, пароль и опции.
- Все строки возвращаются как Collections — если вам нужен простой массив, используйте
$collection->getData(). - Для запросов
IN (?)убедитесь, что вы передаёте массив. - Если вы испытываете проблемы с памятью при логировании запросов в долгоживущих процессах, настройте опцию
maxQueryMetrics.
Журнал изменений
- v3.18.0 — Первое выпуски SimplePdo с методами-помощниками для insert, update, delete и транзакций.
Learn/collections
Коллекции
Обзор
Класс Collection во Flight — это удобный инструмент для управления наборами данных. Он позволяет получать доступ к данным и управлять ими как через синтаксис массивов, так и через синтаксис объектов, делая ваш код чище и гибче.
Понимание
Collection — это, по сути, обёртка над массивом, но с дополнительными возможностями. Вы можете использовать его как массив, перебирать его, подсчитывать элементы и даже обращаться к элементам как к свойствам объекта. Это особенно полезно, когда нужно передавать структурированные данные в приложении или сделать код более читаемым.
Коллекции реализуют несколько PHP-интерфейсов:
ArrayAccess(чтобы использовать синтаксис массива)Iterator(чтобы перебирать черезforeach)Countable(чтобы использоватьcount())JsonSerializable(чтобы легко преобразовывать в JSON)
Базовое использование
Создание коллекции
Вы можете создать коллекцию, просто передав массив в её конструктор:
use flight\util\Collection;
$data = [
'name' => 'Flight',
'version' => 3,
'features' => ['routing', 'views', 'extending']
];
$collection = new Collection($data);
Доступ к элементам
Вы можете получать доступ к элементам, используя как синтаксис массива, так и синтаксис объекта:
// Синтаксис массива
echo $collection['name']; // Вывод: FlightPHP
// Синтаксис объекта
echo $collection->version; // Вывод: 3
Если вы попытаетесь получить доступ к несуществующему ключу, вы получите null, а не ошибку.
Установка элементов
Вы также можете устанавливать элементы, используя любую из этих нотаций:
// Синтаксис массива
$collection['author'] = 'Mike Cao';
// Синтаксис объекта
$collection->license = 'MIT';
Проверка и удаление элементов
Проверьте, существует ли элемент:
if (isset($collection['name'])) {
// Сделать что-то
}
if (isset($collection->version)) {
// Сделать что-то
}
Удалите элемент:
unset($collection['author']);
unset($collection->license);
Перебор коллекции
Коллекции являются итерируемыми, поэтому вы можете использовать их в цикле foreach:
foreach ($collection as $key => $value) {
echo "$key: $value\n";
}
Подсчёт элементов
Вы можете подсчитать количество элементов в коллекции:
echo count($collection); // Вывод: 4
Получение всех ключей или данных
Получить все ключи:
$keys = $collection->keys(); // ['name', 'version', 'features', 'license']
Получить все данные в виде массива:
$data = $collection->getData();
Очистка коллекции
Удалить все элементы:
$collection->clear();
Сериализация в JSON
Коллекции можно легко преобразовать в JSON:
echo json_encode($collection);
// Вывод: {"name":"FlightPHP","version":3,"features":["routing","views","extending"],"license":"MIT"}
Продвинутое использование
При необходимости вы можете полностью заменить внутренний массив данных:
$collection->setData(['foo' => 'bar']);
Коллекции особенно полезны, когда нужно передавать структурированные данные между компонентами или предоставить более объектно-ориентированный интерфейс для данных массива.
Смотрите также
- Запросы — Узнайте, как обрабатывать HTTP-запросы и как коллекции можно использовать для управления данными запросов.
- SimplePdo — Помощник базы данных, который возвращает строки запросов в виде коллекций.
Устранение неполадок
- Если вы попытаетесь получить доступ к несуществующему ключу, вы получите
null, а не ошибку. - Помните, что коллекции не рекурсивны: вложенные массивы не преобразуются автоматически в коллекции.
- Если необходимо сбросить коллекцию, используйте
$collection->clear()или$collection->setData([]).
Журнал изменений
- v3.0 — улучшенные подсказки типов и поддержка PHP 8+.
- v1.0 — первоначальный выпуск класса Collection.
Learn/flight_vs_fat_free
Flight против Fat-Free
Что такое Fat-Free?
Fat-Free (ласково известный как F3) — это мощный, но простой в использовании PHP-микрофреймворк, предназначенный для быстрого создания динамических и надежных веб-приложений.
Flight во многом сравним с Fat-Free и, вероятно, является самым близким «родственником» по функциональности и простоте. У Fat-Free есть много возможностей, которых нет у Flight, но также есть много возможностей, которые есть и у Flight. Fat-Free начинает показывать свой возраст, и его популярность уже не так высока, как раньше.
Обновления становятся все реже, а сообщество уже не так активно, как прежде. Код достаточно прост, но иногда отсутствие дисциплины в синтаксисе может затруднять чтение и понимание. Он работает с PHP 8.3, но сам код все еще выглядит так, будто он застрял в PHP 5.3.
Преимущества по сравнению с Flight
- У Fat-Free немного больше звезд на GitHub, чем у Flight.
- У Fat-Free есть достойная документация, но в некоторых местах ей не хватает ясности.
- У Fat-Free есть немногочисленные ресурсы, такие как руководства на YouTube и онлайн-статьи, которые можно использовать для изучения фреймворка.
- В Fat-Free встроено несколько полезных плагинов, которые иногда оказываются кстати.
- В Fat-Free есть встроенная ORM под названием Mapper, которую можно использовать для взаимодействия с базой данных. Во Flight есть active-record.
- В Fat-Free встроены сессии, кэширование и локализация. Flight требует использования сторонних библиотек, но это описано в документации.
- У Fat-Free есть небольшая группа созданных сообществом плагинов, которые можно использовать для расширения фреймворка. У Flight некоторые из них описаны на страницах документации и примеров.
- Fat-Free, как и Flight, не имеет зависимостей.
- Fat-Free, как и Flight, ориентирован на предоставление разработчику контроля над своим приложением и простой опыт разработки.
- Fat-Free сохраняет обратную совместимость, как и Flight (частично потому, что обновления выходят все реже).
- Fat-Free, как и Flight, предназначен для разработчиков, которые впервые знакомятся с миром фреймворков.
- В Fat-Free встроен шаблонизатор, который более мощный, чем шаблонизатор Flight. Flight рекомендует Latte для достижения этой цели.
- В Fat-Free есть уникальная команда «маршрут» для CLI, позволяющая создавать CLI-приложения прямо внутри Fat-Free и обрабатывать их подобно
GET-запросу. Flight делает это с помощью runway.
Недостатки по сравнению с Flight
- У Fat-Free есть некоторые тесты реализации и даже собственный очень простой тестовый класс. Однако он не покрыт модульными тестами на 100%, в отличие от Flight.
- Для поиска по документации приходится использовать поисковые системы, такие как Google.
- На сайте документации Flight есть темная тема. (микрофон роняю)
- Некоторые модули Fat-Free, к сожалению, не поддерживаются.
- Во Flight есть SimplePdo для доступа к базе данных, который немного проще, чем встроенный класс
DB\SQLу Fat-Free (и предпочтительнее устаревшего PdoWrapper). - Во Flight есть плагин разрешений, который можно использовать для защиты вашего приложения. В Fat-Free для этого требуется сторонняя библиотека.
- Во Flight есть ORM под названием active-record, которая больше похожа на ORM, чем Mapper у Fat-Free. Дополнительное преимущество
active-recordв том, что вы можете определять связи между записями для автоматических объединений, тогда как Mapper в Fat-Free требует создания SQL-представлений. - Удивительно, но у Fat-Free нет корневого пространства имен. Flight полностью использует пространства имен, чтобы не конфликтовать с вашим кодом. Класс
Cacheздесь самый яркий пример. - В Fat-Free нет промежуточного программного обеспечения (middleware). Вместо этого есть хуки
beforerouteиafterroute, которые можно использовать для фильтрации запросов и ответов в контроллерах. - В Fat-Free нельзя группировать маршруты.
- В Fat-Free есть обработчик контейнера внедрения зависимостей, но документация о том, как его использовать, крайне скудна.
- Отладка может стать немного сложной, поскольку практически все хранится в так называемом
HIVE.
Learn/extending
Расширение
Обзор
Flight разработан как расширяемая платформа. Фреймворк поставляется с набором стандартных методов и компонентов, но позволяет вам отображать свои собственные методы, регистрировать свои собственные классы или даже переопределять существующие классы и методы.
Понимание
Есть 2 способа расширить функциональность Flight:
- Отображение методов — это используется для создания простых пользовательских методов, которые вы можете вызывать из любого места в вашем приложении. Они обычно используются для утилитарных функций, которые вы хотите вызывать из любого места в вашем коде.
- Регистрация классов — это используется для регистрации ваших собственных классов в Flight. Это обычно используется для классов, которые имеют зависимости или требуют конфигурации.
Вы также можете переопределять существующие методы фреймворка, чтобы изменить их поведение по умолчанию, чтобы лучше соответствовать потребностям вашего проекта.
Если вы ищете DIC (Dependency Injection Container), перейдите на страницу Dependency Injection Container.
Основное использование
Переопределение методов фреймворка
Flight позволяет вам переопределять его стандартную функциональность в соответствии с вашими потребностями, без необходимости изменять какой-либо код. Вы можете просмотреть все методы, которые можно переопределить, ниже.
Например, когда Flight не может сопоставить URL с маршрутом, он вызывает метод notFound,
который отправляет общий ответ HTTP 404. Вы можете переопределить это поведение,
используя метод map:
Flight::map('notFound', function() {
// Отображение пользовательской страницы 404
include 'errors/404.html';
});
Flight также позволяет заменить основные компоненты фреймворка. Например, вы можете заменить стандартный класс Router на свой собственный пользовательский класс:
// создание вашего пользовательского класса 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();
Круто, правда?
См. также
- Dependency Injection Container — Как использовать DIC с Flight.
- File Cache — Пример использования библиотеки кэширования с Flight.
Устранение неполадок
- Помните, что отображенные методы имеют приоритет над зарегистрированными классами. Если вы объявите оба с одним и тем же именем, будет вызван только отображенный метод.
Журнал изменений
- v2.0 — Первое издание.
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);
См. также
- Collections - Для работы со структурированными данными, которые можно легко преобразовать в JSON.
- Configuration - Как настроить ваше приложение Flight.
- Extending - Как добавить свои утилиты или переопределить основные классы.
Устранение неисправностей
- Если кодирование или декодирование не удастся, выбрасывается исключение — оберните вызовы в try/catch, если хотите обрабатывать ошибки грациозно.
- Если вы получаете неожиданные результаты, проверьте данные на циклические ссылки или не-UTF8 символы.
- Используйте
Json::isValid(), чтобы проверить, является ли строка валидным JSON перед декодированием.
Журнал изменений
- v3.16.0 - Добавлен утилитный класс обёртки JSON.
Learn/flight_vs_slim
Flight против Slim
Что такое Slim?
Slim — это PHP микро-фреймворк, который помогает быстро создавать простые, но мощные веб-приложения и API.
Многие идеи для некоторых функций v3 во Flight на самом деле пришли из Slim. Группировка маршрутов и выполнение middleware в определенном порядке — две функции, вдохновленные Slim. Slim v3 вышел с ориентацией на простоту, но о v4 были неоднозначные отзывы.
Преимущества по сравнению с Flight
- У Slim более крупное сообщество разработчиков, которые создают полезные модули, помогающие не изобретать велосипед.
- Slim соответствует многим интерфейсам и стандартам, распространенным в PHP-сообществе, что повышает совместимость.
- У Slim есть хорошая документация и учебные пособия, которые можно использовать для изучения фреймворка (хотя им далеко до Laravel или Symfony).
- У Slim есть различные ресурсы, такие как учебники на YouTube и онлайн-статьи, которые можно использовать для изучения фреймворка.
- Slim позволяет использовать любые компоненты для обработки основных функций маршрутизации, поскольку он совместим с PSR-7.
Недостатки по сравнению с Flight
- Как ни удивительно, Slim не такой быстрый, как можно было бы ожидать от микро-фреймворка. Смотрите бенчмарки TechEmpower для получения дополнительной информации.
- Flight ориентирован на разработчика, который хочет создать легковесное, быстрое и простое в использовании веб-приложение.
- У Flight нет зависимостей, тогда как у Slim есть несколько зависимостей, которые необходимо установить.
- Flight ориентирован на простоту и удобство использования.
- Одна из ключевых особенностей Flight — он делает все возможное для сохранения обратной совместимости. Переход с Slim v3 на v4 был критическим изменением.
- Flight предназначен для разработчиков, которые впервые входят в мир фреймворков.
- Flight также может работать с приложениями корпоративного уровня, но у него не так много примеров и учебных пособий, как у Slim. Это также потребует большей дисциплины со стороны разработчика, чтобы поддерживать порядок и хорошую структуру.
- Flight дает разработчику больше контроля над приложением, тогда как Slim может незаметно добавить немного "магии" за кулисами.
- Flight имеет SimplePdo для доступа к базе данных (предпочтительнее устаревшего PdoWrapper). Slim требует использования сторонней библиотеки.
- Flight имеет плагин разрешений, который можно использовать для защиты вашего приложения. Slim требует использования сторонней библиотеки.
- Flight имеет ORM под названием active-record, который можно использовать для взаимодействия с базой данных. Slim требует использования сторонней библиотеки.
- Flight имеет CLI-приложение под названием runway, которое можно использовать для запуска приложения из командной строки. У Slim такого нет.
Learn/autoloading
Автозагрузка
Обзор
Автозагрузка — это концепция в PHP, при которой вы указываете каталог или каталоги для загрузки классов. Это гораздо полезнее, чем использование require или include для загрузки классов. Это также требование для использования пакетов Composer.
Правильная настройка автозагрузки важна и для разработки с помощью ИИ: агенты размещают файлы там, куда указывает пространство имён. Если регистр папок и регистр пространства имён не совпадают, на Linux появляются ошибки «класс не найден», даже если на Mac с регистронезависимой файловой системой всё «работало».
Понимание
По умолчанию все классы Flight автоматически загружаются благодаря Composer. Для ваших классов приложения есть два распространённых подхода:
- Composer PSR-4 (используется в официальном скелетоне): сопоставьте префикс пространства имён с каталогом в
composer.json, затем выполнитеcomposer dump-autoload. Flight::path(): укажите загрузчику Flight каталоги (удобно для простых приложений или когда вы не используете Composer для кода приложения).
Использование автозагрузчика значительно упрощает ваш код. Вместо стены из include / require в начале каждого файла классы загружаются при первом использовании.
Чувствительность к регистру (прочитайте дважды)
Пространства имён должны соответствовать структуре каталогов и регистру букв в этих каталогах.
| Работает | Ломается на Linux |
|---|---|
App\Controller\HomeController → app/Controller/HomeController.php |
App\Controller\… с каталогом app/controllers/ |
app\controllers\MyController → app/controllers/MyController.php |
Смешивание App\ с нижним регистром controllers |
Пространства имён PHP не чувствительны к регистру в некоторых контекстах, но Composer и файловая система — нет. Официальный скелетон придерживается следующих стандартов:
- Composer:
"App\\": "app/" - Каталоги:
Controller,Middleware,Model,Utils(PascalCase), а неcontrollers/middlewares
В старой документации и примерах сообщества иногда использовался нижний регистр app\controllers. Это по-прежнему работает, если ваши каталоги в нижнем регистре, — но новые проекты на скелетоне используют App\ + каталоги в PascalCase. Выберите одно соглашение для проекта и придерживайтесь его, чтобы люди и ИИ-инструменты не придумывали вторую структуру.
Скелетон (рекомендуется для новых проектов)
После 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() {
// что-то делаем
}
}
Смотрите также
- Установка — Дерево скелетона и значения по умолчанию
App\для новых проектов. - Маршрутизация — Как сопоставлять маршруты с контроллерами и отображать представления.
- Внедрение зависимостей — Как контроллеры получают
Engineи сервисы. - ИИ и опыт разработчика — Держите агентов в согласии с вашей структурой через
AGENTS.md. - Зачем нужен фреймворк? — Понимание преимуществ использования фреймворка вроде Flight.
Устранение неполадок
- Если вы не можете понять, почему ваши классы с пространствами имён не находятся, запомните: при использовании
Flight::path()указывайте на корень проекта (или правильную базу для вашего пространства имён), а не только на вложенную папку, которую вы забыли отразить в пространстве имён. - При использовании Composer PSR-4 выполните
composer dump-autoloadпосле изменения сопоставлений вcomposer.json. - В CI на Linux или в продакшене неправильный регистр папки — очень распространённый сбой «у меня работает».
Класс не найден (автозагрузка не работает)
Причин этой проблемы может быть несколько. Ниже приведены некоторые примеры.
Неправильное имя файла
Самая распространённая причина — имя класса не совпадает с именем файла.
Если у вас есть класс с именем 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() для контроллеров и моделей там.
История изменений
- Документация — Описание скелетона
App\+ каталогов в PascalCase и проблем с регистром для людей и ИИ-инструментов. - v3.7.2 — Вы можете использовать Pascal_Snake_Case для имён классов, выполнив
Loader::setV2ClassLoading(false); - v2.0 — Добавлена функция автозагрузки.
Learn/uploaded_file
Обработчик загруженного файла
Обзор
Класс UploadedFile в Flight упрощает и делает безопасной обработку загрузки файлов в вашем приложении. Он оборачивает детали процесса загрузки файлов PHP, предоставляя простой объектно-ориентированный способ доступа к информации о файле и перемещения загруженных файлов.
Понимание
Когда пользователь загружает файл через форму, PHP сохраняет информацию о файле в суперглобальной переменной $_FILES. В Flight вы редко взаимодействуете с $_FILES напрямую. Вместо этого объект Request Flight (доступный через Flight::request()) предоставляет метод getUploadedFiles(), который возвращает массив объектов UploadedFile, делая обработку файлов гораздо более удобной и надежной.
Класс UploadedFile предоставляет методы для:
- Получения оригинального имени файла, типа MIME, размера и временного расположения
- Проверки ошибок загрузки
- Перемещения загруженного файла в постоянное расположение
Этот класс помогает избежать распространенных ошибок при загрузке файлов, таких как обработка ошибок или безопасное перемещение файлов.
Базовое использование
Доступ к загруженным файлам из запроса
Рекомендуемый способ доступа к загруженным файлам — через объект запроса:
Flight::route('POST /upload', function() {
// Для поля формы <input type="file" name="myFile">
$uploadedFiles = Flight::request()->getUploadedFiles();
$file = $uploadedFiles['myFile'];
// Теперь вы можете использовать методы UploadedFile
if ($file->getError() === UPLOAD_ERR_OK) {
$file->moveTo('/path/to/uploads/' . $file->getClientFilename());
echo "Файл успешно загружен!";
} else {
echo "Загрузка не удалась: " . $file->getError();
}
});
Обработка множественной загрузки файлов
Если ваша форма использует name="myFiles[]" для множественной загрузки, вы получите массив объектов UploadedFile:
Flight::route('POST /upload', function() {
// Для поля формы <input type="file" name="myFiles[]">
$uploadedFiles = Flight::request()->getUploadedFiles();
foreach ($uploadedFiles['myFiles'] as $file) {
if ($file->getError() === UPLOAD_ERR_OK) {
$file->moveTo('/path/to/uploads/' . $file->getClientFilename());
echo "Загружено: " . $file->getClientFilename() . "<br>";
} else {
echo "Не удалось загрузить: " . $file->getClientFilename() . "<br>";
}
}
});
Создание экземпляра UploadedFile вручную
Обычно вы не создаете UploadedFile вручную, но это возможно при необходимости:
use flight\net\UploadedFile;
$file = new UploadedFile(
$_FILES['myfile']['name'],
$_FILES['myfile']['type'],
$_FILES['myfile']['size'],
$_FILES['myfile']['tmp_name'],
$_FILES['myfile']['error']
);
Доступ к информации о файле
Вы можете легко получить детали о загруженном файле:
echo $file->getClientFilename(); // Оригинальное имя файла с компьютера пользователя
echo $file->getClientMediaType(); // Тип MIME (например, image/png)
echo $file->getSize(); // Размер файла в байтах
echo $file->getTempName(); // Временный путь к файлу на сервере
echo $file->getError(); // Код ошибки загрузки (0 означает отсутствие ошибки)
Перемещение загруженного файла
После валидации файла переместите его в постоянное расположение:
try {
$file->moveTo('/path/to/uploads/' . $file->getClientFilename());
echo "Файл успешно загружен!";
} catch (Exception $e) {
echo "Загрузка не удалась: " . $e->getMessage();
}
Метод moveTo() вызовет исключение, если что-то пойдет не так (например, ошибка загрузки или проблема с правами доступа).
Обработка ошибок загрузки
Если во время загрузки возникла проблема, вы можете получить читаемое сообщение об ошибке:
if ($file->getError() !== UPLOAD_ERR_OK) {
// Вы можете использовать код ошибки или поймать исключение от moveTo()
echo "Произошла ошибка при загрузке файла.";
}
См. также
- Requests - Узнайте, как получать доступ к загруженным файлам из HTTP-запросов и увидеть больше примеров загрузки файлов.
- Configuration - Как настроить лимиты загрузки и директории в PHP.
- Extending - Как настроить или расширить основные классы Flight.
Устранение неисправностей
- Всегда проверяйте
$file->getError()перед перемещением файла. - Убедитесь, что директория загрузки доступна для записи веб-сервером.
- Если
moveTo()не срабатывает, проверьте сообщение исключения для деталей. - Настройки PHP
upload_max_filesizeиpost_max_sizeмогут ограничивать загрузку файлов. - Для множественной загрузки файлов всегда проходите циклом по массиву объектов
UploadedFile.
Журнал изменений
- v3.12.0 - Добавлен класс
UploadedFileв объект запроса для упрощения обработки файлов.
Guides/unit_testing
Модульное тестирование в Flight PHP с PHPUnit
Это руководство знакомит с модульным тестированием во Flight PHP с использованием PHPUnit, предназначено для начинающих, которые хотят понять почему модульное тестирование важно и как применять его на практике. Мы сосредоточимся на тестировании поведения — проверке, что ваше приложение делает то, что вы ожидаете, например, отправляет электронное письмо или сохраняет запись, — а не на тривиальных вычислениях. Мы начнем с простого обработчика маршрута и перейдем к более сложному контроллеру, используя внедрение зависимостей (DI) и имитацию сторонних сервисов.
Зачем нужно модульное тестирование?
Модульное тестирование гарантирует, что ваш код ведет себя ожидаемо, выявляя ошибки до попадания в продакшн. Это особенно ценно во Flight, где легковесная маршрутизация и гибкость могут приводить к сложным взаимодействиям. Для разработчиков-одиночек или команд модульные тесты действуют как страховочная сеть, документируя ожидаемое поведение и предотвращая регрессии при возвращении к коду позже. Они также улучшают дизайн: код, который трудно тестировать, часто сигнализирует о чрезмерно сложных или тесно связанных классах.
В отличие от упрощенных примеров (например, проверка x * y = z), мы сосредоточимся на реальных сценариях, таких как проверка ввода, сохранение данных или запуск действий вроде отправки писем. Наша цель — сделать тестирование доступным и осмысленным.
Общие руководящие принципы
- Тестируйте поведение, а не реализацию: Сосредоточьтесь на результатах (например, «письмо отправлено» или «запись сохранена»), а не на внутренних деталях. Это делает тесты устойчивыми к рефакторингу.
- Перестаньте использовать
Flight::: Статические методы Flight чрезвычайно удобны, но затрудняют тестирование. Привыкайте использовать переменную$appиз$app = Flight::app();.$appимеет все те же методы, что иFlight::. Вы по-прежнему сможете использовать$app->route()или$this->app->json()в контроллере и т.д. Также следует использовать настоящий роутер Flight через$router = $app->router(), и тогда вы сможете использовать$router->get(),$router->post(),$router->group()и т.д. См. Маршрутизация. - Поддерживайте тесты быстрыми: Быстрые тесты побуждают к частому выполнению. Избегайте медленных операций, таких как вызовы баз данных, в модульных тестах. Если у вас медленный тест, это признак того, что вы пишете интеграционный тест, а не модульный. Интеграционные тесты — это когда вы реально подключаете базы данных, реальные HTTP-вызовы, реальную отправку писем и т.д. У них есть свое место, но они медленные и могут быть нестабильными, то есть иногда падать по неизвестной причине.
- Используйте описательные имена: Имена тестов должны четко описывать тестируемое поведение. Это улучшает читаемость и поддерживаемость.
- Избегайте глобальных переменных как чумы: Минимизируйте использование
$app->set()и$app->get(), так как они действуют как глобальное состояние, требуя имитаций в каждом тесте. Предпочитайте DI или контейнер внедрения зависимостей (см. Контейнер внедрения зависимостей). Даже использование метода$app->map()технически является «глобальным» и его следует избегать в пользу DI. Используйте библиотеку сессий, например flightphp/session, чтобы можно было имитировать объект сессии в тестах. Не вызывайте$_SESSIONнапрямую в коде, так как это внедряет глобальную переменную в ваш код, что затрудняет тестирование. - Используйте внедрение зависимостей: Внедряйте зависимости (например,
PDO, почтовые сервисы) в контроллеры, чтобы изолировать логику и упростить имитацию. Если у класса слишком много зависимостей, рассмотрите возможность рефакторинга на более мелкие классы, каждый из которых имеет одну ответственность в соответствии с принципами SOLID. - Имитируйте сторонние сервисы: Имитируйте базы данных, HTTP-клиенты (cURL) или почтовые сервисы, чтобы избежать внешних вызовов. Тестируйте на один-два уровня вглубь, но давайте основной логике выполняться. Например, если ваше приложение отправляет текстовое сообщение, вам НЕ нужно реально отправлять сообщение каждый раз при запуске тестов, потому что расходы будут расти (и это будет медленнее). Вместо этого имитируйте сервис отправки сообщений и просто проверяйте, что ваш код вызвал этот сервис с правильными параметрами.
- Стремитесь к высокому покрытию, а не к совершенству: 100% покрытие строк — это хорошо, но на самом деле не означает, что весь код протестирован так, как нужно (почитайте о покрытии ветвей/путей в PHPUnit). Приоритезируйте критически важное поведение (например, регистрацию пользователя, ответы API и фиксацию неудачных ответов).
- Используйте контроллеры для маршрутов: В определениях маршрутов используйте контроллеры, а не замыкания. Экземпляр
flight\Engine $appвнедряется в каждый контроллер через конструктор по умолчанию. В тестах используйте$app = new Flight\Engine()для создания экземпляра Flight внутри теста, внедряйте его в контроллер и вызывайте методы напрямую (например,$controller->register()). См. Расширение Flight и Маршрутизация. - Выберите стиль имитации и придерживайтесь его: PHPUnit поддерживает несколько стилей имитации (например, prophecy, встроенные имитации), или вы можете использовать анонимные классы, у которых есть свои преимущества, такие как автодополнение кода, поломка при изменении сигнатуры метода и т.д. Просто будьте последовательны в своих тестах. См. PHPUnit Mock Objects.
- Используйте видимость
protectedдля методов/свойств, которые вы хотите тестировать в подклассах: Это позволяет переопределять их в тестовых подклассах без открытия доступа, что особенно полезно для имитаций анонимных классов.
Настройка PHPUnit
Сначала настройте PHPUnit в вашем проекте Flight PHP с помощью Composer для удобного тестирования. См. Руководство по началу работы с PHPUnit для подробностей.
-
В каталоге вашего проекта выполните:
composer require --dev phpunit/phpunitЭто установит последнюю версию PHPUnit как зависимость для разработки.
-
Создайте каталог
testsв корне вашего проекта для файлов тестов. -
Добавьте скрипт тестирования в
composer.jsonдля удобства:// остальное содержимое composer.json "scripts": { "test": "phpunit --configuration phpunit.xml" } -
Создайте файл
phpunit.xmlв корне:<?xml version="1.0" encoding="UTF-8"?> <phpunit bootstrap="vendor/autoload.php"> <testsuites> <testsuite name="Flight Tests"> <directory>tests</directory> </testsuite> </testsuites> </phpunit>
Теперь, когда ваши тесты написаны, вы можете запустить composer test для их выполнения.
Тестирование простого обработчика маршрута
Начнем с простого маршрута, который проверяет ввод email пользователя. Мы протестируем его поведение: возврат сообщения об успехе для корректных email и ошибки для некорректных. Для проверки email мы используем filter_var.
// index.php
$app->route('POST /register', [ UserController::class, 'register' ]);
// UserController.php
class UserController {
protected $app;
public function __construct(flight\Engine $app) {
$this->app = $app;
}
public function register() {
$email = $this->app->request()->data->email;
$responseArray = [];
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$responseArray = ['status' => 'error', 'message' => 'Invalid email'];
} else {
$responseArray = ['status' => 'success', 'message' => 'Valid email'];
}
$this->app->json($responseArray);
}
}
Для тестирования создайте файл теста. См. Модульное тестирование и принципы SOLID для получения дополнительной информации о структуре тестов:
// tests/UserControllerTest.php
use PHPUnit\Framework\TestCase;
use Flight;
use flight\Engine;
class UserControllerTest extends TestCase {
public function testValidEmailReturnsSuccess() {
$app = new Engine();
$request = $app->request();
$request->data->email = 'test@example.com'; // Имитация POST-данных
$UserController = new UserController($app);
$UserController->register($request->data->email);
$response = $app->response()->getBody();
$output = json_decode($response, true);
$this->assertEquals('success', $output['status']);
$this->assertEquals('Valid email', $output['message']);
}
public function testInvalidEmailReturnsError() {
$app = new Engine();
$request = $app->request();
$request->data->email = 'invalid-email'; // Имитация POST-данных
$UserController = new UserController($app);
$UserController->register($request->data->email);
$response = $app->response()->getBody();
$output = json_decode($response, true);
$this->assertEquals('error', $output['status']);
$this->assertEquals('Invalid email', $output['message']);
}
}
Ключевые моменты:
- Мы имитируем POST-данные с помощью класса запроса. Не используйте глобальные переменные, такие как
$_POST,$_GETи т.д., так как это усложняет тестирование (вам придется постоянно сбрасывать эти значения, иначе другие тесты могут упасть). - Все контроллеры по умолчанию получают экземпляр
flight\Engine, внедренный в них, даже без настройки контейнера DI. Это значительно упрощает прямое тестирование контроллеров. - Здесь вообще не используется
Flight::, что делает код более простым для тестирования. - Тесты проверяют поведение: правильный статус и сообщение для корректных/некорректных email.
Запустите 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']);
}
}
Ключевые моменты:
- Контроллер зависит от экземпляра
SimplePdoиMailerInterface(предполагаемый сторонний почтовый сервис). - Зависимости внедряются через конструктор, что позволяет избежать глобальных переменных.
Тестирование контроллера с имитациями
Теперь давайте протестируем поведение UserController: проверку email, сохранение в базу данных и отправку писем. Мы имитируем базу данных и почтовый сервис, чтобы изолировать контроллер.
// tests/UserControllerDICTest.php
use flight\database\SimplePdo;
use PHPUnit\Framework\TestCase;
class UserControllerDICTest extends TestCase {
public function testValidEmailSavesAndSendsEmail() {
// Иногда необходимо смешивать стили имитации
// Здесь мы используем встроенную имитацию PHPUnit для PDOStatement
$statementMock = $this->createMock(PDOStatement::class);
$statementMock->method('execute')->willReturn(true);
// Использование анонимного класса для имитации SimplePdo
$mockDb = new class($statementMock) extends SimplePdo {
protected $statementMock;
public function __construct($statementMock) {
$this->statementMock = $statementMock;
}
// При такой имитации мы на самом деле не обращаемся к базе данных.
// Мы можем дополнительно настроить имитацию PDOStatement для имитации сбоев и т.д.
public function runQuery(string $sql, array $params = []): PDOStatement {
return $this->statementMock;
}
};
$mockMailer = new class implements MailerInterface {
public $sentEmail = null;
public function sendWelcome($email): bool {
$this->sentEmail = $email;
return true;
}
};
$app = new Engine();
$app->request()->data->email = 'test@example.com';
$controller = new UserControllerDIC($app, $mockDb, $mockMailer);
$controller->register();
$response = $app->response()->getBody();
$result = json_decode($response, true);
$this->assertEquals('success', $result['status']);
$this->assertEquals('User registered', $result['message']);
$this->assertEquals('test@example.com', $mockMailer->sentEmail);
}
public function testInvalidEmailSkipsSaveAndEmail() {
$mockDb = new class() extends SimplePdo {
// Пустой конструктор обходит родительский конструктор
public function __construct() {}
public function runQuery(string $sql, array $params = []): PDOStatement {
throw new Exception('Should not be called');
}
};
$mockMailer = new class implements MailerInterface {
public $sentEmail = null;
public function sendWelcome($email): bool {
throw new Exception('Should not be called');
}
};
$app = new Engine();
$app->request()->data->email = 'invalid-email';
// Необходимо сопоставить jsonHalt, чтобы избежать выхода
$app->map('jsonHalt', function($data) use ($app) {
$app->json($data, 400);
});
$controller = new UserControllerDIC($app, $mockDb, $mockMailer);
$controller->register();
$response = $app->response()->getBody();
$result = json_decode($response, true);
$this->assertEquals('error', $result['status']);
$this->assertEquals('Invalid email', $result['message']);
}
}
Ключевые моменты:
- Мы имитируем
SimplePdoиMailerInterface, чтобы избежать реальных вызовов базы данных или отправки писем. - Тесты проверяют поведение: корректные email запускают вставки в базу данных и отправку писем; некорректные email пропускают оба действия.
- Имитируйте сторонние зависимости (например,
SimplePdo,MailerInterface), позволяя логике контроллера выполняться.
Чрезмерная имитация
Будьте осторожны, не имитируйте слишком большую часть вашего кода. Приведу пример, почему это может быть плохо, на основе нашего UserController. Мы изменим проверку на метод isEmailValid (используя filter_var), а остальные новые добавления в отдельный метод registerUser.
use flight\database\SimplePdo;
use flight\Engine;
// UserControllerDICV2.php
class UserControllerDICV2 {
protected $app;
protected $db;
protected $mailer;
public function __construct(Engine $app, SimplePdo $db, MailerInterface $mailer) {
$this->app = $app;
$this->db = $db;
$this->mailer = $mailer;
}
public function register() {
$email = $this->app->request()->data->email;
if (!$this->isEmailValid($email)) {
// добавление return здесь помогает модульному тестированию остановить выполнение
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.
Частые ошибки
- Чрезмерная имитация: Не имитируйте каждую зависимость; позвольте некоторой логике (например, проверке контроллера) выполняться, чтобы тестировать реальное поведение. См. Модульное тестирование и принципы SOLID.
- Глобальное состояние: Активное использование глобальных PHP-переменных (например,
$_SESSION,$_COOKIE) делает тесты хрупкими. То же самое касаетсяFlight::. Выполните рефакторинг, чтобы передавать зависимости явно. - Сложная настройка: Если настройка теста громоздка, возможно, у вашего класса слишком много зависимостей или обязанностей, что нарушает принципы SOLID.
Масштабирование с помощью модульных тестов
Модульные тесты особенно полезны в крупных проектах или при возвращении к коду спустя месяцы. Они документируют поведение и выявляют регрессии, избавляя вас от необходимости заново изучать приложение. Для разработчиков-одиночек тестируйте критические пути (например, регистрацию пользователей, обработку платежей). Для команд тесты обеспечивают согласованное поведение при внесении изменений. См. Зачем нужны фреймворки? для получения дополнительной информации о преимуществах использования фреймворков и тестов.
Внесите свои собственные советы по тестированию в репозиторий документации Flight PHP!
Автор: n0nag0n 2025
Guides/blog
Создание простого блога с Flight PHP
Это руководство проведет вас через создание базового блога с использованием PHP-фреймворка Flight. Вы настроите проект, определите маршруты, будете управлять постами с помощью JSON и отображать их с помощью шаблонизатора Latte — все это демонстрирует простоту и гибкость Flight. В итоге у вас будет работающий блог с главной страницей, отдельными страницами постов и формой создания.
Предварительные требования
- PHP 7.4+: Установлен в вашей системе.
- Composer: Для управления зависимостями.
- Текстовый редактор: Любой редактор, например VS Code или PHPStorm.
- Базовые знания PHP и веб-разработки.
Шаг 1: Настройка проекта
Начните с создания нового каталога проекта и установки Flight через Composer.
-
Создайте каталог:
mkdir flight-blog cd flight-blog -
Установите Flight:
composer require flightphp/core -
Создайте общедоступный каталог: Flight использует единую точку входа (
index.php). Создайте папкуpublic/для него:mkdir public -
Базовый
index.php: Создайтеpublic/index.phpс простым маршрутом «hello world»:<?php require '../vendor/autoload.php'; Flight::route('/', function () { echo 'Hello, Flight!'; }); Flight::start(); -
Запустите встроенный сервер: Проверьте настройку с помощью встроенного сервера разработки PHP:
php -S localhost:8000 -t public/Перейдите по адресу
http://localhost:8000, чтобы увидеть «Hello, Flight!».
Шаг 2: Организация структуры проекта
Для аккуратной настройки структурируйте проект следующим образом:
flight-blog/
├── app/
│ ├── config/
│ └── views/
├── data/
├── public/
│ └── index.php
├── vendor/
└── composer.json
app/config/: Файлы конфигурации (например, события, маршруты).app/views/: Шаблоны для отображения страниц.data/: JSON-файл для хранения записей блога.public/: Корень веб-сайта сindex.php.
Шаг 3: Установка и настройка Latte
Latte — это легкий шаблонизатор, который хорошо интегрируется с Flight.
-
Установите Latte:
composer require latte/latte -
Настройте Latte во Flight: Обновите
public/index.php, чтобы зарегистрировать Latte в качестве шаблонизатора представлений:<?php require '../vendor/autoload.php'; use Latte\Engine; Flight::register('view', Engine::class, [], function ($latte) { $latte->setTempDirectory(__DIR__ . '/../cache/'); $latte->setLoader(new \Latte\Loaders\FileLoader(__DIR__ . '/../app/views/')); }); Flight::route('/', function () { Flight::view()->render('home.latte', ['title' => 'My Blog']); }); Flight::start(); -
Создайте шаблон макета: В
app/views/layout.latte:<!DOCTYPE html> <html> <head> <title>{$title}</title> </head> <body> <header> <h1>My Blog</h1> <nav> <a href="/">Home</a> | <a href="/create">Create a Post</a> </nav> </header> <main> {block content}{/block} </main> <footer> <p>© {date('Y')} Flight Blog</p> </footer> </body> </html> -
Создайте домашний шаблон: В
app/views/home.latte:{extends 'layout.latte'} {block content} <h2>{$title}</h2> <ul> {foreach $posts as $post} <li><a href="/post/{$post['slug']}">{$post['title']}</a></li> {/foreach} </ul> {/block}Перезапустите сервер, если вы его остановили, и перейдите по адресу
http://localhost:8000, чтобы увидеть отрендеренную страницу. -
Создайте файл данных: Используйте JSON-файл для имитации базы данных для простоты.
В
data/posts.json:[ { "slug": "first-post", "title": "My First Post", "content": "This is my very first blog post with Flight PHP!" } ]
Шаг 4: Определение маршрутов
Вынесите ваши маршруты в отдельный файл конфигурации для лучшей организации.
-
Создайте
routes.php: Вapp/config/routes.php:<?php Flight::route('/', function () { Flight::view()->render('home.latte', ['title' => 'My Blog']); }); Flight::route('/post/@slug', function ($slug) { Flight::view()->render('post.latte', ['title' => 'Post: ' . $slug, 'slug' => $slug]); }); Flight::route('GET /create', function () { Flight::view()->render('create.latte', ['title' => 'Create a Post']); }); -
Обновите
index.php: Подключите файл маршрутов:<?php require '../vendor/autoload.php'; use Latte\Engine; Flight::register('view', Engine::class, [], function ($latte) { $latte->setTempDirectory(__DIR__ . '/../cache/'); $latte->setLoader(new \Latte\Loaders\FileLoader(__DIR__ . '/../app/views/')); }); require '../app/config/routes.php'; Flight::start();
Шаг 5: Сохранение и получение записей блога
Добавьте методы для загрузки и сохранения записей.
-
Добавьте метод Posts: В
index.phpдобавьте метод для загрузки записей:Flight::map('posts', function () { $file = __DIR__ . '/../data/posts.json'; return json_decode(file_get_contents($file), true); }); -
Обновите маршруты: Измените
app/config/routes.php, чтобы использовать записи:<?php Flight::route('/', function () { $posts = Flight::posts(); Flight::view()->render('home.latte', [ 'title' => 'My Blog', 'posts' => $posts ]); }); Flight::route('/post/@slug', function ($slug) { $posts = Flight::posts(); $post = array_filter($posts, fn($p) => $p['slug'] === $slug); $post = reset($post) ?: null; if (!$post) { Flight::notFound(); return; } Flight::view()->render('post.latte', [ 'title' => $post['title'], 'post' => $post ]); }); Flight::route('GET /create', function () { Flight::view()->render('create.latte', ['title' => 'Create a Post']); });
Шаг 6: Создание шаблонов
Обновите шаблоны для отображения записей.
-
Страница записи (
app/views/post.latte):{extends 'layout.latte'} {block content} <h2>{$post['title']}</h2> <div class="post-content"> <p>{$post['content']}</p> </div> {/block}
Шаг 7: Добавление создания записей
Обработайте отправку формы для добавления новых записей.
-
Создайте форму (
app/views/create.latte):{extends 'layout.latte'} {block content} <h2>{$title}</h2> <form method="POST" action="/create"> <div class="form-group"> <label for="title">Title:</label> <input type="text" name="title" id="title" required> </div> <div class="form-group"> <label for="content">Content:</label> <textarea name="content" id="content" required></textarea> </div> <button type="submit">Save Post</button> </form> {/block} -
Добавьте POST-маршрут: В
app/config/routes.php:Flight::route('POST /create', function () { $request = Flight::request(); $title = $request->data['title']; $content = $request->data['content']; $slug = strtolower(str_replace(' ', '-', $title)); $posts = Flight::posts(); $posts[] = ['slug' => $slug, 'title' => $title, 'content' => $content]; file_put_contents(__DIR__ . '/../../data/posts.json', json_encode($posts, JSON_PRETTY_PRINT)); Flight::redirect('/'); }); -
Проверьте:
- Перейдите по адресу
http://localhost:8000/create. - Отправьте новую запись (например, «Second Post» с некоторым содержимым).
- Проверьте главную страницу, чтобы увидеть её в списке.
- Перейдите по адресу
Шаг 8: Улучшение с помощью обработки ошибок
Переопределите метод notFound для лучшего отображения ошибки 404.
В index.php:
Flight::map('notFound', function () {
Flight::view()->render('404.latte', ['title' => 'Page Not Found']);
});
Создайте app/views/404.latte:
{extends 'layout.latte'}
{block content}
<h2>404 - {$title}</h2>
<p>Sorry, that page doesn't exist!</p>
{/block}
Следующие шаги
- Добавьте стили: Используйте CSS в шаблонах для лучшего внешнего вида.
- База данных: Замените
posts.jsonна базу данных, например SQLite, с помощью SimplePdo. - Валидация: Добавьте проверки на дублирующиеся слаги или пустые поля.
- Middleware: Реализуйте аутентификацию для создания записей.
Заключение
Вы создали простой блог с помощью Flight PHP! Это руководство демонстрирует основные возможности, такие как маршрутизация, шаблонизация с Latte и обработка отправки форм — всё это остается легковесным. Изучите документацию Flight, чтобы узнать о более продвинутых функциях и развить ваш блог дальше!
License
Лицензия MIT (MIT)
Авторское право © 2024 @mikecao, @n0nag0n
Настоящим предоставляется разрешение на бесплатное использование любому лицу, получившему копию данного программного обеспечения и сопроводительной документации (далее - "Программное обеспечение"), без ограничений, включая право использовать, копировать, изменять, объединять, публиковать, распространять, подлицензировать и/или продавать копии Программного обеспечения и разрешать лицам, которым предоставляется Программное обеспечение, сделать то же самое, при соблюдении следующих условий:
Вышеприведенное уведомление об авторском праве и это уведомление о разрешении должны быть включены во все копии или существенные части Программного обеспечения.
ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ ПРЕДОСТАВЛЯЕТСЯ "КАК ЕСТЬ", БЕЗ КАКИХ-ЛИБО ГАРАНТИЙ, ВЫРАЖЕННЫХ ИЛИ ПОДРАЗУМЕВАЕМЫХ, ВКЛЮЧАЯ, НО НЕ ОГРАНИЧИВАЯСЬ ГАРАНТИЯМИ ТОВАРНОГО СОСТОЯНИЯ, ПРИГОДНОСТИ ДЛЯ КОНКРЕТНОЙ ЦЕЛИ И НЕНАРУШЕНИЯ. НИ В КОЕМ СЛУЧАЕ АВТОРЫ ИЛИ ПРАВООБЛАДАТЕЛИ НЕ НЕСУТ ОТВЕТСТВЕННОСТИ ПО ОТВЕТСТВЕННОСТИ, ВЫТЕКАЮЩЕЙ ИЗ ДОГОВОРА, ДЕЛИКТА ИЛИ ИНАЧЕ, В СВЯЗИ С ПРОГРАММНЫМ ОБЕСПЕЧЕНИЕМ ИЛИ ИСПОЛЬЗОВАНИЕМ ИЛИ ДРУГИМИ ОБРАЩЕНИЯМИ С ПРОГРАММНЫМ ОБЕСПЕЧЕНИЕМ.
About
Flight PHP Framework
Flight — это быстрый, простой и расширяемый фреймворк для PHP, созданный для разработчиков, которые хотят быстро достигать результатов без лишней суеты. Независимо от того, создаёте ли вы классическое веб-приложение, молниеносное API или работаете в паре с AI-ассистентами, низкое потребление ресурсов и понятная архитектура Flight делают его идеальным выбором. Flight задуман как лёгкий инструмент, но при этом способен справляться с требованиями enterprise-архитектуры.
Почему стоит выбрать Flight?
- Дружелюбен к новичкам: Flight — отличная отправная точка для начинающих PHP-разработчиков. Его понятная структура и простой синтаксис помогают изучать веб-разработку без запутанных шаблонов.
- Любим профи: Опытные разработчики ценят Flight за гибкость и контроль. Вы можете масштабировать проект от крошечного прототипа до полноценного приложения, не меняя фреймворк.
- Обратная совместимость: Мы ценим ваше время. Flight v3 — это эволюция v2 с сохранением практически всего API. Мы верим в эволюцию, а не революцию — никаких «разрушений мира» при выходе новых версий.
- Ноль зависимостей: Ядро Flight полностью свободно от зависимостей — без полифилов, внешних пакетов и даже PSR-интерфейсов. Это означает меньше векторов атак, меньший размер и отсутствие неожиданных изменений из-за обновлений зависимостей. Опциональные плагины могут иметь зависимости, но ядро всегда остаётся лёгким и безопасным.
- Дружелюбен к AI: Небольшая поверхность API и официальный скелет (одна разметка,
AGENTS.md, внедрение через конструктор) упрощают работу AI-инструментов. Один и тот же код — пишете ли вы вручную или работаете с агентом. Подробнее об использовании AI с Flight.
Обзор в видео
Быстрый старт
Для быстрой минимальной установки используйте Composer:
composer require flightphp/core
Или скачайте zip-архив репозитория здесь. После этого у вас будет базовый файл index.php:
<?php
// если установлено через composer
require 'vendor/autoload.php';
// или если установлено вручную через zip-архив
// require 'flight/Flight.php';
Flight::route('/', function() {
echo 'hello world!';
});
Flight::route('/json', function() {
Flight::json([
'hello' => 'world'
]);
});
Flight::start();
Вот и всё! У вас есть базовое приложение на Flight. Теперь можно запустить файл командой php -S localhost:8000 и открыть http://localhost:8000 в браузере, чтобы увидеть результат.
Короткие примеры с Flight:: удобны для обучения и микроприложений. Для полноценной структуры проекта, удобной как человеку, так и AI-инструментам, используйте скелет ниже.
Скелет/Boilerplate-приложение
Есть официальный стартовый шаблон, который поможет начать любой новый проект на Flight. Он сразу задаёт структуру, конфигурацию, Composer-скрипты и инструкции для AI.
Ознакомьтесь с flightphp/skeleton, чтобы получить готовый проект, или посетите страницу примеров для вдохновения. Хотите узнать о AI-подходе? Изучите AI и опыт разработчика.
Что вы получите (основное):
- Пространства имён
App\с папками в PascalCase (app/Controller/,app/Middleware/,app/Model/, …) — регистр папок должен совпадать с пространством имён (см. Автозагрузка) - Внедрение через Dice +
Engine— контроллеры остаются тестируемыми (предпочтительно использовать$this->app, а неFlight::в коде приложения) - Шаблоны Twig, SimplePdo + пример ActiveRecord, Runway migrate
- Корневой
AGENTS.md(и локальные копии) иSECURITY.mdдля ассистентов и политики безопасности
Установка скелета приложения
Всё очень просто!
# Создаём новый проект
composer create-project flightphp/skeleton my-project/
# Переходим в каталог нового проекта
cd my-project/
# Запускаем локальный сервер разработки
composer start
Скрипт создаст структуру проекта, скопирует config_sample.php → config.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
И Discord
Участие в разработке
Есть два способа внести свой вклад в Flight:
- Участвовать в разработке ядра фреймворка, посетив репозиторий ядра.
- Помочь улучшить документацию! Этот сайт документации размещён на 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
Преимущества
- Легкий, автономный и простой
- Весь код в одном файле - без лишних драйверов.
- Безопасный - каждый созданный файл кэша имеет PHP-заголовок с die, что делает прямой доступ невозможным, даже если кто-то знает путь и ваш сервер настроен неправильно
- Хорошо документирован и протестирован
- Корректно обрабатывает параллелизм через flock
- Поддерживает PHP 7.4+
- Бесплатно под лицензией MIT
Этот сайт документации использует эту библиотеку для кэширования каждой страницы!
Нажмите здесь для просмотра кода.
Установка
Установите через composer:
composer require flightphp/cache
Использование
Использование довольно простое. Это сохраняет файл кэша в директории кэша.
use flight\Cache;
$app = Flight::app();
// Вы передаете директорию, в которой будет храниться кэш, в конструктор
$app->register('cache', Cache::class, [ __DIR__ . '/../cache/' ], function(Cache $cache) {
// Это гарантирует, что кэш используется только в режиме production
// ENVIRONMENT - это константа, которая устанавливается в вашем bootstrap файле или в другом месте вашего приложения
$cache->setDevMode(ENVIRONMENT === 'development');
});
Получение значения кэша
Вы используете метод get() для получения кэшированного значения. Если вам нужен удобный метод, который обновит кэш, если он истек, вы можете использовать refreshIfExpired().
// Получение экземпляра кэша
$cache = Flight::cache();
$data = $cache->refreshIfExpired('simple-cache-test', function () {
return date("H:i:s"); // 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
Основные параметры конфигурации:
command: Команда для запуска вашего рабочего процессаdirectory: Рабочая директория для рабочего процессаautostart: Автоматически запускать при запуске supervisordautorestart: Автоматически перезапускать, если процесс выходитstartretries: Количество попыток перезапуска, если он завершится с ошибкойstderr_logfile/stdout_logfile: Пути к файлам журналовuser: Системный пользователь, от имени которого будет запущен процессnumprocs: Количество экземпляров рабочего процесса для запускаprocess_name: Форматирование имен для нескольких процессов рабочих
Управление рабочими процессами с помощью Supervisorctl
После создания или изменения конфигурации:
# Перезагрузить конфигурацию супервайзера
sudo supervisorctl reread
sudo supervisorctl update
# Управление конкретными процессами рабочих
sudo supervisorctl start email_worker:*
sudo supervisorctl stop email_worker:*
sudo supervisorctl restart email_worker:*
sudo supervisorctl status email_worker:*
Запуск нескольких конвейеров
Для нескольких конвейеров создайте отдельные файлы рабочих процессов и конфигурации:
[program:email_worker]
command=php /path/to/email_worker.php
# ... другие настройки ...
[program:notification_worker]
command=php /path/to/notification_worker.php
# ... другие настройки ...
Мониторинг и журналы
Проверьте журналы для мониторинга активности рабочих процессов:
# Просмотр журналов
sudo tail -f /var/log/simple_job_queue.log
# Проверка состояния
sudo supervisorctl status
Эта настройка гарантирует, что ваши рабочие процессы задач продолжают работать даже после сбоев, перезагрузок сервера или других проблем, делая вашу систему очередей надежной для производственных сред.
Awesome-plugins/jwt
Firebase JWT - Аутентификация с использованием JSON Web Token
JWT (JSON Web Tokens) — это компактный и безопасный для URL способ представления утверждений между вашим приложением и клиентом. Они идеально подходят для аутентификации stateless API — без необходимости хранения сессий на сервере! Это руководство показывает, как интегрировать Firebase JWT с Flight для безопасной аутентификации на основе токенов.
Посетите репозиторий на Github для полной документации и деталей.
Что такое JWT?
JSON Web Token — это строка, содержащая три части:
- Заголовок: Метаданные о токене (алгоритм, тип)
- Полезная нагрузка: Ваши данные (ID пользователя, роли, срок истечения и т.д.)
- Подпись: Криптографическая подпись для проверки подлинности
Пример JWT: eyJ0eXAiOiJKV1QiLCJhbGc... (выглядит как бессмыслица, но это структурированные данные!)
Почему использовать JWT?
- Stateless: Не требуется хранение сессий на сервере — идеально для микросервисов и API
- Масштабируемость: Отлично работает с балансировщиками нагрузки, поскольку нет требования к affinity сессий
- Кросс-доменная: Может использоваться через разные домены и сервисы
- Дружественно к мобильным: Отлично для мобильных приложений, где куки могут не работать хорошо
- Стандартизировано: Стандарт отрасли (RFC 7519)
Установка
Установите через Composer:
composer require firebase/php-jwt
Базовое использование
Вот быстрый пример создания и проверки JWT:
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
// Ваш секретный ключ (ХРАНИТЕ ЕГО В БЕЗОПАСНОМ МЕСТЕ!)
$secretKey = 'your-256-bit-secret-key-here-keep-it-safe';
// Создание токена
$payload = [
'user_id' => 123,
'username' => 'johndoe',
'role' => 'admin',
'iat' => time(), // Выдан в
'exp' => time() + 3600 // Истекает через 1 час
];
$jwt = JWT::encode($payload, $secretKey, 'HS256');
echo "Token: " . $jwt;
// Проверка и декодирование токена
try {
$decoded = JWT::decode($jwt, new Key($secretKey, 'HS256'));
echo "User ID: " . $decoded->user_id;
} catch (Exception $e) {
echo "Invalid token: " . $e->getMessage();
}
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)
- HS256 (Рекомендуется для большинства приложений): Использует один секретный ключ
- HS384, HS512: Более сильные варианты
$jwt = JWT::encode($payload, $secretKey, 'HS256');
$decoded = JWT::decode($jwt, new Key($secretKey, 'HS256'));
Асимметричные алгоритмы (RSA/ECDSA)
- RS256, RS384, RS512: Использует пары публичный/приватный ключ
- ES256, ES384, ES512: Варианты на эллиптических кривых
// Генерация ключей: openssl genrsa -out private.key 2048
// openssl rsa -in private.key -pubout -out public.key
$privateKey = file_get_contents('/path/to/private.key');
$publicKey = file_get_contents('/path/to/public.key');
// Кодирование с приватным ключом
$jwt = JWT::encode($payload, $privateKey, 'RS256');
// Декодирование с публичным ключом
$decoded = JWT::decode($jwt, new Key($publicKey, 'RS256'));
Когда использовать RSA: Используйте RSA, когда нужно распространить публичный ключ для проверки (например, микросервисы, интеграции с третьими сторонами). Для одного приложения HS256 проще и достаточно.
Устранение неисправностей
Ошибка "Expired token"
Утверждение exp вашего токена в прошлом. Выдайте новый токен или реализуйте обновление токена.
"Signature verification failed"
- Вы используете другой секретный ключ для декодирования, чем для кодирования
- Токен был изменен
- Разница во времени между серверами (добавьте буфер leeway)
use Firebase\JWT\JWT;
JWT::$leeway = 60; // Разрешить 60 секунд разницы во времени
$decoded = JWT::decode($jwt, new Key($secretKey, 'HS256'));
Токен не отправляется в запросах
Убедитесь, что ваш клиент отправляет заголовок Authorization:
// Пример на JavaScript
fetch('/api/users', {
headers: {
'Authorization': 'Bearer ' + token
}
});
Методы
Библиотека Firebase JWT предоставляет эти основные методы:
JWT::encode(array $payload, string $key, string $alg): Создает JWT из полезной нагрузкиJWT::decode(string $jwt, Key $key): Декодирует и проверяет JWTJWT::urlsafeB64Encode(string $input): Base64 кодирование безопасное для URLJWT::urlsafeB64Decode(string $input): Base64 декодирование безопасное для URLJWT::$leeway: Статическое свойство для установки временного leeway для валидации (в секундах)
Почему использовать эту библиотеку?
- Стандарт отрасли: Firebase JWT — самая популярная и широко доверенная библиотека JWT для PHP
- Активное обслуживание: Поддерживается командой Google/Firebase
- Фокус на безопасности: Регулярные обновления и патчи безопасности
- Простой API: Легко понять и реализовать
- Хорошо документировано: Обширная документация и поддержка сообщества
- Гибко: Поддерживает несколько алгоритмов и настраиваемые опции
См. также
- Репозиторий Firebase JWT на Github
- JWT.io - Отладка и декодирование JWT
- RFC 7519 - Официальная спецификация JWT
- Документация по Middleware Flight
- Плагин сессий Flight - Для традиционной аутентификации на основе сессий
Лицензия
Библиотека Firebase JWT лицензирована по BSD 3-Clause License. См. репозиторий на Github для деталей.
Awesome-plugins/n0nag0n_wordpress
Интеграция с WordPress: n0nag0n/wordpress-integration-for-flight-framework
Хотите использовать Flight PHP внутри вашего сайта WordPress? Этот плагин делает это очень простым! С n0nag0n/wordpress-integration-for-flight-framework вы можете запустить полноценное приложение Flight прямо рядом с вашей установкой WordPress — идеально для создания пользовательских API, микросервисов или даже полноценных приложений, не выходя из комфорта WordPress.
Что он делает?
- Безупречно интегрирует Flight PHP с WordPress
- Направляет запросы либо на Flight, либо на WordPress в зависимости от шаблонов URL
- Организует ваш код с контроллерами, моделями и представлениями (MVC)
- Легко настраивает рекомендуемую структуру папок Flight
- Использует соединение с базой данных WordPress или свою собственную
- Тонко настраивает взаимодействие между Flight и WordPress
- Простой интерфейс администрирования для конфигурации
Установка
- Загрузите папку
flight-integrationв ваш каталог/wp-content/plugins/. - Активируйте плагин в админ-панели WordPress (меню Плагины).
- Перейдите в Настройки > Flight Framework, чтобы настроить плагин.
- Укажите путь к установке Flight (или используйте Composer для установки Flight).
- Настройте путь к папке вашего приложения и создайте структуру папок (плагин может помочь с этим!).
- Начните создавать ваше приложение Flight!
Примеры использования
Пример базового маршрута
В вашем файле app/config/routes.php:
Flight::route('GET /api/hello', function() {
Flight::json(['message' => 'Hello World!']);
});
Пример контроллера
Создайте контроллер в app/controllers/ApiController.php:
namespace app\controllers;
use Flight;
class ApiController {
public function getUsers() {
// Вы можете использовать функции WordPress внутри Flight!
$users = get_users();
$result = [];
foreach($users as $user) {
$result[] = [
'id' => $user->ID,
'name' => $user->display_name,
'email' => $user->user_email
];
}
Flight::json($result);
}
}
Затем, в вашем routes.php:
Flight::route('GET /api/users', [app\controllers\ApiController::class, 'getUsers']);
ЧАВО
В: Мне нужно знать Flight, чтобы использовать этот плагин?
О: Да, это для разработчиков, которые хотят использовать Flight в WordPress. Рекомендуется базовое знание маршрутизации и обработки запросов Flight.
В: Это замедлит мой сайт WordPress?
О: Нет! Плагин обрабатывает только запросы, которые соответствуют вашим маршрутам Flight. Все остальные запросы идут в WordPress как обычно.
В: Могу ли я использовать функции WordPress в своем приложении Flight?
О: Абсолютно! У вас есть полный доступ ко всем функциям, хукам и глобальным переменным WordPress из ваших маршрутов и контроллеров Flight.
В: Как создать пользовательские маршруты?
О: Определите свои маршруты в файле config/routes.php в папке вашего приложения. Посмотрите образец файла, созданный генератором структуры папок, для примеров.
Журнал изменений
1.0.0
Первоначальный релиз.
Для получения дополнительной информации посетите GitHub repo.
Awesome-plugins/ghost_session
Ghostff/Session
PHP Менеджер сессий (неблокирующий, 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 ниже для готовых конфигураций копирования-вставки для самых популярных инструментов.
Что он делает
После подключения ваш ИИ-ассистент может:
- Просматривать все доступные документы — перечислять все основные темы, руководства и страницы плагинов
- Получать любую страницу документации — извлекать полный контент для маршрутизации, middleware, запросов, безопасности и многое другое
- Искать документацию плагинов — получать полную документацию для ActiveRecord, Session, Tracy, Runway и всех других официальных плагинов
- Следовать пошаговым руководствам — получать доступ к полным пошаговым инструкциям по созданию блогов, REST API и тестируемых приложений
- Искать по всему — находить релевантные страницы по основным документам, руководствам и плагинам одновременно
Ключевые моменты
- Нулевая настройка — хостинговый сервер по адресу
https://mcp.flightphp.com/mcpне требует установки или API-ключей. - Всегда актуальный — сервер запрашивает документы в реальном времени с docs.flightphp.com, поэтому всегда обновлен.
- Работает везде — любой инструмент, поддерживающий транспорт MCP Streamable HTTP, может подключиться.
- Самохостинг — запустите свою собственную инстанцию с PHP >= 8.1 и Composer, если предпочитаете.
Конфигурация 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 (или другой асинхронный драйвер) для продакшена с минимальными изменениями.
Требования
- PHP 7.4 или выше
- Фреймворк Flight 3.16.1 или выше
- Расширение Swoole
Установка
Установите через composer:
composer require flightphp/async
Если вы планируете запускать с Swoole, установите расширение:
# используя pecl
pecl install swoole
# или openswoole
pecl install openswoole
# или с помощью менеджера пакетов (пример для Debian/Ubuntu)
sudo apt-get install php-swoole
Быстрый пример Swoole
Ниже приведена минимальная настройка, которая показывает, как поддерживать как PHP-FPM (или встроенный сервер), так и Swoole, используя один и тот же код.
Файлы, которые вам понадобятся в проекте:
- index.php
- swoole_server.php
- SwooleServerDriver.php
index.php
Этот файл представляет собой простой переключатель, который заставляет приложение работать в режиме PHP для разработки.
// index.php
<?php
define('NOT_SWOOLE', true);
include 'swoole_server.php';
swoole_server.php
Этот файл инициализирует ваше приложение Flight и запустит драйвер Swoole, когда NOT_SWOOLE не определен.
// swoole_server.php
<?php
require_once __DIR__ . '/vendor/autoload.php';
$app = Flight::app();
$app->route('/', function() use ($app) {
$app->json(['hello' => 'world']);
});
if (!defined('NOT_SWOOLE')) {
// Require the SwooleServerDriver class when running in Swoole mode.
require_once __DIR__ . '/SwooleServerDriver.php';
Swoole\Runtime::enableCoroutine();
$Swoole_Server = new SwooleServerDriver('127.0.0.1', 9501, $app);
$Swoole_Server->start();
} else {
$app->start();
}
SwooleServerDriver.php
Краткий драйвер, показывающий, как связывать запросы Swoole с Flight с использованием AsyncBridge и адаптеров Swoole.
// SwooleServerDriver.php
<?php
use flight\adapter\SwooleAsyncRequest;
use flight\adapter\SwooleAsyncResponse;
use flight\AsyncBridge;
use flight\Engine;
use Swoole\HTTP\Server as SwooleServer;
use Swoole\HTTP\Request as SwooleRequest;
use Swoole\HTTP\Response as SwooleResponse;
class SwooleServerDriver {
protected $Swoole;
protected $app;
public function __construct(string $host, int $port, Engine $app) {
$this->Swoole = new SwooleServer($host, $port);
$this->app = $app;
$this->setDefault();
$this->bindWorkerEvents();
$this->bindHttpEvent();
}
protected function setDefault() {
$this->Swoole->set([
'daemonize' => false,
'dispatch_mode' => 1,
'max_request' => 8000,
'open_tcp_nodelay' => true,
'reload_async' => true,
'max_wait_time' => 60,
'enable_reuse_port' => true,
'enable_coroutine' => true,
'http_compression' => false,
'enable_static_handler' => true,
'document_root' => __DIR__,
'static_handler_locations' => ['/css', '/js', '/images', '/.well-known'],
'buffer_output_size' => 4 * 1024 * 1024,
'worker_num' => 4,
]);
$app = $this->app;
$app->map('stop', function (?int $code = null) use ($app) {
if ($code !== null) {
$app->response()->status($code);
}
});
}
protected function bindHttpEvent() {
$app = $this->app;
$AsyncBridge = new AsyncBridge($app);
$this->Swoole->on('Start', function(SwooleServer $server) {
echo "Swoole http server is started at http://127.0.0.1:9501\n";
});
$this->Swoole->on('Request', function (SwooleRequest $request, SwooleResponse $response) use ($AsyncBridge) {
$SwooleAsyncRequest = new SwooleAsyncRequest($request);
$SwooleAsyncResponse = new SwooleAsyncResponse($response);
$AsyncBridge->processRequest($SwooleAsyncRequest, $SwooleAsyncResponse);
$response->end();
gc_collect_cycles();
});
}
protected function bindWorkerEvents() {
$createPools = function() {
// create worker-specific connection pools here
};
$closePools = function() {
// close pools / cleanup here
};
$this->Swoole->on('WorkerStart', $createPools);
$this->Swoole->on('WorkerStop', $closePools);
$this->Swoole->on('WorkerError', $closePools);
}
public function start() {
$this->Swoole->start();
}
}
Запуск сервера
- Разработка (встроенный сервер PHP / PHP-FPM):
- php -S localhost:8000 (или добавьте -t public/, если ваш index находится в public/)
- Продакшен (Swoole):
- php swoole_server.php
Совет: Для продакшена используйте обратный прокси (Nginx) перед Swoole для обработки TLS, статических файлов и балансировки нагрузки.
Заметки по конфигурации
Драйвер Swoole предоставляет несколько опций конфигурации:
- worker_num: количество рабочих процессов
- max_request: запросов на работника перед перезапуском
- enable_coroutine: использование корутин для параллелизма
- buffer_output_size: размер буфера вывода
Настройте эти параметры в соответствии с ресурсами хоста и шаблонами трафика.
Обработка ошибок
AsyncBridge преобразует ошибки Flight в правильные HTTP-ответы. Вы также можете добавить обработку ошибок на уровне маршрута:
$app->route('/*', function() use ($app) {
try {
// route logic
} catch (Exception $e) {
$app->response()->status(500);
$app->json(['error' => $e->getMessage()]);
}
});
AdapterMan и другие рантаймы
AdapterMan поддерживается как альтернативный адаптер рантайма. Пакет спроектирован для адаптивности — добавление или использование других адаптеров в целом следует тому же шаблону: преобразование запроса/ответа сервера в запрос/ответ Flight через AsyncBridge и адаптеры, специфичные для рантайма.
Awesome-plugins/migrations
Миграции
Миграция для вашего проекта отслеживает все изменения базы данных, связанные с вашим проектом. byjg/php-migration — это действительно полезная основная библиотека, с которой вы можете начать.
Установка
PHP библиотека
Если вы хотите использовать только PHP библиотеку в вашем проекте:
composer require "byjg/migration"
Интерфейс командной строки
Интерфейс командной строки является отдельным и не требует установки вместе с вашим проектом.
Вы можете установить его глобально и создать символическую ссылку.
composer require "byjg/migration-cli"
Пожалуйста, посетите byjg/migration-cli, чтобы получить больше информации о Migration CLI.
Поддерживаемые базы данных
| База данных | Драйвер | Строка соединения |
|---|---|---|
| Sqlite | pdo_sqlite | sqlite:///path/to/file |
| MySql/MariaDb | pdo_mysql | mysql://username:password@hostname:port/database |
| Postgres | pdo_pgsql | pgsql://username:password@hostname:port/database |
| Sql Server | pdo_dblib, pdo_sysbase Linux | dblib://username:password@hostname:port/database |
| Sql Server | pdo_sqlsrv Windows | sqlsrv://username:password@hostname:port/database |
Как это работает?
Миграция базы данных использует ЧИСТЫЙ SQL для управления версионностью базы данных. Чтобы это заработало, вам необходимо:
- Создать SQL скрипты
- Управлять, используя командную строку или API.
SQL скрипты
Скрипты делятся на три группы:
- БАЗОВЫЙ скрипт содержит ВСЕ sql команды для создания новой базы данных;
- UP скрипты содержат все sql миграционные команды для "повышения" версии базы данных;
- DOWN скрипты содержат все sql миграционные команды для "понижения" или возврата версии базы данных;
Директория скриптов:
<root dir>
|
+-- base.sql
|
+-- /migrations
|
+-- /up
|
+-- 00001.sql
+-- 00002.sql
+-- /down
|
+-- 00000.sql
+-- 00001.sql
- "base.sql" — это базовый скрипт
- Папка "up" содержит скрипты для миграции вверх. Например: 00002.sql — это скрипт для перемещения базы данных с версии '1' на '2'.
- Папка "down" содержит скрипты для миграции вниз. Например: 00001.sql — это скрипт для перемещения базы данных с версии '2' на '1'. Папка "down" является необязательной.
Многоразвивающая среда
Если вы работаете с несколькими разработчиками и несколькими ветками, будет сложно определить, какое число следующее.
В этом случае вы добавляете суффикс "-dev" после номера версии.
Смотрите сценарий:
- Разработчик 1 создает ветку, и самая последняя версия, например, 42.
- Разработчик 2 создаёт ветку одновременно и имеет тот же номер версии базы данных.
В обоих случаях разработчики создадут файл под названием 43-dev.sql. Оба разработчика будут мигрировать UP и DOWN без проблем, и ваша локальная версия будет 43.
Но разработчик 1 объединил ваши изменения и создал окончательную версию 43.sql (git mv 43-dev.sql 43.sql). Если разработчик 2 обновит свою локальную ветку, он получит файл 43.sql (от dev 1) и ваш файл 43-dev.sql. Если он попытается мигрировать UP или DOWN, скрипт миграции упадет и предупредит его, что есть ДВЕ версии 43. В этом случае разработчик 2 должен будет обновить ваш файл до 44-dev.sql и продолжить работать, пока не объединит ваши изменения и не сгенерирует окончательную версию.
Использование PHP API и интеграция его в ваши проекты
Основное использование:
- Создайте объект ConnectionManagement для соединения. Для получения дополнительной информации смотрите компонент "byjg/anydataset".
- Создайте объект миграции с этим соединением и папкой, в которой находятся sql-скрипты.
- Используйте соответствующую команду для "сброса", "up" или "down" миграционных скриптов.
Смотрите пример:
<?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.dbAwesome-plugins/comment_template
CommentTemplate
CommentTemplate — это мощный шаблонный движок для PHP с компиляцией ресурсов, наследованием шаблонов и обработкой переменных. Он предоставляет простой и гибкий способ управления шаблонами с встроенной минификацией CSS/JS и кэшированием.
Особенности
- Наследование шаблонов: Используйте макеты и включайте другие шаблоны
- Компиляция ресурсов: Автоматическая минификация и кэширование CSS/JS
- Обработка переменных: Переменные шаблонов с фильтрами и командами
- Кодирование Base64: Встраивание ресурсов как data URI
- Интеграция с Flight Framework: Опциональная интеграция с PHP-фреймворком Flight
Установка
Установите с помощью Composer.
composer require knifelemon/comment-template
Базовая конфигурация
Есть некоторые базовые опции конфигурации для начала работы. Вы можете прочитать больше о них в CommentTemplate Repo.
Метод 1: Использование функции обратного вызова
<?php
require_once 'vendor/autoload.php';
use KnifeLemon\CommentTemplate\Engine;
$app = Flight::app();
$app->register('view', Engine::class, [], function (Engine $engine) use ($app) {
// Корневая директория (где находится index.php) — корень документов вашего веб-приложения
$engine->setPublicPath(__DIR__);
// Директория файлов шаблонов — поддерживает как относительные, так и абсолютные пути
$engine->setSkinPath('views'); // Относительно пути public
// Куда будут сохраняться скомпилированные ресурсы — поддерживает как относительные, так и абсолютные пути
$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\
Умное определение путей:
- Относительные пути: Без ведущих разделителей (
/,\) или букв дисков - Абсолютные Unix: Начинаются с
/(например,/var/www/assets) - Абсолютные Windows: Начинаются с буквы диска (например,
C:\www,D:/assets) - UNC-пути: Начинаются с
\\(например,\\server\share)
Как это работает:
- Все пути автоматически разрешаются в зависимости от типа (относительный vs абсолютный)
- Относительные пути объединяются с публичным путем
@cssи@jsсоздают минифицированные файлы в:{resolvedAssetPath}/css/или{resolvedAssetPath}/js/@assetкопирует отдельные файлы в:{resolvedAssetPath}/{relativePath}@assetDirкопирует директории в:{resolvedAssetPath}/{relativePath}- Умное кэширование: файлы копируются только если исходный файл новее целевого
Интеграция с Tracy Debugger
CommentTemplate включает интеграцию с Tracy Debugger для логирования и отладки в разработке.

Установка
composer require tracy/tracy
Использование
<?php
use KnifeLemon\CommentTemplate\Engine;
use Tracy\Debugger;
// Включить Tracy (должно быть вызвано перед любым выводом)
Debugger::enable(Debugger::DEVELOPMENT);
Flight::set('flight.content_length', false);
// Переопределение шаблона
$app->register('view', Engine::class, [], function (Engine $builder) use ($app) {
$builder->setPublicPath($app->get('flight.views.topPath'));
$builder->setAssetPath($app->get('flight.views.assetPath'));
$builder->setSkinPath($app->get('flight.views.path'));
$builder->setFileExtension($app->get('flight.views.extension'));
});
$app->map('render', function(string $template, array $data) use ($app): void {
echo $app->view()->render($template, $data);
});
$app->start();
Функции панели отладки
CommentTemplate добавляет пользовательскую панель в панель отладки Tracy с четырьмя вкладками:
- Overview: Конфигурация, метрики производительности и счетчики
- Assets: Детали компиляции CSS/JS с коэффициентами сжатия
- Variables: Исходные и преобразованные значения с примененными фильтрами
- Timeline: Хронологический вид всех операций шаблонов
Что логируется
- Рендеринг шаблонов (начало/конец, длительность, макеты, импорты)
- Компиляция ассетов (файлы CSS/JS, размеры, коэффициенты сжатия)
- Обработка переменных (исходные/преобразованные значения, фильтры)
- Операции с ассетами (кодирование base64, копирование файлов)
- Метрики производительности (длительность, использование памяти)
Примечание: Нулевое влияние на производительность, когда Tracy не установлен или отключен.
См. полный рабочий пример с Flight PHP.
Директивы шаблонов
Наследование макетов
Используйте макеты для создания общей структуры:
layout/global_layout.php:
<!DOCTYPE html>
<html>
<head>
<title>{$title}</title>
</head>
<body>
<!--@contents-->
</body>
</html>
view/page.php:
<!--@layout(layout/global_layout)-->
<h1>{$title}</h1>
<p>{$content}</p>
Управление ресурсами
CSS-файлы
<!--@css(/css/styles.css)--> <!-- Минифицировано и кэшировано -->
<!--@cssSingle(/css/critical.css)--> <!-- Один файл, не минифицирован -->
JavaScript-файлы
CommentTemplate поддерживает различные стратегии загрузки JavaScript:
<!--@js(/js/script.js)--> <!-- Минифицировано, загружается внизу -->
<!--@jsAsync(/js/analytics.js)--> <!-- Минифицировано, загружается внизу с async -->
<!--@jsDefer(/js/utils.js)--> <!-- Минифицировано, загружается внизу с defer -->
<!--@jsTop(/js/critical.js)--> <!-- Минифицировано, загружается в head -->
<!--@jsTopAsync(/js/tracking.js)--> <!-- Минифицировано, загружается в head с async -->
<!--@jsTopDefer(/js/polyfill.js)--> <!-- Минифицировано, загружается в head с defer -->
<!--@jsSingle(/js/widget.js)--> <!-- Один файл, не минифицирован -->
<!--@jsSingleAsync(/js/ads.js)--> <!-- Один файл, не минифицирован, async -->
<!--@jsSingleDefer(/js/social.js)--> <!-- Один файл, не минифицирован, defer -->
Директивы ресурсов в CSS/JS-файлах
CommentTemplate также обрабатывает директивы ресурсов внутри CSS- и JavaScript-файлов во время компиляции:
Пример CSS:
/* В ваших CSS-файлах */
@font-face {
font-family: 'CustomFont';
src: url('<!--@asset(fonts/custom.woff2)-->') format('woff2');
}
.background-image {
background: url('<!--@asset(images/bg.jpg)-->');
}
.inline-icon {
background: url('<!--@base64(icons/star.svg)-->');
}
Пример JavaScript:
/* В ваших JS-файлах */
const fontUrl = '<!--@asset(fonts/custom.woff2)-->';
const imageData = '<!--@base64(images/icon.png)-->';
Кодирование Base64
<!--@base64(images/logo.png)--> <!-- Встраивается как data URI -->
Пример:
<!-- Встраивание маленьких изображений как data URI для более быстрой загрузки -->
<img src="<!--@base64(images/logo.png)-->" alt="Logo">
<div style="background-image: url('<!--@base64(icons/star.svg)-->');">
Маленькая иконка как фон
</div>
Копирование ресурсов
<!--@asset(images/photo.jpg)--> <!-- Копирование одного ресурса в публичную директорию -->
<!--@assetDir(assets)--> <!-- Копирование всей директории в публичную директорию -->
Пример:
<!-- Копирование и ссылка на статические ресурсы -->
<img src="<!--@asset(images/hero-banner.jpg)-->" alt="Hero Banner">
<a href="<!--@asset(documents/brochure.pdf)-->" download>Скачать брошюру</a>
<!-- Копирование всей директории (шрифты, иконки и т.д.) -->
<!--@assetDir(assets/fonts)-->
<!--@assetDir(assets/icons)-->
Включение шаблонов
<!--@import(components/header)--> <!-- Включение других шаблонов -->
Пример:
<!-- Включение повторно используемых компонентов -->
<!--@import(components/header)-->
<main>
<h1>Добро пожаловать на наш сайт</h1>
<!--@import(components/sidebar)-->
<div class="content">
<p>Основное содержимое здесь...</p>
</div>
</main>
<!--@import(components/footer)-->
Обработка переменных
Базовые переменные
<h1>{$title}</h1>
<p>{$description}</p>
Фильтры переменных
{$title|upper} <!-- Преобразовать в верхний регистр -->
{$content|lower} <!-- Преобразовать в нижний регистр -->
{$html|striptag} <!-- Удалить HTML-теги -->
{$text|escape} <!-- Экранировать HTML -->
{$multiline|nl2br} <!-- Преобразовать переносы строк в <br> -->
{$html|br2nl} <!-- Преобразовать теги <br> в переносы строк -->
{$description|trim} <!-- Удалить пробелы -->
{$subject|title} <!-- Преобразовать в заглавный регистр -->
Команды переменных
{$title|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.
Особенности
- 🔗 Fluent API — Цепочка методов для читаемого построения запросов
- 🛡️ Защита от SQL-инъекций — Автоматическая привязка параметров с подготовленными запросами
- 🔧 Поддержка сырого SQL — Вставка сырых SQL-выражений с помощью
raw() - 📝 Множество типов запросов — SELECT, INSERT, UPDATE, DELETE, COUNT
- 🔀 Поддержка JOIN — INNER, LEFT, RIGHT соединения с алиасами
- 🎯 Расширенные условия — LIKE, IN, NOT IN, BETWEEN, операторы сравнения
- 🌐 Независимость от базы данных — Возвращает SQL + параметры, используйте с любой DB-связью
- 🪶 Легковесность — Минимальный след с нулевыми зависимостями
Установка
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 показывает:
- Общее количество запросов и разбивку по типам
- Сгенерированный SQL (с подсветкой синтаксиса)
- Массив параметров
- Детали запроса (таблица, where, joins и т.д.)
Для полной документации посетите GitHub-репозиторий.
Awesome-plugins/twig
Twig
Twig — это гибкий, быстрый и безопасный шаблонизатор для PHP. Это язык шаблонов, используемый Symfony и многими другими проектами, что означает, что инструменты ИИ-кодирования и большинство PHP-разработчиков уже хорошо знакомы с его синтаксисом. Twig компилирует шаблоны в оптимизированный PHP, автоматически экранирует вывод по умолчанию (отлично для защиты от XSS) и легко расширяется фильтрами, функциями и расширениями.
Установка
Установите с помощью composer.
composer require twig/twig
Базовая настройка
Существуют некоторые базовые параметры настройки, чтобы начать работу. Вы можете узнать больше о них в документации Twig.
require 'vendor/autoload.php';
$app = Flight::app();
$app->map('render', function(string $template, array $data): void {
$loader = new \Twig\Loader\FilesystemLoader(Flight::get('flight.views.path'));
$twig = new \Twig\Environment($loader, [
// Где Twig хранит скомпилированные шаблоны
'cache' => __DIR__ . '/../cache/twig',
// Перекомпилировать шаблоны при изменении исходного кода (удобно при разработке)
'auto_reload' => true,
]);
echo $twig->render($template, $data);
});
Регистрация Twig как класса представления
Если вы предпочитаете повторно использовать одну среду 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">
© 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();
Ключевые аспекты
- Неблокирующее: По умолчанию использует
read_and_closeдля запуска сессии, предотвращая проблемы с блокировкой сессий. - Автоматическая фиксация: Включена по умолчанию, поэтому изменения сохраняются автоматически при завершении, если не отключена.
- Хранение в файлах: Сессии хранятся в каталоге временных файлов системы в
/flight_sessionsпо умолчанию.
Конфигурация
Вы можете настроить обработчик сессий, передавая массив опций при регистрации:
// Да, это двойной массив :)
$app->register('session', Session::class, [ [
'save_path' => '/custom/path/to/sessions', // Каталог для файлов сессий
'prefix' => 'myapp_', // Префикс для файлов сессий
'encryption_key' => 'a-secure-32-byte-key-here', // Включить шифрование (рекомендуется 32 байта для AES-256-CBC)
'auto_commit' => false, // Отключить автоматическую фиксацию для ручного контроля
'start_session' => true, // Автоматически запускать сессию (по умолчанию: true)
'test_mode' => false, // Включить режим тестирования для разработки
'serialization' => 'json', // Метод сериализации: 'json' (по умолчанию) или 'php' (устаревший)
] ]);
Опции конфигурации
| Опция | Описание | Значение по умолчанию |
|---|---|---|
save_path |
Каталог, где хранятся файлы сессий | sys_get_temp_dir() . '/flight_sessions' |
prefix |
Префикс для сохраненного файла сессии | sess_ |
encryption_key |
Ключ для шифрования AES-256-CBC (необязательно) | null (без шифрования) |
auto_commit |
Автоматическое сохранение данных сессии при завершении | true |
start_session |
Автоматически запускать сессию | true |
test_mode |
Запуск в режиме тестирования без влияния на сессии PHP | false |
test_session_id |
Кастомный ID сессии для режима тестирования (необязательно) | Генерируется случайно, если не указано |
serialization |
Метод сериализации: 'json' (по умолчанию, безопасный) или 'php' (устаревший, позволяет объекты) | 'json' |
Режимы сериализации
По умолчанию эта библиотека использует сериализацию JSON для данных сессий, что безопасно и предотвращает уязвимости, связанные с внедрением объектов PHP. Если вам нужно хранить объекты PHP в сессии (не рекомендуется для большинства приложений), вы можете выбрать устаревшую сериализацию PHP:
'serialization' => 'json'(по умолчанию):- Допускаются только массивы и примитивы в данных сессий.
- Безопаснее: устойчив к внедрению объектов PHP.
- Файлы имеют префикс
J(простой JSON) илиF(зашифрованный JSON).
'serialization' => 'php':- Позволяет хранить объекты PHP (используйте с осторожностью).
- Файлы имеют префикс
P(простая сериализация PHP) илиE(зашифрованная сериализация PHP).
Примечание: Если вы используете сериализацию JSON, попытка сохранить объект вызовет исключение.
Продвинутое использование
Ручная фиксация
Если вы отключите автоматическую фиксацию, вам нужно вручную фиксировать изменения:
$app->register('session', Session::class, ['auto_commit' => false]);
Flight::route('/update', function() {
$session = Flight::session();
$session->set('key', 'value');
$session->commit(); // Явно сохранить изменения
});
Безопасность сессий с шифрованием
Включите шифрование для конфиденциальных данных:
$app->register('session', Session::class, [
'encryption_key' => 'your-32-byte-secret-key-here'
]);
Flight::route('/secure', function() {
$session = Flight::session();
$session->set('credit_card', '4111-1111-1111-1111'); // Шифруется автоматически
echo $session->get('credit_card'); // Дешифруется при извлечении
});
Регенерация сессии
Регенерируйте 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 предоставляет эти методы:
set(string $key, $value): Сохраняет значение в сессии.get(string $key, $default = null): Извлекает значение, с необязательным значением по умолчанию, если ключ не существует.delete(string $key): Удаляет конкретный ключ из сессии.clear(): Удаляет все данные сессии, но сохраняет то же имя файла для сессии.commit(): Сохраняет текущие данные сессии в файловую систему.id(): Возвращает текущий ID сессии.regenerate(bool $deleteOldFile = false): Регенерирует ID сессии, включая создание нового файла сессии, сохраняя все старые данные, а старый файл остается в системе. Если$deleteOldFileравенtrue, старый файл сессии удаляется.destroy(string $id): Уничтожает сессию по ID и удаляет файл сессии из системы. Это частьSessionHandlerInterface, и$idобязателен. Типичное использование:$session->destroy($session->id()).getAll(): Возвращает все данные из текущей сессии.
Все методы, кроме get() и id(), возвращают экземпляр Session для цепочки вызовов.
Почему использовать этот плагин?
- Легковесный: Нет внешних зависимостей — только файлы.
- Неблокирующее: Избегает блокировки сессий с
read_and_closeпо умолчанию. - Безопасный: Поддерживает шифрование AES-256-CBC для конфиденциальных данных.
- Гибкий: Опции автоматической фиксации, режима тестирования и ручного контроля.
- Flight-Native: Разработан специально для фреймворка Flight.
Технические детали
- Формат хранения: Файлы сессий имеют префикс
sess_и хранятся в настроенномsave_path. Префиксы содержимого файлов:J: Простой JSON (по умолчанию, без шифрования)F: Зашифрованный JSON (по умолчанию с шифрованием)P: Простая сериализация PHP (устаревшая, без шифрования)E: Зашифрованная сериализация PHP (устаревшая с шифрованием)
- Шифрование: Использует AES-256-CBC с случайным IV на каждую запись сессии, когда предоставлен
encryption_key. Шифрование работает как для режимов JSON, так и для PHP. - Сериализация: JSON — это режим по умолчанию и самый безопасный метод. Сериализация PHP доступна для устаревшего/продвинутого использования, но менее безопасна.
- Сбор мусора: Реализует
SessionHandlerInterface::gc()для очистки истекших сессий.
Вклад
Вклад приветствуется! Создайте форк репозитория, внесите изменения и отправьте пул-реквест. Сообщайте об ошибках или предлагайте функции через трекер задач на 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.
- Если вы используете проект скелета, вы можете запустить
php runway [команда]из корня вашего проекта. - Если вы используете Runway как пакет, установленный через composer, вы можете запустить
vendor/bin/runway [команда]из корня вашего проекта.
Список команд
Вы можете просмотреть список всех доступных команд, выполнив команду php runway.
php runway
Полагайтесь только на команды, которые действительно появляются в этом списке для вашей установки (основные команды Runway против специфичных для проекта, таких как migrate скелета).
Справка по команде
Для любой команды вы можете передать флаг --help, чтобы получить больше информации о том, как использовать команду.
php runway routes --help
php runway make:controller --help
Вот несколько примеров:
Генерация контроллера
make:controller создаёт каркас контроллера, который соответствует макету официального скелета:
| Путь | app/Controller/{Name}.php |
| Пространство имён | App\Controller |
| Стиль | Конструкторская инъекция flight\Engine (без Flight:: в теле класса) |
php runway make:controller MyController
# → app/Controller/MyController.php
# namespace App\Controller;
Пример ожидаемой структуры (упрощённый):
<?php
declare(strict_types=1);
namespace App\Controller;
use flight\Engine;
class MyController
{
protected Engine $app;
public function __construct(Engine $app)
{
$this->app = $app;
}
public function index(): void
{
// например $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.php—config:setпереписывает этот файл как статические данные и может встроить секреты в файл. См. Конфигурацию.
Миграция старой конфигурации
Если у вас есть старый файл .runway-config.json, вы можете легко перенести его в app/config/config.php с помощью следующей команды:
php runway config:migrate
Установка значения конфигурации
Вы можете установить значение конфигурации с помощью команды config:set. Это полезно, если вы хотите обновить значение конфигурации без открытия файла.
php runway config:set app_root "app/"
Получение значения конфигурации
Вы можете получить значение конфигурации с помощью команды config:get.
php runway config:get app_root
Все конфигурации Runway
Если вам нужно настроить конфигурацию для Runway, вы можете установить эти значения в app/config/config.php. Ниже приведены некоторые дополнительные конфигурации, которые вы можете установить:
<?php
// app/config/config.php
return [
// ... другие значения конфигурации ...
'runway' => [
// Здесь находится директория вашего приложения
'app_root' => 'app/',
// Это директория, где находится ваш корневой index-файл
'index_root' => 'public/',
// Это пути к корням других проектов
'root_paths' => [
'/home/user/different-project',
'/var/www/another-project'
],
// Базовые пути, скорее всего, не нуждаются в настройке, но они здесь, если вам нужно
'base_paths' => [
'/includes/libs/vendor', // если у вас действительно уникальный путь к директории vendor или что-то подобное
],
// Конечные пути — это места в проекте для поиска файлов команд
'final_paths' => [
'src/diff-path/commands',
'app/module/admin/commands',
],
// Если вы хотите просто добавить полный путь, вперёд (абсолютный или относительный к корню проекта)
'paths' => [
'/home/user/different-project/src/diff-path/commands',
'/var/www/another-project/app/module/admin/commands',
'app/my-unique-commands'
]
]
];
Доступ к конфигурации
Если вам нужно эффективно получить доступ к значениям конфигурации, вы можете получить к ним доступ через метод __construct или метод app(). Также важно отметить, что если у вас есть файл app/config/services.php, эти сервисы также будут доступны для вашей команды.
public function execute()
{
$io = $this->app()->io();
// Доступ к конфигурации
$app_root = $this->config['runway']['app_root'];
// Доступ к сервисам, например, к соединению с базой данных
$database = $this->config['database']
// ...
}
Обёртки помощников ИИ
Runway имеет некоторые обёртки помощников, которые облегчают генерацию команд ИИ. Вы можете использовать addOption и addArgument способом, который похож на Symfony Console. Это полезно, если вы используете инструменты ИИ для генерации ваших команд.
public function __construct(array $config)
{
parent::__construct('make:example', 'Создать пример для документации', $config);
// Аргумент mode может быть null и по умолчанию полностью необязателен
$this->addOption('name', 'Название примера', null);
}
См. также
- Установка - Дерево скелета и значения по умолчанию create-project
- Автозагрузка -
App\и регистр папки - Внедрение зависимостей - Dice + инъекция Engine для сгенерированных контроллеров
- ИИ и опыт разработчика -
ai:init,ai:generate-instructions,AGENTS.md - Active Record - Модели, используемые с
make:record/ скелетомApp\Model - SimplePdo - Соединение с БД, используемое миграциями и моделями скелета
Awesome-plugins/tracy_extensions
Расширения панели Tracy для Flight
Это набор расширений, которые делают работу с Flight немного богаче.
- Flight - Анализировать все переменные Flight.
- Database - Анализировать все запросы, которые выполнялись на странице (если правильно инициализировать подключение к базе данных)
- Request - Анализировать все переменные
$_SERVERи проверять все глобальные полезные данные ($_GET,$_POST,$_FILES) - Session - Анализировать все переменные
$_SESSION, если сессии активны. - Twig (опционально) - Анализировать время рендеринга шаблонов Twig, память и какие шаблоны/блоки/макросы выполнялись (требует
twig/twigи конфигурациюtwig_profile)
Это особенно удобно с официальным скелетом, который по умолчанию использует Twig: тот же макет AI tools также четко отображается на панели Tracy.
Это панель

И каждая панель отображает очень полезную информацию о вашем приложении!

Нажмите здесь, чтобы просмотреть код.
Установка
Выполните composer require flightphp/tracy-extensions --dev и вы на пути!
Twig не является жесткой зависимостью пакета. Установите twig/twig только если хотите панель Twig (скелет уже делает это для представлений).
Конфигурация
Для начала требуется очень мало конфигурации. Вам нужно будет инициализировать отладчик Tracy перед использованием этого https://tracy.nette.org/en/guide:
<?php
use Tracy\Debugger;
use flight\debug\tracy\TracyExtensionLoader;
// bootstrap code
require __DIR__ . '/vendor/autoload.php';
Debugger::enable();
// Возможно, вам нужно указать окружение с Debugger::enable(Debugger::DEVELOPMENT)
// если вы используете подключения к базе данных в вашем приложении, есть
// обязательная обертка PDO для использования ТОЛЬКО В РАЗРАБОТКЕ (не в продакшене!)
// Она имеет те же параметры, что и обычное подключение PDO
$pdo = new PdoQueryCapture('sqlite:test.db', 'user', 'pass');
// или если вы прикрепляете это к фреймворку Flight
Flight::register('db', PdoQueryCapture::class, ['sqlite:test.db', 'user', 'pass']);
// теперь при каждом выполнении запроса будет фиксироваться время, запрос и параметры
// Это соединяет точки
if(Debugger::$showBar === true) {
// Это должно быть false, иначе Tracy не сможет рендерить :(
Flight::set('flight.content_length', false);
new TracyExtensionLoader(Flight::app());
}
// еще код
Flight::start();
Дополнительная конфигурация
Данные сессии
Если у вас есть пользовательский обработчик сессий (такой как ghostff/session), вы можете передать любой массив данных сессии в Tracy, и он автоматически выведет их для вас. Вы передаете это с ключом session_data во втором параметре конструктора TracyExtensionLoader.
use Ghostff\Session\Session;
// или используйте flight\Session;
require 'vendor/autoload.php';
$app = Flight::app();
$app->register('session', Session::class);
if(Debugger::$showBar === true) {
// Это должно быть false, иначе Tracy не сможет рендерить :(
Flight::set('flight.content_length', false);
new TracyExtensionLoader(Flight::app(), [ 'session_data' => Flight::session()->getAll() ]);
}
// маршруты и другие вещи...
Flight::start();
Панель Twig (опционально)
Если ваше приложение использует Twig (включая официальный скелет), вы можете показывать метрики шаблонов на панели Tracy. Создайте 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 скрыта, когда для запроса не рендерились шаблоны, или когда вы опускаете 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);
});
См. также
- Tracy - Базовая настройка Tracy для Flight
- Twig - Шаблонизация, используемая скелетом и панелью Twig
- Templates - Как Flight связывает
renderс Twig/Latte - Installation - Скелет включает tracy-extensions в dev
Awesome-plugins/apm
Документация FlightPHP APM
Добро пожаловать в FlightPHP APM — ваш личный тренер по производительности приложений! Это руководство — ваша дорожная карта по настройке, использованию и освоению мониторинга производительности приложений (APM) с помощью FlightPHP. Независимо от того, ищете ли вы медленные запросы или просто хотите разобраться в графиках задержек, мы вас поддержим. Давайте сделаем ваше приложение быстрее, пользователей счастливее, а отладку — проще!
Посмотрите демо дашборда для сайта документации Flight.

Почему APM важен
Представьте: ваше приложение — это оживлённый ресторан. Без возможности отслеживать, сколько времени занимают заказы или где кухня тормозит, вы только гадаете, почему клиенты уходят недовольными. APM — ваш су-шеф: он следит за каждым этапом, от входящих запросов до SQL-запросов, и отмечает всё, что замедляет работу. Медленные страницы теряют пользователей (исследования показывают, что 53% пользователей уходят, если сайт загружается дольше 3 секунд!), а APM помогает выявлять проблемы до того, как они навредят. Это проактивное спокойствие — меньше моментов «почему это не работает?», больше побед «посмотрите, как плавно всё работает!».
Установка
Начните с Composer:
composer require flightphp/apm
Вам потребуется:
- PHP 7.4+: Обеспечивает совместимость с LTS-дистрибутивами Linux и поддерживает современный PHP.
- FlightPHP Core v3.15+: Лёгкий фреймворк, который мы улучшаем.
Поддерживаемые базы данных
FlightPHP APM в настоящее время поддерживает следующие базы данных для хранения метрик:
- SQLite3: Простая, файловая, отлично подходит для локальной разработки или небольших приложений. Вариант по умолчанию в большинстве конфигураций.
- MySQL/MariaDB: Идеально для крупных проектов или продакшен-сред, где требуется надёжное, масштабируемое хранилище.
Вы можете выбрать тип базы данных на этапе настройки (см. ниже). Убедитесь, что в вашем окружении PHP установлены необходимые расширения (например, pdo_sqlite или pdo_mysql).
Начало работы
Вот пошаговая инструкция для достижения крутых результатов с APM:
1. Регистрация APM
Добавьте следующий код в index.php или файл services.php, чтобы начать отслеживание:
use flight\apm\logger\LoggerFactory;
use flight\database\SimplePdo;
use flight\Apm;
$ApmLogger = LoggerFactory::create(__DIR__ . '/../../.runway-config.json');
$Apm = new Apm($ApmLogger);
$Apm->bindEventsToFlightInstance($app);
// Если вы добавляете подключение к базе данных
// Рекомендуется использовать SimplePdo (или PdoQueryCapture из Tracy Extensions в режиме разработки).
// Включите отслеживание запросов APM через массив опций (5-й аргумент).
$pdo = new SimplePdo('mysql:host=localhost;dbname=example', 'user', 'pass', null, [
'trackApmQueries' => true, // обязательно для захвата запросов в APM
]);
$Apm->addPdoConnection($pdo);
Что здесь происходит?
LoggerFactory::create()получает вашу конфигурацию (подробнее ниже) и настраивает логгер — по умолчанию SQLite.Apm— главный компонент: он слушает события Flight (запросы, маршруты, ошибки и т.д.) и собирает метрики.bindEventsToFlightInstance($app)связывает всё с вашим приложением Flight.
Совет: Сэмплирование Если ваше приложение нагружено, логирование каждого запроса может перегрузить систему. Используйте долю сэмплирования (от 0.0 до 1.0):
$Apm = new Apm($ApmLogger, 0.1); // Логирует 10% запросов
Это сохраняет производительность на высоком уровне и при этом даёт достаточно данных.
2. Настройка
Выполните следующую команду для создания .runway-config.json:
php vendor/bin/runway apm:init
Что делает эта команда?
- Запускает мастер, который спрашивает, откуда брать сырые метрики (источник) и куда сохранять обработанные данные (назначение).
- По умолчанию используется SQLite — например,
sqlite:/tmp/apm_metrics.sqliteдля источника и ещё один файл для назначения. - В итоге вы получите конфигурацию вида:
{ "apm": { "source_type": "sqlite", "source_db_dsn": "sqlite:/tmp/apm_metrics.sqlite", "storage_type": "sqlite", "dest_db_dsn": "sqlite:/tmp/apm_metrics_processed.sqlite" } }
В процессе также будет предложено запустить миграции для данной настройки. Если вы настраиваете APM впервые, ответьте «да».
Зачем два хранилища? Сырые метрики быстро накапливаются (как нефильтрованные логи). Воркер обрабатывает их и сохраняет в структурированное хранилище для дашборда. Так всё остаётся аккуратным!
3. Обработка метрик с помощью воркера
Воркер преобразует сырые метрики в данные, готовые для дашборда. Запустите его один раз:
php vendor/bin/runway apm:worker
Что он делает?
- Читает данные из источника (например,
apm_metrics.sqlite). - Обрабатывает до 100 метрик (размер пакета по умолчанию) и сохраняет в назначение.
- Завершает работу, когда обработаны все метрики или их больше нет.
Постоянная работа Для работающих приложений вам потребуется непрерывная обработка. Вот ваши варианты:
-
Режим демона:
php vendor/bin/runway apm:worker --daemonРаботает постоянно, обрабатывая метрики по мере их поступления. Отлично подходит для разработки или небольших проектов.
-
Crontab: Добавьте это в crontab (
crontab -e):* * * * * php /path/to/project/vendor/bin/runway apm:workerЗапускается каждую минуту — идеально для продакшена.
-
Tmux/Screen: Запустите detachable-сессию:
tmux new -s apm-worker php vendor/bin/runway apm:worker --daemon # Ctrl+B, затем D для отсоединения; `tmux attach -t apm-worker` для повторного подключенияПозволяет продолжать работу даже после выхода из системы.
-
Пользовательские параметры:
php vendor/bin/runway apm:worker --batch_size 50 --max_messages 1000 --timeout 300--batch_size 50: Обрабатывать 50 метрик за раз.--max_messages 1000: Остановить после 1000 метрик.--timeout 300: Завершить работу через 5 минут.
Зачем это нужно? Без воркера ваш дашборд будет пустым. Он служит мостом между сырыми логами и полезными insights.
4. Запуск дашборда
Посмотрите показатели вашего приложения:
php vendor/bin/runway apm:dashboard
Что делает эта команда?
- Запускает PHP-сервер по адресу
http://localhost:8001/apm/dashboard. - Показывает логи запросов, медленные маршруты, частоту ошибок и многое другое.
Настройка:
php vendor/bin/runway apm:dashboard --host 0.0.0.0 --port 8080 --php-path=/usr/local/bin/php
--host 0.0.0.0: Доступно с любого IP (удобно для удалённого просмотра).--port 8080: Используйте другой порт, если 8001 занят.--php-path: Укажите путь к PHP, если он не находится в PATH.
Откройте URL в браузере и исследуйте!
Продакшен-режим
В продакшене может потребоваться несколько техник для запуска дашборда, так как обычно присутствуют файрволы и другие меры безопасности. Вот несколько вариантов:
- Использование обратного прокси: Настройте Nginx или Apache для перенаправления запросов к дашборду.
- SSH-туннель: Если у вас есть SSH-доступ к серверу, используйте
ssh -L 8080:localhost:8001 youruser@yourserver, чтобы туннелировать дашборд на вашу локальную машину. - VPN: Если сервер находится за VPN, подключитесь к нему и получите прямой доступ к дашборду.
- Настройка файрвола: Откройте порт 8001 для вашего IP или сети сервера (или любой другой порт, который вы указали).
- Настройка Apache/Nginx: Если перед приложением стоит веб-сервер, вы можете настроить его на домен или поддомен. В этом случае укажите document root как
/path/to/your/project/vendor/flightphp/apm/dashboard.
Хотите другой дашборд?
Вы можете создать собственный дашборд! Посмотрите директорию vendor/flightphp/apm/src/apm/presenter для идей по отображению данных.
Возможности дашборда
Дашборд — это ваш APM-центр. Вот что вы увидите:
- Лог запросов: Каждый запрос с временем, URL, кодом ответа и общим временем. Нажмите «Подробнее» для просмотра middleware, запросов и ошибок.
- Самые медленные запросы: Топ-5 запросов по времени (например, «/api/heavy» за 2.5с).
- Самые медленные маршруты: Топ-5 маршрутов по среднему времени — удобно для выявления паттернов.
- Частота ошибок: Процент неудачных запросов (например, 2.3% кодов 500).
- Перцентили задержек: Время ответа на 95-м (p95) и 99-м (p99) перцентилях — знайте свои худшие сценарии.
- График кодов ответов: Визуализация 200, 404, 500 во времени.
- Долгие запросы/Middleware: Топ-5 медленных SQL-запросов и middleware.
- Cache Hit/Miss: Как часто кэш спасает ситуацию.
Дополнительно:
- Фильтр по «Последний час», «Последний день» или «Последняя неделя».
- Тёмный режим для ночных сессий.
Пример:
Запрос к /users может показать:
- Общее время: 150мс
- Middleware:
AuthMiddleware->handle(50мс) - Запрос:
SELECT * FROM users(80мс) - Кэш: Попадание по
user_list(5мс)
Добавление пользовательских событий
Отслеживайте что угодно — например, вызов 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);
Что вы получите:
- Текст запроса (например,
SELECT * FROM users WHERE id = ?) - Время выполнения (например, 0.015с)
- Количество строк (например, 42)
Внимание:
- Опционально: Пропустите этот шаг, если не требуется отслеживание БД.
- SimplePdo (рекомендуется): Используйте
SimplePdoсtrackApmQueries => true. УстаревшийPdoWrapperвсё ещё работает (5-й аргумент конструктораtrue). Обычный PDO из ядра пока не подключён — следите за обновлениями! - Предупреждение о производительности: Логирование каждого запроса на сайте с тяжёлой БД может замедлить работу. Используйте сэмплирование (
$Apm = new Apm($ApmLogger, 0.1)) для снижения нагрузки.
Пример вывода:
- Запрос:
SELECT name FROM products WHERE price > 100 - Время: 0.023с
- Строк: 15
Опции воркера
Настройте воркер по своему вкусу:
--timeout 300: Останавливается через 5 минут — удобно для тестирования.--max_messages 500: Ограничение в 500 метрик — делает работу конечной.--batch_size 200: Обрабатывает 200 метрик за раз — баланс между скоростью и памятью.--daemon: Работает непрерывно — идеально для мониторинга в реальном времени.
Пример:
php vendor/bin/runway apm:worker --daemon --batch_size 100 --timeout 3600
Работает в течение часа, обрабатывая по 100 метрик за раз.
Request ID в приложении
Каждый запрос имеет уникальный идентификатор (request ID) для отслеживания. Вы можете использовать этот ID в приложении для сопоставления логов и метрик. Например, можно добавить request ID на страницу ошибки:
Flight::map('error', function($message) {
// Получить 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 дней.
Устранение неполадок
Возникли проблемы? Попробуйте следующее:
-
Нет данных на дашборде?
- Запущен ли воркер? Проверьте
ps aux | grep apm:worker. - Совпадают ли пути в конфиге? Убедитесь, что DSN в
.runway-config.jsonуказывают на реальные файлы. - Запустите
php vendor/bin/runway apm:workerвручную, чтобы обработать накопившиеся метрики.
- Запущен ли воркер? Проверьте
-
Ошибки воркера?
- Посмотрите ваши SQLite-файлы (например,
sqlite3 /tmp/apm_metrics.sqlite "SELECT * FROM apm_metrics_log LIMIT 5"). - Проверьте PHP-логи на наличие stack trace.
- Посмотрите ваши SQLite-файлы (например,
-
Дашборд не запускается?
- Порт 8001 занят? Используйте
--port 8080. - PHP не найден? Используйте
--php-path /usr/bin/php. - Файрвол блокирует? Откройте порт или используйте
--host localhost.
- Порт 8001 занят? Используйте
-
Слишком медленно?
- Уменьшите долю сэмплирования:
$Apm = new Apm($ApmLogger, 0.05)(5%). - Уменьшите размер пакета:
--batch_size 20.
- Уменьшите долю сэмплирования:
-
Не отслеживаются исключения/ошибки?
- Если у вас включён Tracy для проекта, он переопределяет обработку ошибок Flight. Вам нужно отключить Tracy и убедиться, что установлено
Flight::set('flight.handle_errors', true);.
- Если у вас включён Tracy для проекта, он переопределяет обработку ошибок Flight. Вам нужно отключить Tracy и убедиться, что установлено
-
Не отслеживаются SQL-запросы?
- Предпочтительно использовать
SimplePdoс['trackApmQueries' => true]как 5-й аргумент конструктора (массив опций). - Если вы всё ещё используете устаревший
PdoWrapper, передайтеtrueкак 5-й аргумент. - Вызовите
$Apm->addPdoConnection($pdo)после создания соединения.
- Предпочтительно использовать
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);
}
Полезные советы
При отладке вашего кода есть несколько очень полезных функций для вывода данных.
bdump($var)- Это выведет переменную в панель Tracy Bar в отдельной панели.dumpe($var)- Это выведет переменную, а затем немедленно завершит выполнение.
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%!
Важные примечания
- Жадная загрузка полностью опциональна — ленивая загрузка работает как раньше
- Уже загруженные отношения автоматически пропускаются
- Обратные ссылки работают с жадной загрузкой
- Колбэки отношений уважаются во время жадной загрузки
Ограничения
- Вложенная жадная загрузка (например, with(['contacts.addresses']) ) в настоящее время не поддерживается
- Ограничения жадной загрузки через замыкания не поддерживаются в этой версии
Установка пользовательских данных
Иногда вам может понадобиться прикрепить что-то уникальное к вашему 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">
© 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 можно сделать еще круче с помощью плагинов на базе ИИ.
- Flight MCP - Плагин для интеграции MCP (Model Control Protocol) с Flight, обеспечивающий бесшовную функциональность на базе ИИ. В основном ориентирован на страницы документации, помогает снизить затраты на токены, предоставляя самую актуальную информацию о ваших проектах Flight.
Документация API
Документация API необходима для любого API. Она помогает разработчикам понять, как взаимодействовать с вашим API и чего ожидать в ответ. Существует несколько инструментов, которые помогут вам генерировать документацию API для ваших проектов Flight.
- FlightPHP OpenAPI Generator - Статья в блоге, написанная Даниэлем Шрайбером о том, как использовать спецификацию OpenAPI с FlightPHP для создания вашего API с использованием подхода API-first.
- SwaggerUI - Swagger UI — отличный инструмент, который поможет вам генерировать документацию API для ваших проектов Flight. Его очень легко использовать и можно настроить под ваши нужды. Это PHP-библиотека для генерации документации Swagger.
Мониторинг Производительности Приложений (APM)
Мониторинг производительности приложений (APM) необходим для любого приложения. Он помогает понять, как работает ваше приложение и где находятся узкие места. Существует ряд инструментов APM, которые можно использовать с Flight.
- официальный flightphp/apm - Flight APM — простая библиотека APM, которую можно использовать для мониторинга ваших приложений Flight. Ее можно использовать для мониторинга производительности вашего приложения и помощи в выявлении узких мест.
Асинхронность
Flight и так быстрый фреймворк, но установка турбины делает все еще веселее (и сложнее)!
- flightphp/async - Официальная асинхронная библиотека Flight. Эта библиотека — простой способ добавить асинхронную обработку в ваше приложение. Она использует Swoole/Openswoole под капотом для предоставления простого и эффективного способа асинхронного выполнения задач.
Авторизация/Разрешения
Авторизация и разрешения необходимы для любого приложения, которое требует контроля доступа к различным ресурсам.
- официальный flightphp/permissions - Официальная библиотека разрешений Flight. Эта библиотека — простой способ добавления пользовательских и прикладных разрешений в ваше приложение.
Аутентификация
Аутентификация необходима для приложений, которым нужно проверять личность пользователей и защищать конечные точки API.
- firebase/php-jwt - Библиотека JSON Web Token (JWT) для PHP. Простой и безопасный способ реализации аутентификации на основе токенов в ваших приложениях Flight. Идеально подходит для stateless аутентификации API, защиты маршрутов с помощью middleware и реализации OAuth-подобных потоков авторизации.
Кэширование
Кэширование — отличный способ ускорить ваше приложение. Существует ряд библиотек кэширования, которые можно использовать с Flight.
- официальный flightphp/cache - Легкий, простой и автономный PHP-класс для файлового кэширования
CLI
CLI-приложения — отличный способ взаимодействия с вашим приложением. Вы можете использовать их для генерации контроллеров, отображения всех маршрутов и многого другого.
- официальный flightphp/runway - Runway — CLI-приложение, которое помогает управлять вашими приложениями Flight.
Cookies
Cookies — отличный способ хранения небольших объемов данных на стороне клиента. Их можно использовать для хранения пользовательских предпочтений, настроек приложения и многого другого.
- overclokk/cookie - PHP Cookie — PHP-библиотека, которая предоставляет простой и эффективный способ управления cookies.
Отладка
Отладка необходима при разработке в локальной среде. Существует несколько плагинов, которые могут улучшить ваш опыт отладки.
- tracy/tracy - Это полнофункциональный обработчик ошибок, который можно использовать с Flight. У него есть ряд панелей, которые помогут вам отладить ваше приложение. Его также очень легко расширить и добавить собственные панели.
- официальный flightphp/tracy-extensions - Используется с обработчиком ошибок Tracy, этот плагин добавляет несколько дополнительных панелей для помощи в отладке конкретно для проектов Flight.
Базы Данных
Базы данных — основа большинства приложений. Это то, как вы храните и извлекаете данные. Некоторые библиотеки баз данных — это просто обертки для написания запросов, а некоторые — полноценные ORM.
- официальный flightphp/core SimplePdo - Официальный PDO-помощник Flight, который входит в ядро. Это современная обертка с удобными вспомогательными методами, такими как
insert(),update(),delete()иtransaction()для упрощения операций с базой данных. Все результаты возвращаются как Collections для гибкого доступа к массивам/объектам. Не ORM, просто лучший способ работы с PDO. - устаревший flightphp/core PdoWrapper - Официальная PDO-обертка Flight, которая входит в ядро (устарела начиная с v3.18.0). Используйте SimplePdo вместо нее.
- официальный flightphp/active-record - Официальный ORM/Mapper Flight ActiveRecord. Отличная небольшая библиотека для легкого получения и сохранения данных в вашей базе данных.
- byjg/php-migration - Плагин для отслеживания всех изменений базы данных в вашем проекте.
- knifelemon/easy-query - Легковесный, fluent SQL-конструктор запросов, который генерирует SQL и параметры для подготовленных выражений. Отлично работает с SimplePdo.
Шифрование
Шифрование необходимо для любого приложения, которое хранит конфиденциальные данные. Шифрование и расшифровка данных не так уж сложны, но правильное хранение ключа шифрования может быть сложным. Самое важное — никогда не храните ключ шифрования в публичной директории или не коммитьте его в репозиторий кода.
- defuse/php-encryption - Это библиотека, которую можно использовать для шифрования и расшифровки данных. Начать работу довольно просто, чтобы начать шифровать и расшифровывать данные.
Очередь Задач
Очереди задач очень полезны для асинхронной обработки задач. Это может быть отправка электронных писем, обработка изображений или что угодно, что не нужно делать в реальном времени.
- n0nag0n/simple-job-queue - Simple Job Queue — библиотека, которую можно использовать для асинхронной обработки задач. Ее можно использовать с beanstalkd, MySQL/MariaDB, SQLite и PostgreSQL.
Сессии
Сессии не очень полезны для API, но для создания веб-приложения сессии могут быть необходимы для поддержания состояния и информации о входе в систему.
- официальный flightphp/session - Официальная библиотека сессий Flight. Это простая библиотека сессий, которую можно использовать для хранения и извлечения данных сессии. Она использует встроенную обработку сессий PHP.
- Ghostff/Session - Менеджер сессий PHP (неблокирующий, flash, сегмент, шифрование сессий). Использует PHP open_ssl для опционального шифрования/расшифровки данных сессии.
Шаблонизация
Шаблонизация — основа любого веб-приложения с пользовательским интерфейсом. Существует ряд движков шаблонизации, которые можно использовать с Flight.
- устаревший flightphp/core View - Это очень базовый движок шаблонизации, который входит в ядро. Не рекомендуется использовать, если у вас больше пары страниц в проекте.
- latte/latte - Latte — полнофункциональный движок шаблонизации, который очень прост в использовании и больше похож на PHP-синтаксис, чем Twig или Smarty. Его также очень легко расширить и добавить собственные фильтры и функции.
- twig/twig - Twig — гибкий, быстрый и безопасный движок шаблонов (тот же, который используется Symfony). Инструменты ИИ и многие PHP-разработчики хорошо его знают, он автоматически экранирует вывод по умолчанию, и у него огромная экосистема расширений.
- knifelemon/comment-template - CommentTemplate — мощный PHP-движок шаблонов с компиляцией ассетов, наследованием шаблонов и обработкой переменных. Функции включают автоматическую минимизацию CSS/JS, кэширование, кодирование Base64 и опциональную интеграцию с фреймворком Flight PHP.
Интеграция с WordPress
Хотите использовать Flight в вашем проекте WordPress? Для этого есть удобный плагин!
- n0nag0n/wordpress-integration-for-flight-framework - Этот WordPress-плагин позволяет запускать Flight прямо рядом с WordPress. Он идеально подходит для добавления пользовательских API, микросервисов или даже полноценных приложений на ваш сайт WordPress с использованием фреймворка Flight. Супер полезен, если вы хотите получить лучшее из обоих миров!
Участие
Есть плагин, которым вы хотели бы поделиться? Отправьте pull request, чтобы добавить его в список!
Media
Медиа
Мы постарались отследить то, что смогли, различных типов медиа в интернете, связанных с Flight. См. ниже различные ресурсы, которые вы можете использовать, чтобы узнать больше о Flight.
Статьи и обзоры
- Unit Testing and SOLID Principles by Brian Fenton (2015?)
- PHP Web Framework Flight by ojambo (2025)
- Define, Generate, and Implement: An API-First Approach with OpenAPI Generator and FlightPHP by Daniel Schreiber (2025)
- Best PHP Micro Frameworks for 2024 by n0nag0n (2024)
- Creating a RESTful API with Flight Framework by n0nag0n (2024)
- Building a Simple Blog with Flight Part 2 by n0nag0n (2024)
- Building a Simple Blog with Flight Part 1 by n0nag0n (2024)
- 🚀 Build a Simple CRUD API in PHP with the Flight Framework by soheil-khaledabadi (2024)
- Building a PHP Web Application with the Flight Micro-framework by Arthur C. Codex (2023)
- Best PHP Frameworks for Web Development in 2024 by Ravikiran A S (2023)
- Top 12 PHP Frameworks: A Comprehensive Guide for 2023 by marketing kbk (2023)
- 5 PHP Frameworks You've (Probably) Never Heard of by n0nag0n (2022)
- 12 top PHP frameworks for web developers to consider in 2023 by Anna Monus (2022)
- The Best PHP Microframeworks on a Cloud Server by Shahzeb Ahmed (2021)
- PHP framework: Top 15 powerful ones for your web development by AHT Tech (2020)
- Easy PHP Routing with FlightPHP by Lucas Conceição (2019)
- Trying Out New PHP Framework (Flight) by Leon (2017)
- Setting up FlightPHP to work with Backbonejs by Timothy Tocci (2015)
Видео и уроки
- Build a Flight PHP App with MVC & MariaDB in 10 Minutes! (Beginner Friendly) by ojamboshop (2025)
- Create a REST API for IoT Devices Using PHP & FlightPHP - ESP32 API by IoT Craft Hub (2024)
- PHP Flight Framework Simple Introductory Video by n0nag0n (2024)
- Set header HTTP code in Flightphp (3 Solutions!!) by Roel Van de Paar (2024)
- PHP Flight Framework Tutorial. Super easy API Project! by n0nag0n (2022)
- Aplicación web CRUD con php y mysql y bootstrap usando flight by Devlopteca - Oscar Uh (2021)
- DevOps & SysAdmins: Lighttpd rewrite rule for Flight PHP microframework by Roel Van de Paar (2021)
- Tutorial REST API Flight PHP #PART2 INSERT TABLE Info #Code (Tagalog) by Info Singkat Official (2020)
- Tutorial REST API Flight PHP #PART1 Info #Code (Tagalog) by Info Singkat Official (2020)
- How To Create JSON REST API IN PHP - Part 2 by Codewife (2018)
- How To Create JSON REST API IN PHP - Part 1 by Codewife (2018)
- Teste Micro Frameworks PHP - Flight PHP, Lumen, Slim 3 e Laravel by Codemarket (2016)
- Tutorial 1 Flight PHP - Instalación by absagg (2014)
- Tutorial 2 Flight PHP - Route parte 1 by absagg (2014)
Что-то упущено?
Мы пропустили что-то, что вы написали или записали? Дайте нам знать с помощью issue или pull request!
Examples
Нужен быстрый старт?
У вас есть два варианта для начала работы с новым проектом Flight:
- Full Skeleton Boilerplate: Более полный пример с контроллерами и представлениями.
- Single File Skeleton Boilerplate: Один файл, содержащий всё необходимое для запуска вашего приложения в простом единственном файле.
Примеры, внесённые сообществом:
- flightravel: FlightPHP с директориями Laravel, с инструментами PHP + GH Actions
- fleact - Стартовый комплект FlightPHP с интеграцией ReactJS.
- flastro - Стартовый комплект FlightPHP с интеграцией Astro.
- velt - Velt — это быстрый и простой шаблон Svelte для старта с бэкендом на FlightPHP.
- vite-flightphp - FlightPHP и современный фронтенд (Vite + Tailwind CSS) с поддержкой hot reload.
Нужен ли вам некоторый вдохновение?
Хотя эти примеры не спонсируются официально командой Flight, они могут дать вам идеи о том, как структурировать свои собственные проекты, построенные на Flight!
- ASC REST API Spell Checker - Лёгкий REST API для проверки орфографии арабского языка, построенный на FlightPHP и библиотеке ArPHP. Этот API предоставляет возможности проверки орфографии арабского текста, включая обнаружение ошибочно написанных слов и предложения по исправлению.
- Eventify - Eventify — это одностраничное приложение, соединяющее организаторов событий с участниками. Построено на PHP (FlightPHP), JavaScript и MySQL, с функциями JWT-аутентификации, управления событиями и документацией RESTful API с использованием OpenAPI.
- Ivox Car Rental - Ivox Car Rental — это одностраничное, мобильно-дружественное веб-приложение для аренды автомобилей, построенное на PHP (FlightPHP), JavaScript и MySQL. Оно поддерживает регистрацию пользователей, просмотр и бронирование автомобилей, в то время как администраторы могут управлять автомобилями, пользователями и бронированиями. Приложение включает REST API, JWT-аутентификацию и адаптивный дизайн для современного опыта аренды.
- Decay - Flight v3 с HTMX и SleekDB, всё о зомби! (Demo)
- Flight Example Blog - Flight v3 с Middleware, Controllers, Active Record и Latte.
- Flight CRUD RESTful API - Простой проект CRUD API с использованием фреймворка Flight, который предоставляет базовую структуру для новых пользователей, чтобы быстро настроить PHP-приложение с операциями CRUD и подключением к базе данных. Проект демонстрирует, как использовать Flight для разработки RESTful API, делая его идеальным инструментом для обучения для начинающих и полезным стартовым набором для более опытных разработчиков.
- Flight School Management System - Flight v3
- Paste Bin with Comments - Flight v3
- Basic Skeleton App
- Example Wiki
- The IT-Innovator PHP Framework Application
- LittleEducationalCMS (Spanish)
- Italian Yellow Pages API
- Generic Content Management System (with....very little documentation)
- A tiny php framework based on Flight and medoo.
- Example MVC Application
- Production ready Flight Boilerplate - Готовый к производству фреймворк аутентификации, который сэкономит вам недели разработки. Функции корпоративного уровня безопасности: 2FA/TOTP, интеграция LDAP, Azure SSO, интеллектуальное ограничение скорости, отпечатки сессий, защита от brute-force, панель аналитики безопасности, всестороннее логирование аудита и гранулярный контроль доступа на основе ролей.
Хотите поделиться своим собственным примером?
Если у вас есть проект, которым вы хотите поделиться, пожалуйста, отправьте pull request, чтобы добавить его в этот список!
Install/install
Инструкции по установке
Перед установкой Flight необходимо выполнить несколько базовых предварительных требований. А именно, вам понадобится:
- Установите PHP на вашей системе
- Установите Composer для лучшего опыта разработчика.
Базовая установка
Если вы используете Composer, вы можете выполнить следующую команду:
composer require flightphp/core
Это добавит на вашу систему только основные файлы Flight. Вам нужно будет самостоятельно определить структуру проекта, макет, зависимости, конфигурации, автозагрузку и т.д. Этот метод гарантирует, что никакие другие зависимости, кроме Flight, установлены не будут.
Вы также можете скачать файлы напрямую и распаковать их в вашу веб-директорию.
Базовая установка отлично подходит для обучения, микросервисных API и экспериментов с копированием и вставкой. Для полной структуры приложения, которой одинаково легко будут следовать люди и AI-инструменты для кодирования, используйте рекомендуемый скелет ниже.
Рекомендуемая установка
Настоятельно рекомендуется начинать любой новый проект с приложения flightphp/skeleton. Установка проста как ветерок.
composer create-project flightphp/skeleton my-project/
cd my-project/
composer start
# необязательная демо БД + посты
php runway migrate
Этот шаг настраивает структуру проекта, автозагрузку Composer PSR-4, конфигурацию и такие инструменты, как Tracy, Tracy Extensions и Runway. Он также включает корневой файл AGENTS.md (и локальные копии в app/), чтобы AI-ассистенты работали с той же структурой, что и вы — см. AI и опыт разработчика.
Что даёт скелет
project-root/
├── AGENTS.md # источник истины для AI / агентов
├── SECURITY.md # ожидания по безопасности
├── .env.example # секреты / оверлеи деплоя (копируется в .env)
├── public/index.php # только веб-вход
├── app/
│ ├── config/ # загрузка, маршруты, сервисы, config_sample.php
│ ├── Controller/ # App\Controller\* (папка в PascalCase!)
│ ├── Middleware/ # App\Middleware\*
│ ├── Model/ # App\Model\* (ActiveRecord)
│ ├── Utils/ # Config, Env, DatabaseFactory
│ ├── commands/ # команды CLI Runway
│ ├── views/ # шаблоны Twig (*.twig)
│ ├── cache/
│ └── log/
├── migrations/ # SQL-миграции (.sql / .mysql.sql)
└── tests/ # PHPUnit
Пространства имён соответствуют регистру папок. Composer сопоставляет "App\\": "app/", поэтому:
| Путь на диске | Пространство имён |
|---|---|
app/Controller/HomeController.php |
App\Controller\HomeController |
app/Middleware/… |
App\Middleware\… |
app/Model/… |
App\Model\… |
app/Utils/… |
App\Utils\… |
В Linux app/controller/ не то же самое, что app/Controller/. Автозагрузка чувствительна к регистру — используйте папки в PascalCase, как в скелете. Подробнее: Автозагрузка.
Стандартный стек (новые проекты): представления Twig, SimplePdo + ActiveRecord, Dice с внедрением Engine (предпочтительно без Flight:: внутри классов приложения), опционально SQLite после php runway migrate.
create-project обычно копирует app/config/config_sample.php → config.php и .env.example → .env, если они присутствуют. Маршруты находятся в app/config/routes.php; сервисы и DI — в app/config/services.php.
Документация ↔ скелет: Эта документация описывает 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
-
Установите Homebrew (если он ещё не установлен):
- Откройте Терминал и выполните:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
- Откройте Терминал и выполните:
-
Установите PHP:
- Установите последнюю версию:
brew install php - Чтобы установить конкретную версию, например, PHP 8.1:
brew tap shivammathur/php brew install shivammathur/php/php@8.1
- Установите последнюю версию:
-
Переключение между версиями PHP:
- Отвяжите текущую версию и привяжите нужную:
brew unlink php brew link --overwrite --force php@8.1 - Проверьте установленную версию:
php -v
- Отвяжите текущую версию и привяжите нужную:
Windows 10/11
Установка PHP вручную
-
Загрузите PHP:
- Посетите PHP для Windows и загрузите последнюю или конкретную версию (например, 7.4, 8.0) в виде zip-файла для не многопоточной (Non-Thread-Safe) сборки.
-
Распакуйте PHP:
- Извлеките загруженный zip-файл в
C:\php.
- Извлеките загруженный zip-файл в
-
Добавьте PHP в системный PATH:
- Перейдите в Свойства системы > Переменные среды.
- В разделе Системные переменные найдите Path и нажмите Изменить.
- Добавьте путь
C:\php(или туда, куда вы извлекли PHP). - Нажмите ОК, чтобы закрыть все окна.
-
Настройте PHP:
- Скопируйте
php.ini-developmentвphp.ini. - Отредактируйте
php.iniдля настройки PHP по необходимости (например, укажитеextension_dir, включите расширения).
- Скопируйте
-
Проверьте установку PHP:
- Откройте командную строку и выполните:
php -v
- Откройте командную строку и выполните:
Установка нескольких версий PHP
-
Повторите описанные выше шаги для каждой версии, помещая каждую в отдельную директорию (например,
C:\php7,C:\php8). -
Переключайтесь между версиями, изменяя системную переменную PATH, чтобы она указывала на директорию нужной версии.
Ubuntu (20.04, 22.04, etc.)
Установка PHP с помощью apt
-
Обновите списки пакетов:
- Откройте Терминал и выполните:
sudo apt update
- Откройте Терминал и выполните:
-
Установите PHP:
- Установите последнюю версию PHP:
sudo apt install php - Чтобы установить конкретную версию, например, PHP 8.1:
sudo apt install php8.1
- Установите последнюю версию PHP:
-
Установите дополнительные модули (необязательно):
- Например, для поддержки MySQL:
sudo apt install php8.1-mysql
- Например, для поддержки MySQL:
-
Переключение между версиями PHP:
- Используйте
update-alternatives:sudo update-alternatives --set php /usr/bin/php8.1
- Используйте
-
Проверьте установленную версию:
- Выполните:
php -v
- Выполните:
Rocky Linux
Установка PHP с помощью yum/dnf
-
Включите репозиторий EPEL:
- Откройте Терминал и выполните:
sudo dnf install epel-release
- Откройте Терминал и выполните:
-
Установите репозиторий Remi:
- Выполните:
sudo dnf install https://rpms.remirepo.net/enterprise/remi-release-8.rpm sudo dnf module reset php
- Выполните:
-
Установите PHP:
- Чтобы установить версию по умолчанию:
sudo dnf install php - Чтобы установить конкретную версию, например, PHP 7.4:
sudo dnf module install php:remi-7.4
- Чтобы установить версию по умолчанию:
-
Переключение между версиями PHP:
- Используйте команду модуля
dnf:sudo dnf module reset php sudo dnf module enable php:remi-8.0 sudo dnf install php
- Используйте команду модуля
-
Проверьте установленную версию:
- Выполните:
php -v
- Выполните:
Общие примечания
- Для сред разработки важно настраивать параметры PHP в соответствии с требованиями вашего проекта.
- При переключении версий PHP убедитесь, что для конкретной версии, которую вы собираетесь использовать, установлены все соответствующие расширения PHP.
- Перезапустите ваш веб-сервер (Apache, Nginx и т.д.) после переключения версий PHP или обновления конфигураций, чтобы изменения применились.
Guides
Руководства
Flight PHP предназначен для того, чтобы быть простым, но мощным, и наши руководства помогут вам создавать реальные приложения шаг за шагом. Эти практические уроки проведут вас через полные проекты, чтобы продемонстрировать, как Flight можно использовать эффективно.
Официальные руководства
Создание блога
Узнайте, как создать функциональное приложение для блога с помощью Flight PHP. Это руководство проведет вас через:
- Настройку структуры проекта
- Работа с шаблонами с использованием Latte
- Реализация маршрутов для постов
- Хранение и извлечение данных
- Обработка отправки форм
- Основная обработка ошибок
Этот учебник идеален для начинающих, которые хотят увидеть, как все элементы собираются вместе в реальном приложении.
Юнит-тестирование и принципы SOLID
Это руководство охватывает основы юнит-тестирования в приложениях Flight PHP. В него входит:
- Настройка PHPUnit
- Написание тестируемого кода с использованием принципов SOLID
- Мокирование зависимостей
- Распространенные ошибки, которых следует избегать
- Масштабирование тестов по мере роста вашего приложения Этот учебник идеален для разработчиков, желающих улучшить качество кода и его поддерживаемость.
Неофициальные руководства
Хотя эти руководства не поддерживаются официально командой Flight, они являются ценными ресурсами, созданными сообществом. Они охватывают различные темы и сценарии использования, предоставляя дополнительные insights по использованию Flight PHP.
Создание RESTful API с Flight Framework
Это руководство проведет вас через создание RESTful API с использованием Flight PHP. В нем рассматриваются основы настройки API, определения маршрутов и возврата ответов в формате JSON.
Создание простого блога
Это руководство проведет вас через создание базового блога с использованием Flight PHP. Оно состоит из 2 частей: одна охватывает основы, а другая — более продвинутые темы и доработки для блога, готового к производству.
- Создание простого блога с Flight — Часть 1 — Начало работы с простым блогом.
- Создание простого блога с Flight — Часть 2 — Доработка блога для производства.
Создание API для Pokémon в PHP: Руководство для начинающих
Это забавное руководство проведет вас через создание простого API для Pokémon с использованием Flight PHP. В нем рассматриваются основы настройки API, определения маршрутов и возврата ответов в формате JSON.
Вклад в проект
У вас есть идея для руководства? Вы нашли ошибку? Мы приветствуем вклад! Наши руководства поддерживаются в репозитории документации FlightPHP.
Если вы создали что-то интересное с помощью Flight и хотите поделиться этим в виде руководства, пожалуйста, отправьте pull request. Обмен знаниями помогает сообществу Flight расти.
Ищете документацию по API?
Если вы ищете конкретную информацию о основных функциях и методах Flight, загляните в раздел Learn нашей документации.