Learn/flight_vs_laravel
Flight vs Laravel
¿Qué es Laravel?
Laravel es un framework completo con todas las campanas y silbatos y un ecosistema enfocado en el desarrollador impresionante, pero a costa de rendimiento y complejidad. El objetivo de Laravel es que el desarrollador tenga el nivel más alto de productividad y hacer que las tareas comunes sean fáciles. Laravel es una gran elección para desarrolladores que buscan construir una aplicación web empresarial completa. Eso viene con algunos compromisos, específicamente en términos de rendimiento y complejidad. Aprender los inicios de Laravel puede ser fácil, pero ganar proficiency en el framework puede tomar algo de tiempo.
También hay tantos módulos de Laravel que los desarrolladores a menudo sienten que la única manera de resolver problemas es a través de estos módulos, cuando en realidad podrías simplemente usar otra biblioteca o escribir tu propio código.
Pros comparado con Flight
- Laravel tiene un enorme ecosistema de desarrolladores y módulos que se pueden usar para resolver problemas comunes.
- Laravel tiene un ORM completo que se puede usar para interactuar con tu base de datos.
- Laravel tiene una insana cantidad de documentación y tutoriales que se pueden usar para aprender el framework. Eso puede ser bueno para profundizar en los detalles o malo porque hay tanto por revisar.
- Laravel tiene un sistema de autenticación integrado que se puede usar para asegurar tu aplicación.
- Laravel tiene podcasts, conferencias, reuniones, videos y otros recursos que se pueden usar para aprender el framework.
- Laravel está orientado a un desarrollador experimentado que busca construir una aplicación web empresarial completa.
Cons comparado con Flight
- Laravel tiene mucho más ocurriendo bajo el capó que Flight. Esto viene con un costo dramático en términos de rendimiento. Ver los benchmarks de TechEmpower para más información.
- Flight está orientado a un desarrollador que busca construir una aplicación web ligera, rápida y fácil de usar.
- Flight está orientado a la simplicidad y facilidad de uso.
- Una de las características principales de Flight es que hace lo mejor para mantener la compatibilidad hacia atrás. Laravel causa mucha frustración entre versiones mayores.
- Flight está destinado a desarrolladores que se aventuran por primera vez en el mundo de los frameworks.
- Flight no tiene dependencias, mientras que Laravel tiene una cantidad atroz de dependencias
- Flight también puede hacer aplicaciones de nivel empresarial, pero no tiene tanto código boilerplate como Laravel. También requerirá más disciplina por parte del desarrollador para mantener las cosas organizadas y bien estructuradas.
- Flight le da al desarrollador más control sobre la aplicación, mientras que Laravel tiene montones de magia detrás de escena que puede ser frustrante.
Learn/migrating_to_v3
Migrando a v3
La compatibilidad hacia atrás se ha mantenido en su mayor parte, pero hay algunos cambios de los que debes estar al tanto al migrar de v2 a v3. Hay algunos cambios que conflictuaban demasiado con los patrones de diseño, por lo que se tuvieron que hacer algunos ajustes.
Comportamiento de Buffering de Salida
v3.5.0
Output buffering es el proceso en el que la salida generada por un script PHP se almacena en un búfer (interno de PHP) antes de ser enviada al cliente. Esto te permite modificar la salida antes de que se envíe al cliente.
En una aplicación MVC, el Controlador es el "gestor" y gestiona lo que hace la vista. Tener salida generada fuera del controlador (o en el caso de Flight, a veces una función anónima) rompe el patrón MVC. Este cambio se realiza para alinearse más con el patrón MVC y hacer que el framework sea más predecible y fácil de usar.
En v2, el buffering de salida se manejaba de una manera en la que no cerraba consistentemente su propio búfer de salida, lo que hacía más difícil el unit testing y el streaming. Para la mayoría de los usuarios, este cambio puede no afectarte realmente. Sin embargo, si estás haciendo eco de contenido fuera de callables y controladores (por ejemplo, en un hook), es probable que te encuentres con problemas. Hacer eco de contenido en hooks, y antes de que el framework se ejecute realmente, puede haber funcionado en el pasado, pero no funcionará hacia adelante.
Dónde podrías tener problemas
// index.php
require 'vendor/autoload.php';
// solo un ejemplo
define('START_TIME', microtime(true));
function hello() {
echo 'Hello World';
}
Flight::map('hello', 'hello');
Flight::after('hello', function(){
// esto en realidad estará bien
echo '<p>This Hello World phrase was brought to you by the letter "H"</p>';
});
Flight::before('start', function(){
// cosas como esta causarán un error
echo '<html><head><title>My Page</title></head><body>';
});
Flight::route('/', function(){
// esto en realidad está bien
echo 'Hello World';
// Esto también debería estar bien
Flight::hello();
});
Flight::after('start', function(){
// esto causará un error
echo '<div>Your page loaded in '.(microtime(true) - START_TIME).' seconds</div></body></html>';
});
Activando el Comportamiento de Renderizado de v2
¿Puedes mantener tu código antiguo tal como está sin hacer una reescritura para que funcione con v3? ¡Sí, puedes! Puedes activar el comportamiento de renderizado de v2 estableciendo la opción de configuración flight.v2.output_buffering en true. Esto te permitirá continuar usando el comportamiento de renderizado antiguo, pero se recomienda corregirlo hacia adelante. En v4 del framework, esto se eliminará.
// index.php
require 'vendor/autoload.php';
Flight::set('flight.v2.output_buffering', true);
Flight::before('start', function(){
// Ahora esto estará bien
echo '<html><head><title>My Page</title></head><body>';
});
// más código
Cambios en el Dispatcher
v3.7.0
Si has estado llamando directamente métodos estáticos para Dispatcher como Dispatcher::invokeMethod(), Dispatcher::execute(), etc., necesitarás actualizar tu código para no llamar directamente a estos métodos. Dispatcher se ha convertido en más orientado a objetos para que los Contenedores de Inyección de Dependencias puedan usarse de una manera más fácil. Si necesitas invocar un método similar a como lo hacía Dispatcher, puedes usar manualmente algo como $result = $class->$method(...$params); o call_user_func_array() en su lugar.
Cambios en halt() stop() redirect() y error()
v3.10.0
El comportamiento predeterminado antes de 3.10.0 era limpiar tanto los encabezados como el cuerpo de la respuesta. Esto se cambió para limpiar solo el cuerpo de la respuesta. Si necesitas limpiar también los encabezados, puedes usar Flight::response()->clear().
Learn/configuration
Configuración
Resumen
Flight proporciona una forma sencilla de configurar varios aspectos del framework para adaptarse a las necesidades de tu aplicación. Algunos valores están establecidos por defecto, pero puedes sobrescribirlos según sea necesario. También puedes establecer tus propias variables para usarlas en toda tu aplicación.
Una configuración clara y por capas (valores predeterminados en archivos + secretos de entorno) también ayuda a herramientas de codificación de IA: los agentes aprenden un único lugar para los literales y un único lugar para los secretos, en lugar de inventar lecturas de $_ENV dentro de los controladores.
Comprensión
Puedes personalizar ciertos comportamientos de Flight estableciendo valores de configuración a través del método set.
Flight::set('flight.log_errors', true);
En una aplicación estructurada (incluido el skeleton), normalmente cargas la configuración del proyecto desde app/config/config.php y luego aplicas las claves relevantes al Engine (por ejemplo, flight.base_url, flight.views.path). También puedes inyectar un pequeño objeto de configuración en los controladores en lugar de leer variables globales en todas partes, lo que resulta más amigable para las pruebas y para los agentes que siguen AGENTS.md.
Uso Básico
Opciones de Configuración de Flight
La siguiente es una lista de todas las configuraciones disponibles:
- flight.base_url
?string- Sobrescribe la URL base de la solicitud si Flight se ejecuta en un subdirectorio. (predeterminado: null) - flight.case_sensitive
bool- Coincidencia de URLs sensible a mayúsculas. (predeterminado: false) - flight.handle_errors
bool- Permitir que Flight maneje todos los errores internamente. (predeterminado: true)- Si quieres que Flight maneje los errores en lugar del comportamiento predeterminado de PHP, esto debe ser true.
- Si tienes Tracy instalado, querrás establecer esto en false para que Tracy pueda manejar los errores.
- Si tienes el plugin APM instalado, querrás establecer esto en true para que el APM pueda registrar los errores.
- flight.log_errors
bool- Registra los errores en el archivo de registro de errores del servidor web. (predeterminado: false)- Si tienes Tracy instalado, Tracy registrará los errores según su propia configuración, no según esta.
- flight.debug
bool- Muestra información detallada del error (mensaje de excepción, código y traza de pila) en el navegador cuando ocurre un error. (predeterminado: false)- Nunca habilites esto en producción — filtra detalles internos de la aplicación. Úsalo solo para desarrollo local o staging.
- Cuando es
false, se muestra un500 Internal Server Errorgenérico. Combínalo conflight.log_errorspara capturar errores en el servidor.
- flight.allow_method_override
bool- Permite sobrescribir el método HTTP mediante la cabecera de solicitudX-HTTP-Method-Overrideo un campo_methoden el cuerpo POST. (predeterminado: true)- Se recomienda establecer esto en
falsepara aplicaciones que no necesitan suplantación de métodos basada en formularios HTML, ya que evita que los clientes forjen solicitudesDELETEoPUTa través de un formulario POST estándar. - Consulta Seguridad para más detalles.
- Se recomienda establecer esto en
- flight.views.path
string- Directorio que contiene los archivos de plantilla de vistas. (predeterminado: ./views) - flight.views.extension
string- Extensión de archivo de plantilla de vistas. (predeterminado:.php; el skeleton oficial establece esto en.twigcuando se usa Twig) - flight.content_length
bool- Establece la cabeceraContent-Length. (predeterminado: true)- Si estás usando Tracy, esto debe establecerse en false para que Tracy pueda renderizar correctamente.
- flight.v2.output_buffering
bool- Usa el almacenamiento en búfer de salida heredado. Consulta migración a v3. (predeterminado: false)
Configuración del Cargador
Hay además otra configuración para el cargador. Esto te permitirá autocargar clases con _ en el nombre de la clase.
// Habilitar la carga de clases con guiones bajos
// Valor predeterminado: true
Loader::$v2ClassLoading = false;
Recuerda que la autocarga también depende de que el uso de mayúsculas en las carpetas coincida con tus espacios de nombres, especialmente con la disposición App\ + app/Controller/ del skeleton.
Configuración del proyecto y .env (patrón del skeleton)
El núcleo de Flight no requiere archivos .env. Muchas aplicaciones solo usan un array de configuración PHP. El skeleton oficial organiza la configuración en capas para que los secretos no se incluyan en git, mientras que Runway aún puede reescribir de forma segura la configuración literal:
.env/ entorno real — secretos y sobrescrituras de implementación (ignorados por git).app/config/config.php— valores predeterminados literales en un array PHP (copiados deconfig_sample.php). Se recomienda no usar expresiones$_ENV[...]dentro de este archivo: herramientas comorunway config:setpueden reescribirlo con valores estáticos y podrían terminar horneando secretos en el archivo.- Combinar en el arranque — el entorno gana para las claves mapeadas; el código de la aplicación lee un objeto de configuración o
$app->get(), no$_ENVen los controladores.
Ejemplo de la forma de config_sample.php / config.php (simplificado):
<?php
// Solo literales — los secretos pertenecen a .env para el flujo de trabajo del skeleton
return [
'app' => [
'env' => 'development',
'debug' => true,
'base_url' => '/',
'timezone' => 'UTC',
],
'database' => [
'driver' => 'sqlite', // o mysql, o '' para deshabilitar
'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=...
Esta división es deliberada para proyectos amigables con IA: las instrucciones pueden decir "valores predeterminados en config.php, secretos en .env, inyecta Config / Engine — nunca inventes acceso a variables de entorno en un controlador". Las aplicaciones existentes pueden ignorar .env por completo y mantener un único archivo de configuración.
Variables
Flight te permite guardar variables para que puedan usarse en cualquier parte de tu aplicación.
// Guarda tu variable
Flight::set('id', 123);
// En otra parte de tu aplicación
$id = Flight::get('id');
Para ver si una variable ha sido establecida, puedes hacer:
if (Flight::has('id')) {
// Hacer algo
}
Puedes limpiar una variable haciendo:
// Limpia la variable id
Flight::clear('id');
// Limpia todas las variables
Flight::clear();
Nota: El hecho de que puedas establecer una variable no significa que debas hacerlo. Usa esta función con moderación. La razón es que cualquier cosa almacenada aquí se convierte en una variable global. Las variables globales son malas porque pueden cambiarse desde cualquier parte de tu aplicación, lo que dificulta el rastreo de errores. Además, esto puede complicar cosas como pruebas unitarias. Prefiere la inyección por constructor (como en el skeleton con la configuración de Dice) para los servicios y la configuración que los controladores necesitan.
Errores y Excepciones
Todos los errores y excepciones son capturados por Flight y pasados al método error si flight.handle_errors está establecido en true.
El comportamiento predeterminado es enviar una respuesta genérica HTTP 500 Internal Server Error con cierta información del error.
Puedes sobrescribir este comportamiento según tus necesidades:
Flight::map('error', function (Throwable $error) {
// Manejar el error
echo $error->getTraceAsString();
});
Por defecto, los errores no se registran en el servidor web. Puedes habilitar esto cambiando la configuración:
Flight::set('flight.log_errors', true);
404 No Encontrado
Cuando una URL no se puede encontrar, Flight llama al método notFound. El comportamiento predeterminado es enviar una respuesta HTTP 404 Not Found con un mensaje simple.
Puedes sobrescribir este comportamiento según tus necesidades:
Flight::map('notFound', function () {
// Manejar el no encontrado
});
Ver También
- Instalación - Configuración del skeleton,
.envy estructura de arranque. - Autocarga - Espacios de nombres y uso de mayúsculas en carpetas.
- Extender Flight - Cómo extender y personalizar la funcionalidad principal de Flight.
- Pruebas Unitarias - Cómo escribir pruebas unitarias para tu aplicación Flight.
- IA y Experiencia del Desarrollador -
AGENTS.mde instrucciones consistentes del proyecto. - Tracy - Un plugin para el manejo avanzado de errores y depuración.
- Extensiones de Tracy - Extensiones para integrar Tracy con Flight.
- APM - Un plugin para monitoreo de rendimiento de aplicaciones y seguimiento de errores.
- Seguridad - Indicadores de endurecimiento y manejo de secretos.
Solución de Problemas
- Si tienes problemas para descubrir todos los valores de tu configuración, puedes hacer
var_dump(Flight::get()); - Si Runway o las herramientas de implementación reescribieron
config.php, confirma que los secretos no se hayan comprometido; mantenlos en.envo en el entorno real cuando uses el patrón del skeleton.
Historial de Cambios
- Docs – Documentar el estilo de skeleton para la configuración / capas de
.envy el valor predeterminado de extensión de vistas Twig para nuevos proyectos. - v3.18.1 - Se agregaron las opciones de configuración
flight.debugyflight.allow_method_override. - v3.5.0 - Se agregó configuración para
flight.v2.output_bufferingpara admitir el comportamiento de almacenamiento en búfer de salida heredado. - v2.0 - Se agregaron las configuraciones principales.
Learn/ai
IA y Experiencia de Desarrollo con Flight
Resumen
Flight está diseñado para trabajar con las herramientas de codificación de IA, no para luchar contra ellas. Una API pequeña y predecible, una disposición clara de la aplicación en el skeleton oficial y archivos de instrucciones específicos del proyecto significan que asistentes como GitHub Copilot, Cursor, Windsurf, Claude Code y Gemini pueden seguir los mismos patrones que escribirías a mano.
Con los comandos integrados de Runway para conectarse a proveedores de LLM y generar instrucciones de proyecto, Flight te ayuda a ti y a tu equipo a obtener ayuda consistente y relevante sin tener que pegar el mismo contexto en cada chat.
Comprensión
Los asistentes de codificación con IA son más útiles cuando comprenden el contexto, las convenciones y los objetivos de tu proyecto. Los ayudantes de IA de Flight te permiten:
- Conectar tu proyecto a proveedores de LLM populares (OpenAI, Grok, Claude, etc.)
- Generar y actualizar instrucciones específicas del proyecto para que todos reciban la misma guía
- Mantener el código escrito a mano y el generado por IA en un solo diseño (especialmente con el skeleton)
Estas características vienen con el CLI principal de Flight (a través de Runway) y están preconfiguradas en el iniciador oficial flightphp/skeleton.
Lo que el skeleton incluye para IA
El iniciador oficial trata AGENTS.md como la fuente de verdad para las herramientas de IA:
| Archivo | Rol |
|---|---|
AGENTS.md (raíz del proyecto) |
Reglas globales, flujo de arranque, espacios de nombres, DI, "qué no hacer" |
AGENTS.md con ámbito en app/, migrations/, tests/, etc. |
Consejos ligeros y específicos de carpeta cuando trabajas en ese árbol |
SECURITY.md |
Secretos, cabeceras, XSS/SQL, reportes—la seguridad se mantiene deliberada y separada |
No hay un archivo de estilo de casa separado para Copilot / Cursor / Gemini / Windsurf en el skeleton. Señala a tu asistente al AGENTS.md raíz (y deja que siga los enlaces a los archivos con ámbito). Los humanos pueden ignorar estos archivos por completo y usar el README; el diseño es el mismo de cualquier manera.
Los documentos enseñan APIs; el skeleton enseña el diseño. Los ejemplos cortos de
Flight::en estos documentos son excelentes para aprender. En una aplicación skeleton, prefiere las clasesApp\…, la inyección de constructor y$this->appsobre la fachada estática dentro de los controladores. Consulta Instalación y Autocarga.
Uso Básico
Configuración de Credenciales de LLM
El comando ai:init te guía a través de la conexión de tu proyecto a un proveedor de LLM.
php runway ai:init
Se te pedirá que:
- Elijas tu proveedor (OpenAI, Grok, Claude, etc.)
- Ingreses tu clave de API
- Establezcas la URL base y el nombre del modelo
Esto crea las credenciales utilizadas para solicitudes posteriores de LLM (por ejemplo, para generar instrucciones).
Ejemplo:
Welcome to AI Init!
Which LLM API do you want to use? [1] openai, [2] grok, [3] claude: 1
Enter the base URL for the LLM API [https://api.openai.com]:
Enter your API key for openai: sk-...
Enter the model name you want to use (e.g. gpt-4, claude-3-opus, etc) [gpt-4o]:
Credentials saved to .runway-creds.json
Generación de Instrucciones de IA Específicas del Proyecto
El comando ai:generate-instructions crea o actualiza instrucciones para asistentes de codificación con IA, adaptadas a tu proyecto.
php runway ai:generate-instructions
Responderás algunas preguntas (descripción, base de datos, plantillas, seguridad, tamaño del equipo, etc.). Flight utiliza tu proveedor de LLM para generar las instrucciones y las escribe principalmente en:
AGENTS.mden la raíz del proyecto (independiente de la herramienta; lo que el skeleton oficial y la mayoría de los agentes modernos esperan)
Dependiendo de la versión del CLI y las opciones, el comando también puede escribir copias específicas de herramientas para flujos de trabajo más antiguos (por ejemplo, archivos de reglas de Copilot, Cursor, Windsurf o Gemini). Para proyectos nuevos desde el skeleton, trata AGENTS.md (más cualquier archivo AGENTS.md con ámbito que mantengas bajo app/) como la única fuente de verdad—no mantengas cinco archivos de instrucciones divergentes a mano.
Ejemplo:
Please describe what your project is for? My awesome API
What database are you planning on using? MySQL
What HTML templating engine will you plan on using (if any)? twig
Is security an important element of this project? (y/n) y
...
AI instructions updated successfully.
Ahora las herramientas de IA pueden sugerir código que coincida con tu pila y diseño reales, no un tutorial genérico de PHP.
Uso Avanzado
- Personaliza credenciales o rutas de salida con las opciones de los comandos (consulta
--helpen cada comando). - Los ayudantes funcionan con cualquier proveedor de LLM que hable una API compatible con OpenAI.
- Vuelve a ejecutar
ai:generate-instructionsa medida que el proyecto evolucione para que los agentes se mantengan sincronizados. - En el skeleton, mantén la política de seguridad en
SECURITY.mdy el diseño de codificación enAGENTS.mdpara que ningún documento se convierta en una caja de todo. - Prefiere docs.flightphp.com y el servidor MCP de Flight cuando los agentes necesiten detalles de la API; verifica los métodos inventados contra
vendor/flightphp/core.
Ver También
- Flight Skeleton – Iniciador oficial con
AGENTS.md, Twig, SimplePdo y Dice configurados para una estructura amigable con IA - Instalación – Diseño recomendado de
create-project - Autocarga – Las mayúsculas de las carpetas coinciden con los espacios de nombres (
App\Controller↔app/Controller/) - CLI de Runway – CLI que impulsa los comandos
ai:*y de scaffolding - Seguridad – Valores predeterminados seguros que los agentes (y los humanos) no deberían debilitar
Solución de Problemas
- Si ves "Missing .runway-creds.json", ejecuta
php runway ai:initprimero. - Asegúrate de que tu clave de API sea válida y tenga acceso al modelo seleccionado.
- Si las instrucciones no se actualizan, verifica los permisos de archivo en el directorio de tu proyecto.
- Si un agente inventa APIs de Flight o el diseño de carpetas incorrecto, apúntalo al
AGENTS.mdraíz y a este sitio de documentación; el diseño del skeleton tiene prioridad para el código bajoapp/.
Registro de Cambios
- v3.18.4 –
ai:generate-instructionsescribe las instrucciones del proyecto enAGENTS.mden la raíz del proyecto. - v3.16.0 – Se agregaron los comandos CLI
ai:inityai:generate-instructionspara la integración de IA.
Learn/unit_testing_and_solid_principles
Este artículo se publicó originalmente en Airpair en 2015. Todo el crédito se otorga a Airpair y Brian Fenton, quien escribió originalmente este artículo, aunque el sitio web ya no está disponible y el artículo solo existe en la Wayback Machine. Este artículo se ha agregado al sitio con fines educativos y de aprendizaje para la comunidad de PHP en general.
1 Configuración y configuración inicial
1.1 Mantenerse actualizado
Digamos esto desde el principio: un número deprimentemente pequeño de instalaciones de PHP en uso están actualizadas o se mantienen actualizadas. Ya sea debido a restricciones de alojamiento compartido, valores predeterminados que nadie piensa en cambiar o falta de tiempo/presupuesto para las pruebas de actualización, los humildes binarios de PHP tienden a quedarse atrás. Por lo tanto, una práctica recomendada clara que necesita más énfasis es siempre usar una versión actual de PHP (5.6.x en el momento de este artículo). Además, también es importante programar actualizaciones regulares tanto de PHP como de cualquier extensión o bibliotecas de proveedores que pueda estar usando. Las actualizaciones le brindan nuevas características del lenguaje, mayor velocidad, menor uso de memoria y actualizaciones de seguridad. Cuanto más frecuentemente actualice, menos doloroso se vuelve el proceso.
1.2 Establecer valores predeterminados sensatos
PHP hace un trabajo decente al establecer buenos valores predeterminados de la caja con sus archivos php.ini.development y php.ini.production, pero podemos hacerlo mejor. Por un lado, no establecen una zona horaria para nosotros. Eso tiene sentido desde una perspectiva de distribución, pero sin una, PHP lanzará un error E_WARNING cada vez que llamemos a una función relacionada con fecha/hora. Aquí hay algunas configuraciones recomendadas:
- date.timezone - elija de la lista de zonas horarias compatibles
- session.savepath - si estamos usando archivos para sesiones y no algún otro controlador de guardado, establezca esto en algo fuera de /tmp. Dejar esto como /tmp puede ser riesgoso en un entorno de alojamiento compartido ya que /tmp_ suele tener permisos amplios. Incluso con el bit sticky establecido, cualquiera con acceso para listar el contenido de este directorio puede aprender todos sus ID de sesión activos.
- session.cookie_secure - obvio, active esto si está sirviendo su código PHP a través de HTTPS.
- session.cookie_httponly - establezca esto para evitar que las cookies de sesión de PHP sean accesibles a través de JavaScript
- Más... use una herramienta como iniscan para probar su configuración en busca de vulnerabilidades comunes
1.3 Extensiones
También es una buena idea deshabilitar (o al menos no habilitar) extensiones que no usará, como controladores de bases de datos. Para ver qué está habilitado, ejecute el comando phpinfo() o vaya a la línea de comandos y ejecute esto.
$ php -i
La información es la misma, pero phpinfo() tiene formato HTML agregado. La versión de CLI es más fácil de canalizar a grep para encontrar información específica. Ej.
$ php -i | grep error_log
Una advertencia de este método: es posible tener configuraciones de PHP diferentes que se aplican a la versión orientada a la web y a la versión de CLI.
2 Use Composer
Esto puede sorprender, pero una de las mejores prácticas para escribir PHP moderno es escribir menos de él. Si bien es cierto que una de las mejores formas de mejorar en la programación es hacerlo, hay un gran número de problemas que ya se han resuelto en el espacio de PHP, como enrutamiento, bibliotecas de validación de entrada básica, conversión de unidades, capas de abstracción de bases de datos, etc... Solo vaya a Packagist y explore. Probablemente descubrirá que porciones significativas del problema que está intentando resolver ya se han escrito y probado.
Si bien es tentador escribir todo el código usted mismo (y no hay nada malo en escribir su propio framework o biblioteca como una experiencia de aprendizaje), debe luchar contra esos sentimientos de "No Inventado Aquí" y ahorrarse mucho tiempo y dolor de cabeza. Siga la doctrina de PIE en su lugar: Orgullosamente Inventado En Otro Lugar. Además, si decide escribir su propio "lo que sea", no lo libere a menos que haga algo significativamente diferente o mejor que las ofertas existentes.
Composer es un gestor de paquetes para PHP, similar a pip en Python, gem en Ruby y npm en Node. Le permite definir un archivo JSON que lista las dependencias de su código y tratará de resolver esos requisitos descargando e instalando los paquetes de código necesarios.
2.1 Instalación de Composer
Suponiendo que esto es un proyecto local, instalemos una instancia de Composer solo para el proyecto actual. Navegue a su directorio de proyecto y ejecute esto:
$ curl -sS https://getcomposer.org/installer | php
Tenga en cuenta que canalizar cualquier descarga directamente a un intérprete de scripts (sh, ruby, php, etc.) es un riesgo de seguridad, así que lea el código de instalación y asegúrese de estar cómodo con él antes de ejecutar cualquier comando como este.
Por conveniencia (si prefieres escribir composer install en lugar de php composer.phar install), puedes usar este comando para instalar una copia única de composer de forma global:
$ mv composer.phar /usr/local/bin/composer
$ chmod +x composer
Es posible que necesite ejecutar esos con sudo dependiendo de sus permisos de archivo.
2.2 Usar Composer
Composer tiene dos categorías principales de dependencias que puede gestionar: "require" y "require-dev". Las dependencias listadas como "require" se instalan en todas partes, pero las dependencias "require-dev" solo se instalan cuando se solicitan específicamente. Por lo general, estas son herramientas para cuando el código está en desarrollo activo, como PHP_CodeSniffer. La línea a continuación muestra un ejemplo de cómo instalar Guzzle, una biblioteca HTTP popular.
$ php composer.phar require guzzle/guzzle
Para instalar una herramienta solo con fines de desarrollo, agregue la bandera --dev:
$ php composer.phar require --dev 'sebastian/phpcpd'
Esto instala PHP Copy-Paste Detector, otra herramienta de calidad de código como una dependencia solo para desarrollo.
2.3 Instalar vs actualizar
Cuando ejecutamos composer install por primera vez, instalará cualquier biblioteca y sus dependencias que necesitemos, basadas en el archivo composer.json. Una vez hecho eso, composer crea un archivo de bloqueo, predeciblemente llamado composer.lock. Este archivo contiene una lista de las dependencias que composer encontró para nosotros y sus versiones exactas, con hashes. Luego, cualquier futura vez que ejecutemos composer install, mirará en el archivo de bloqueo e instalará esas versiones exactas.
composer update es un poco diferente. Ignorará el archivo composer.lock (si está presente) e intentará encontrar las versiones más actualizadas de cada una de las dependencias que aún satisfagan las restricciones en composer.json. Luego escribe un nuevo archivo composer.lock cuando termine.
2.4 Autocarga
Tanto composer install como composer update generarán un autocargador para nosotros que le indica a PHP dónde encontrar todos los archivos necesarios para usar las bibliotecas que acabamos de instalar. Para usarlo, solo agregue esta línea (generalmente a un archivo de inicialización que se ejecute en cada solicitud):
require 'vendor/autoload.php';
3 Siga buenos principios de diseño
3.1 SOLID
SOLID es un mnemotécnico para recordarnos cinco principios clave en el buen diseño de software orientado a objetos.
3.1.1 S - Principio de Responsabilidad Única
Esto establece que las clases solo deben tener una responsabilidad, o dicho de otra manera, solo deben tener una sola razón para cambiar. Esto encaja bien con la filosofía de Unix de muchas herramientas pequeñas, haciendo una cosa bien. Las clases que solo hacen una cosa son mucho más fáciles de probar y depurar, y son menos propensas a sorprenderte. No quieres que una llamada a un método de una clase Validator actualice registros de base de datos. Aquí hay un ejemplo de una violación de SRP, como la que comúnmente verías en una aplicación basada en el patrón ActiveRecord.
class Person extends Model
{
public $name;
public $birthDate;
protected $preferences;
public function getPreferences() {}
public function save() {}
}
Entonces esto es un modelo de entidad bastante básico. Sin embargo, una de estas cosas no pertenece aquí. La única responsabilidad de un modelo de entidad debería ser el comportamiento relacionado con la entidad que representa, no debería ser responsable de persistirse a sí mismo.
class Person extends Model
{
public $name;
public $birthDate;
protected $preferences;
public function getPreferences() {}
}
class DataStore
{
public function save(Model $model) {}
}
Esto es mejor. El modelo Person está de vuelta a hacer solo una cosa, y el comportamiento de guardado se ha movido a un objeto de persistencia en su lugar. Nota también que solo hice una sugerencia de tipo en Model, no en Person. Volveremos a eso cuando lleguemos a las partes L y D de SOLID.
3.1.2 O - Principio Abierto-Cerrado
Hay una prueba increíble para esto que resume bastante bien qué es este principio: piensa en una característica para implementar, probablemente la más reciente en la que trabajaste o en la que estás trabajando. ¿Puedes implementar esa característica en tu base de código existente SOLO agregando nuevas clases y no cambiando ninguna clase existente en tu sistema? Tu código de configuración y cableado obtiene un poco de indulgencia, pero en la mayoría de los sistemas esto es sorprendentemente difícil. Tienes que depender mucho de la despacho polimórfica y la mayoría de las bases de código simplemente no están configuradas para eso. Si estás interesado en eso, hay una buena charla de Google en YouTube sobre polimorfismo y escribir código sin Ifs que profundiza más. Como bono, la charla la da Miško Hevery, a quien muchos pueden conocer como el creador de AngularJs.
3.1.3 L - Principio de Sustitución de Liskov
Este principio lleva el nombre de Barbara Liskov y se imprime a continuación:
"Los objetos en un programa deberían ser reemplazables con instancias de sus subtipos sin alterar la corrección de ese programa."
Esto suena bien, pero se ilustra más claramente con un ejemplo.
abstract class Shape
{
public function getHeight();
public function setHeight($height);
public function getLength();
public function setLength($length);
}
Esto va a representar nuestra forma básica de cuatro lados. Nada fancy aquí.
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;
}
}
Aquí está nuestra primera forma, el Cuadrado. Forma bastante directa, ¿verdad? Puedes asumir que hay un constructor donde establecemos las dimensiones, pero ves aquí de esta implementación que la longitud y la altura siempre van a ser las mismas. Los cuadrados son así.
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;
}
}
Así que aquí tenemos una forma diferente. Todavía tiene las mismas firmas de métodos, todavía es una forma de cuatro lados, pero ¿qué pasa si empezamos a intentar usarlos en lugar de uno del otro? De repente, si cambiamos la altura de nuestra Shape, ya no podemos asumir que la longitud de nuestra forma coincida. Hemos violado el contrato que teníamos con el usuario cuando les dimos nuestra forma Square.
Este es un ejemplo de libro de texto de una violación del LSP y necesitamos este tipo de principio en su lugar para hacer el mejor uso de un sistema de tipos. Incluso duck typing no nos dirá si el comportamiento subyacente es diferente, y como no podemos saberlo sin verlo romperse, es mejor asegurarse de que no lo sea en primer lugar.
3.1.3 I - Principio de Segregación de Interfaces
Este principio dice favorecer muchas interfaces pequeñas y detalladas en lugar de una grande. Las interfaces deberían basarse en el comportamiento en lugar de "es una de estas clases". Piense en interfaces que vienen con PHP. Traversable, Countable, Serializable, cosas como esas. Anuncian capacidades que posee el objeto, no de qué hereda. Así que mantenga sus interfaces pequeñas. No quieres una interfaz con 30 métodos, 3 es un objetivo mucho mejor.
3.1.4 D - Principio de Inversión de Dependencias
Probablemente hayas oído hablar de esto en otros lugares que hablaron de Inyección de Dependencias, pero Inversión de Dependencias e Inyección de Dependencias no son exactamente lo mismo. La inversión de dependencias es realmente solo una forma de decir que deberías depender de abstracciones en tu sistema y no de sus detalles. ¿Qué significa eso para ti en el día a día?
No uses directamente mysqli_query() en todo tu código, usa algo como DataStore->query() en su lugar.
El núcleo de este principio es en realidad sobre abstracciones. Se trata más de decir "usa un adaptador de base de datos" en lugar de depender de llamadas directas a cosas como mysqli_query. Si estás usando directamente mysqli_query en la mitad de tus clases, estás atando todo directamente a tu base de datos. Nada en contra de MySQL aquí, pero si estás usando mysqli_query, ese tipo de detalle de bajo nivel debería estar oculto en solo un lugar y luego esa funcionalidad debería exponerse a través de un contenedor genérico.
Ahora sé que este es un ejemplo un poco trillado si lo piensas, porque el número de veces que vas a cambiar completamente el motor de tu base de datos después de que tu producto esté en producción es muy, muy bajo. Lo elegí porque pensé que la gente estaría familiarizada con la idea de su propio código. Además, incluso si tienes una base de datos con la que te vas a quedar, ese objeto contenedor abstracto te permite arreglar errores, cambiar el comportamiento o implementar características que deseas que tuviera tu base de datos elegida. También hace posible las pruebas unitarias donde las llamadas de bajo nivel no lo harían.
4 Calistenia de objetos
Esto no es un buceo completo en estos principios, pero los dos primeros son fáciles de recordar, proporcionan un buen valor y se pueden aplicar inmediatamente a casi cualquier base de código.
4.1 No más de un nivel de indentación por método
Esta es una forma útil de pensar en descomponer métodos en fragmentos más pequeños, dejándote con código que es más claro y autodocumentado. Cuantos más niveles de indentación tengas, más está haciendo el método y más estado tienes que rastrear en tu cabeza mientras trabajas con él.
Inmediatamente sé que la gente objetará esto, pero esto es solo una guía/heurística, no una regla estricta. No espero que nadie haga cumplir reglas de PHP_CodeSniffer para esto (aunque la gente ha).
Pasemos por una muestra rápida de cómo podría verse esto:
public function transformToCsv($data)
{
$csvLines = array();
$csvLines[] = implode(',', array_keys($data[0]));
foreach ($data as $row) {
if (!$row) {
continue;
}
$csvLines[] = implode(',', $row);
}
return $csvLines;
}
Si bien este no es un código terrible (es técnicamente correcto, probables, etc.), podemos hacer mucho más para aclararlo. ¿Cómo reduciríamos los niveles de anidamiento aquí?
Sabemos que necesitamos simplificar enormemente el contenido del bucle foreach (o eliminarlo por completo), así que empecemos allí.
if (!$row) {
continue;
}
Esta primera parte es fácil. Todo lo que está haciendo es ignorar filas vacías. Podemos acortar todo este proceso usando una función incorporada de PHP antes de llegar siquiera al bucle.
$data = array_filter($data);
foreach ($data as $row) {
$csvLines[] = implode(',', $row);
}
Ahora tenemos nuestro único nivel de anidamiento. Pero mirando esto, todo lo que estamos haciendo es aplicar una función a cada elemento de un arreglo. Ni siquiera necesitamos el bucle foreach para eso.
$data = array_filter($data);
$csvLines = array_map(function($row) {
return implode(',', $row);
}, $data);
Ahora no tenemos anidamiento en absoluto, y el código probablemente será más rápido ya que estamos haciendo todo el bucle con funciones nativas en C en lugar de PHP. Tenemos que participar en un poco de truco para pasar la coma a implode, así que podrías argumentar que detenerte en el paso anterior es mucho más comprensible.
4.2 Intenta no usar else
Esto realmente trata con dos ideas principales. La primera es múltiples declaraciones de retorno de un método. Si tienes suficiente información para tomar una decisión sobre el resultado del método, adelante, toma esa decisión y retorna. La segunda es una idea conocida como Cláusulas de Guardia. Estas son básicamente verificaciones de validación combinadas con retornos tempranos, generalmente cerca de la parte superior de un método. Déjame mostrarte lo que quiero decir.
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;
}
Entonces esto es bastante directo, suma 3 enteros y devuelve el resultado, o null si cualquiera de los parámetros no es un entero. Ignorando el hecho de que podríamos combinar todas esas verificaciones en una sola línea con operadores AND, creo que puedes ver cómo la estructura if/else anidada hace que el código sea más difícil de seguir. Ahora mira este ejemplo en su lugar.
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;
}
Para mí, este ejemplo es mucho más fácil de seguir. Aquí estamos usando cláusulas de guardia para verificar nuestras aserciones iniciales sobre los parámetros que estamos pasando e inmediatamente saliendo del método si no pasan. También ya no tenemos la variable intermedia para rastrear la suma a lo largo del método. En este caso hemos verificado que ya estamos en el camino feliz y podemos simplemente hacer lo que vinimos a hacer. Nuevamente podríamos hacer todas esas verificaciones en un if solo, pero el principio debería estar claro.
5 Pruebas unitarias
Las pruebas unitarias son la práctica de escribir pruebas pequeñas que verifican el comportamiento en tu código. Casi siempre se escriben en el mismo lenguaje que el código (en este caso PHP) y están destinadas a ser lo suficientemente rápidas como para ejecutarse en cualquier momento. Son extremadamente valiosas como una herramienta para mejorar tu código. Además de los beneficios obvios de asegurar que tu código esté haciendo lo que crees que está haciendo, las pruebas unitarias pueden proporcionar retroalimentación de diseño muy útil también. Si un pedazo de código es difícil de probar, a menudo muestra problemas de diseño. También te dan una red de seguridad contra regresiones, y eso te permite refactorizar mucho más a menudo y evolucionar tu código a un diseño más limpio.
5.1 Herramientas
Hay varias herramientas de pruebas unitarias allí en PHP, pero con diferencia la más común es PHPUnit. Puedes instalarla descargando un PHAR directamente, o instalarla con composer. Dado que estamos usando composer para todo lo demás, mostraremos ese método. Además, como PHPUnit probablemente no se desplegará en producción, podemos instalarlo como una dependencia de desarrollo con el siguiente comando:
composer require --dev phpunit/phpunit
5.2 Las pruebas son una especificación
El rol más importante de las pruebas unitarias en tu código es proporcionar una especificación ejecutable de lo que se supone que debe hacer el código. Incluso si el código de prueba está equivocado o el código tiene errores, el conocimiento de lo que el sistema se supone que debe hacer es invaluable.
5.3 Escribe tus pruebas primero
Si has tenido la oportunidad de ver un conjunto de pruebas escritas antes del código y uno escrito después de que el código se terminó, son notablemente diferentes. Las pruebas "después" están mucho más preocupadas por los detalles de implementación de la clase y asegurándose de que tengan una buena cobertura de líneas, mientras que las pruebas "antes" se centran más en verificar el comportamiento externo deseado. Eso es realmente lo que nos importa con las pruebas unitarias de todos modos, es asegurarnos de que la clase exhiba el comportamiento correcto. Las pruebas enfocadas en la implementación realmente hacen que el refactoring sea más difícil porque se rompen si los internos de las clases cambian, y acabas de costarte los beneficios de ocultación de información de OOP.
5.4 Qué hace una buena prueba unitaria
Las buenas pruebas unitarias comparten muchas de las siguientes características:
- Rápidas - deberían ejecutarse en milisegundos.
- Sin acceso a la red - deberían poder apagar el inalámbrico/desconectar y todas las pruebas aún pasar.
- Acceso limitado al sistema de archivos - esto agrega velocidad y flexibilidad si se despliega código a otros entornos.
- Sin acceso a la base de datos - evita actividades costosas de configuración y desmontaje.
- Prueba solo una cosa a la vez - una prueba unitaria debería tener solo una razón para fallar.
- Bien nombradas - véase 5.2 arriba.
- Mayormente objetos falsos - los únicos "reales" objetos en pruebas unitarias deberían ser el objeto que estamos probando y objetos de valor simples. El resto debería ser alguna forma de doble de prueba
Hay razones para ir en contra de algunas de estas, pero como guías generales te servirán bien.
5.5 Cuando las pruebas son dolorosas
Las pruebas unitarias te obligan a sentir el dolor del mal diseño desde el principio - Michael Feathers
Cuando escribes pruebas unitarias, te obligas a usar realmente la clase para lograr cosas. Si escribes pruebas al final, o peor aún, solo arrojas el código sobre la pared para QA o quien sea para escribir pruebas, no obtienes retroalimentación sobre cómo se comporta realmente la clase. Si estamos escribiendo pruebas y la clase es un dolor real de usar, lo descubriremos mientras la escribimos, que es casi el momento más barato para arreglarlo.
Si una clase es difícil de probar, es un defecto de diseño. Diferentes defectos se manifiestan de diferentes maneras. Si tienes que hacer un montón de burlas, tu clase probablemente tiene demasiadas dependencias o tus métodos están haciendo demasiado. Cuanto más configuración tengas que hacer para cada prueba, más probable es que tus métodos estén haciendo demasiado. Si tienes que escribir escenarios de prueba realmente enredados para ejercer el comportamiento, los métodos de la clase probablemente están haciendo demasiado. Si tienes que cavar dentro de un montón de métodos privados y estado para probar cosas, quizás haya otra clase tratando de salir. Las pruebas unitarias son muy buenas para exponer "clases iceberg" donde el 80% de lo que hace la clase está oculto en código protegido o privado. Solía ser un gran fan de hacer lo más posible protegido, pero ahora me di cuenta de que solo estaba haciendo que mis clases individuales fueran responsables de demasiado, y la solución real era dividir la clase en piezas más pequeñas.
Escrito por Brian Fenton - Brian Fenton ha sido un desarrollador de PHP durante 8 años en el Medio Oeste y el Área de la Bahía, actualmente en Thismoment. Se enfoca en la artesanía del código y los principios de diseño. Blog en www.brianfenton.us, Twitter en @brianfenton. Cuando no está ocupado siendo padre, disfruta de la comida, la cerveza, los juegos y el aprendizaje.
Learn/security
Seguridad
Resumen
La seguridad es un gran tema cuando se trata de aplicaciones web. Debes asegurarte de que tu aplicación sea segura y de que los datos de tus usuarios estén a salvo. Flight proporciona una serie de características para ayudarte a proteger tus aplicaciones web.
El esqueleto oficial también incluye un SECURITY.md dedicado y middleware de encabezados de seguridad para que las herramientas de codificación con IA (y los humanos) tengan un lugar deliberado para secretos, encabezados y reglas XSS/SQL, separado del estilo de codificación general en AGENTS.md.
Comprensión
Existen varias amenazas de seguridad comunes que debes conocer al crear aplicaciones web. Algunas de las amenazas más comunes incluyen:
- Cross Site Request Forgery (CSRF) (Falsificación de solicitudes entre sitios)
- Cross Site Scripting (XSS) (Scripting entre sitios)
- Inyección SQL
- Cross Origin Resource Sharing (CORS) (Intercambio de recursos de origen cruzado)
Las plantillas ayudan contra XSS al escapar la salida de forma predeterminada (Twig y Latte lo hacen; aprovecha esa ventaja). Las sesiones pueden ayudar con CSRF almacenando un token CSRF en la sesión del usuario como se describe a continuación. El uso de consultas preparadas con PDO—o de los ayudantes en SimplePdo—ayuda a prevenir la inyección SQL. CORS puede manejarse con un simple hook antes de que se llame a Flight::start().
Todos estos métodos trabajan juntos para ayudar a mantener seguras tus aplicaciones web. Siempre debes tener presente aprender y comprender las mejores prácticas de seguridad. No le pidas a un asistente de IA que "desactive CSP" o que debilite los encabezados solo para hacer que una página cargue sin comprender la compensación.
Uso básico
Encabezados
Los encabezados HTTP son una de las formas más fáciles de proteger tus aplicaciones web. Puedes usar encabezados para prevenir clickjacking, XSS y otros ataques. Hay varias formas de agregar estos encabezados a tu aplicación.
Dos excelentes sitios web para verificar la seguridad de tus encabezados son securityheaders.com y observatory.mozilla.org. Después de configurar el código a continuación, puedes verificar fácilmente que tus encabezados funcionan con esos dos sitios web.
El esqueleto incluye App\Middleware\SecurityHeadersMiddleware (CSP con un nonce por solicitud, opciones de marco, HSTS y más). Prefiere extender eso deliberadamente en lugar de desactivar los encabezados.
Agregar manualmente
Puedes agregar estos encabezados manualmente usando el método header en el objeto Flight\Response.
// Establece el encabezado X-Frame-Options para prevenir el clickjacking
Flight::response()->header('X-Frame-Options', 'SAMEORIGIN');
// Establece el encabezado Content-Security-Policy para prevenir XSS
// Nota: este encabezado puede volverse muy complejo, por lo que querrás
// consultar ejemplos en internet para tu aplicación
Flight::response()->header("Content-Security-Policy", "default-src 'self'");
// Establece el encabezado X-XSS-Protection para prevenir XSS
Flight::response()->header('X-XSS-Protection', '1; mode=block');
// Establece el encabezado X-Content-Type-Options para prevenir la detección de MIME
Flight::response()->header('X-Content-Type-Options', 'nosniff');
// Establece el encabezado Referrer-Policy para controlar cuánta información de referrer se envía
Flight::response()->header('Referrer-Policy', 'no-referrer-when-downgrade');
// Establece el encabezado Strict-Transport-Security para forzar HTTPS
Flight::response()->header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
// Establece el encabezado Permissions-Policy para controlar qué funciones y APIs se pueden usar
Flight::response()->header('Permissions-Policy', 'geolocation=()');
Estos se pueden agregar al principio de tus archivos routes.php o index.php.
Agregar como filtro
También puedes agregarlos en un filtro/hook de la siguiente manera:
// Agrega los encabezados en un filtro
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=()');
});
Agregar como middleware
También puedes agregarlos como una clase de middleware, lo que brinda la mayor flexibilidad sobre a qué rutas aplicar esto. En general, estos encabezados deberían aplicarse a todas las respuestas HTML y API.
Ruta y espacio de nombres estilo esqueleto (la carpeta coincide con 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();
// Prefiere un nonce CSP del bootstrap cuando tengas scripts en línea (el esqueleto define 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 — grupo vacío = middleware global para todas las rutas
use App\Middleware\SecurityHeadersMiddleware;
use flight\net\Router;
$router->group('', function (Router $router) {
$router->get('/users', [ \App\Controller\UserController::class, 'getUsers' ]);
// más rutas
}, [SecurityHeadersMiddleware::class]);
Los proyectos más antiguos pueden seguir usando app/middlewares y app\middlewares; eso funciona si las carpetas coinciden. Las nuevas aplicaciones esqueleto usan app/Middleware/ y App\Middleware. Consulta Autocarga.
Falsificación de solicitudes entre sitios (CSRF)
Cross Site Request Forgery (CSRF) es un tipo de ataque en el que un sitio web malicioso puede hacer que el navegador de un usuario envíe una solicitud a tu sitio web. Esto puede usarse para realizar acciones en tu sitio web sin el conocimiento del usuario. Flight no proporciona un mecanismo de protección CSRF integrado, pero puedes implementar fácilmente el tuyo propio usando middleware.
Configuración
Primero necesitas generar un token CSRF y almacenarlo en la sesión del usuario. Luego puedes usar este token en tus formularios y verificarlo cuando se envíe el formulario. Usaremos el plugin flightphp/session para gestionar las sesiones.
// Genera un token CSRF y lo almacena en la sesión del usuario
// (asumiendo que has creado un objeto de sesión y lo has adjuntado a Flight)
// consulta la documentación de sesiones para más información
Flight::register('session', flight\Session::class);
// Solo necesitas generar un token por sesión (para que funcione
// en múltiples pestañas y solicitudes para el mismo usuario)
if(Flight::session()->get('csrf_token') === null) {
Flight::session()->set('csrf_token', bin2hex(random_bytes(32)) );
}
Usando la plantilla PHP predeterminada de Flight
<!-- Usa el token CSRF en tu formulario -->
<form method="post">
<input type="hidden" name="csrf_token" value="<?= Flight::session()->get('csrf_token') ?>">
<!-- otros campos del formulario -->
</form>
Usando Twig (predeterminado del esqueleto)
Registra una función de Twig o pasa el token a cada vista de formulario. Ejemplo mínimo con un global y un campo de formulario:
// Al configurar Twig (por ejemplo, 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 }}">
{# otros campos #}
</form>
Usando Latte
También puedes configurar una función personalizada para mostrar el token CSRF en tus plantillas Latte.
Flight::map('render', function(string $template, array $data, ?string $block): void {
$latte = new Latte\Engine;
// otras configuraciones...
// Configura una función personalizada para mostrar el token 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);
});
Y ahora en tus plantillas Latte puedes usar la función csrf() para mostrar el token CSRF.
<form method="post">
{csrf()}
<!-- otros campos del formulario -->
</form>
Verificar el token CSRF
Puedes verificar el token CSRF usando varios métodos.
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' ]);
// más rutas
}, [CsrfMiddleware::class]);
Filtros de eventos
// Este middleware verifica si la solicitud es POST y, si lo es, comprueba si el token CSRF es válido
Flight::before('start', function() {
if(Flight::request()->method == 'POST') {
// captura el token csrf de los valores del formulario
$token = Flight::request()->data->csrf_token;
if($token !== Flight::session()->get('csrf_token')) {
Flight::halt(403, 'Invalid CSRF token');
// o para una respuesta JSON
Flight::jsonHalt(['error' => 'Invalid CSRF token'], 403);
}
}
});
Cross Site Scripting (XSS)
Cross Site Scripting (XSS) es un tipo de ataque en el que una entrada de formulario maliciosa puede inyectar código en tu sitio web. La mayoría de estas oportunidades provienen de valores de formulario que tus usuarios finales completarán. Nunca debes confiar en la salida de tus usuarios. Siempre asume que todos son los mejores hackers del mundo. Pueden inyectar JavaScript o HTML malicioso en tu página. Este código puede usarse para robar información de tus usuarios o realizar acciones en tu sitio web. Usando la clase de vista de Flight o un motor de plantillas como Twig o Latte, puedes escapar fácilmente la salida para prevenir ataques XSS.
// Supongamos que el usuario es inteligente e intenta usar esto como su nombre
$name = '<script>alert("XSS")</script>';
// Esto escapará la salida
Flight::view()->set('name', $name);
// Esto mostrará: <script>alert("XSS")</script>
// Twig (predeterminado del esqueleto) y Latte escapan automáticamente por defecto — prefierelos sobre echo PHP sin formato
Flight::render('template', ['name' => $name]);
// Twig: {{ name }} → escapado
// Evita |raw / salida sin escapar a menos que el contenido sea totalmente confiable
Inyección SQL
SQL Injection es un tipo de ataque en el que un usuario malicioso puede inyectar código SQL en tu base de datos. Esto puede usarse para robar información de tu base de datos o realizar acciones en ella. Nuevamente, nunca debes confiar en la entrada de tus usuarios. Siempre asume que buscan sangre. Usa consultas preparadas —los ayudantes de SimplePdo hacen que este sea el camino predeterminado.
// Asumiendo que tienes Flight::db() registrado como SimplePdo (o inyecta SimplePdo en el controlador)
$statement = Flight::db()->prepare('SELECT * FROM users WHERE username = :username');
$statement->execute([':username' => $username]);
$users = $statement->fetchAll();
// SimplePdo (preferido) — líneas de una sola expresión con parámetros vinculados
$users = Flight::db()->fetchAll('SELECT * FROM users WHERE username = :username', [ 'username' => $username ]);
// Misma idea con comodines ?
$users = Flight::db()->fetchAll('SELECT * FROM users WHERE username = ?', [ $username ]);
En los controladores estilo esqueleto, prefiere la inyección por constructor de SimplePdo sobre Flight::db() para que las pruebas y el código generado por IA se mantengan consistentes (DIC).
Ejemplo inseguro
Lo siguiente es por qué usamos consultas preparadas SQL para proteger contra ejemplos inocentes como el siguiente:
// el usuario final completa un formulario web.
// para el valor del formulario, el hacker pone algo como esto:
$username = "' OR 1=1; -- ";
$sql = "SELECT * FROM users WHERE username = '$username' LIMIT 5";
$users = Flight::db()->fetchAll($sql);
// Después de que la consulta se construye, se ve así
// SELECT * FROM users WHERE username = '' OR 1=1; -- LIMIT 5
// Parece extraño, pero es una consulta válida que funcionará. De hecho,
// es un ataque de inyección SQL muy común que devolverá todos los usuarios.
var_dump($users); // esto volcará todos los usuarios en la base de datos, no solo el único nombre de usuario
Secretos y configuración
- Coloca los secretos en
.env(o en el entorno real), no en muestras deconfig.phpque se confirmen en el repositorio. - Regla del esqueleto: valores predeterminados literales en
config.php; fusiona el entorno en el bootstrap; no leas$_ENVdentro de los controladores — inyecta la configuración en su lugar. Consulta Configuración. - Nunca confirmes claves de API, contraseñas de bases de datos o claves de cifrado de sesiones. Apunta las herramientas de IA a
SECURITY.mdpara que no inventen atajos inseguros.
Validación de devolución de llamada JSONP
Si usas el método Flight::jsonp(), ten en cuenta que Flight valida el nombre del parámetro de devolución de llamada JSONP contra una lista blanca estricta de expresiones regulares (/^[A-Za-z_$][\w$.]{0,127}$/). Cualquier nombre de devolución de llamada que no coincida con este patrón hará que Flight lance una excepción, evitando la inyección de JavaScript arbitrario a través de un valor de devolución de llamada malicioso.
Esta validación está integrada y no requiere configuración adicional, pero vale la pena conocerla al depurar errores inesperados de endpoints JSONP.
CORS
Cross-Origin Resource Sharing (CORS) es un mecanismo que permite que muchos recursos (por ejemplo, fuentes, JavaScript, etc.) en una página web sean solicitados desde otro dominio fuera del dominio desde el cual se originó el recurso. Flight no tiene funcionalidad integrada, pero esto puede manejarse fácilmente con un hook que se ejecute antes de que se llame al método Flight::start().
// app/Utils/CorsUtil.php (esqueleto: carpeta Utils en 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
{
// personaliza aquí tus hosts permitidos.
$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 / rutas — ejecutar antes de start
$app = Flight::app();
$cors = new \App\Utils\CorsUtil($app);
$app->before('start', [ $cors, 'set' ]);
Endurecimiento de la configuración de Flight
Flight expone varias configuraciones del motor que tienen implicaciones directas en la seguridad. Configurarlas correctamente es una de las formas más fáciles de endurecer tu aplicación.
flight.allow_method_override
De forma predeterminada, Flight permite que los clientes anulen el método HTTP de una solicitud usando el encabezado X-HTTP-Method-Override o un campo _method en el cuerpo de una solicitud POST. Aunque esto es útil para formularios HTML que solo pueden enviar GET/POST, puede ser peligroso si no lo esperas — un atacante podría falsificar solicitudes DELETE o PUT a través de un formulario normal.
Si tu aplicación no depende de este comportamiento (por ejemplo, estás construyendo una API consumida por clientes modernos o frontends de JavaScript que pueden enviar cualquier verbo HTTP), deberías deshabilitarlo:
// En tu index.php o archivo de bootstrap, antes de Flight::start()
Flight::set('flight.allow_method_override', false);
El valor predeterminado es true por compatibilidad hacia atrás, pero se recomienda encarecidamente establecerlo en false para cualquier aplicación que no necesite explícitamente la función de anulación.
flight.debug
Flight tiene una configuración flight.debug que controla si se muestra información detallada del error (mensaje de excepción, código y traza de pila completa) en el navegador cuando ocurre una excepción no controlada. El valor predeterminado es false, lo que significa que solo se muestra un mensaje genérico 500 Internal Server Error — no se filtran detalles internos al cliente.
Nunca lo habilites en un servidor de producción. Úsalo solo localmente o en un entorno de staging:
// Seguro solo para desarrollo local — NUNCA en producción
Flight::set('flight.debug', true);
Cuando flight.debug es false (el valor predeterminado), aún puedes capturar errores habilitando flight.log_errors:
// Registra errores en el servidor sin exponerlos al cliente
Flight::set('flight.debug', false);
Flight::set('flight.log_errors', true);
Configuración recomendada para producción
// index.php o aplicado desde la configuración de la aplicación / bootstrap
Flight::set('flight.allow_method_override', false);
Flight::set('flight.debug', false);
Flight::set('flight.log_errors', true);
Manejo de errores
Oculta los detalles sensibles de errores en producción para evitar filtrar información a los atacantes. En producción, registra los errores en lugar de mostrarlos con display_errors establecido a 0.
// En tu bootstrap.php o index.php
// agrega esto a tu app/config/config.php
$environment = ENVIRONMENT;
if ($environment === 'production') {
ini_set('display_errors', 0); // Deshabilita la visualización de errores
ini_set('log_errors', 1); // Registra los errores en su lugar
ini_set('error_log', '/path/to/error.log');
}
// En tus rutas o controladores
// Usa Flight::halt() para respuestas de error controladas
Flight::halt(403, 'Access denied');
Saneamiento de entradas
Nunca confíes en la entrada del usuario. Sanéala usando filter_var antes de procesarla para evitar que datos maliciosos se cuelen. Prefiere leer la entrada mediante $app->request() (o Flight::request()) en lugar de $_GET / $_POST sin procesar en el código de la aplicación.
// Supongamos una solicitud $_POST con $_POST['input'] y $_POST['email']
// Sanear una entrada de cadena
$clean_input = filter_var(Flight::request()->data->input, FILTER_SANITIZE_STRING);
// Sanear un correo electrónico
$clean_email = filter_var(Flight::request()->data->email, FILTER_SANITIZE_EMAIL);
Hash de contraseñas
Almacena las contraseñas de forma segura y verifícalas de manera segura usando las funciones integradas de PHP como password_hash y password_verify. Las contraseñas nunca deben almacenarse en texto plano, ni deben cifrarse con métodos reversibles. El hash asegura que incluso si tu base de datos se ve comprometida, las contraseñas reales permanezcan protegidas.
$password = Flight::request()->data->password;
// Hashea una contraseña al almacenarla (por ejemplo, durante el registro)
$hashed_password = password_hash($password, PASSWORD_DEFAULT);
// Verifica una contraseña (por ejemplo, durante el inicio de sesión)
if (password_verify($password, $stored_hash)) {
// La contraseña coincide
}
Limitación de velocidad
Protege contra ataques de fuerza bruta o ataques de denegación de servicio limitando las tasas de solicitud con una caché.
// Asumiendo que tienes flightphp/cache instalado y registrado
// Usando flightphp/cache en un filtro
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); // Restablecer después de 60 segundos
});
Ver también
- Sesiones - Cómo gestionar las sesiones de usuario de forma segura.
- Plantillas - Escape automático de Twig/Latte y XSS.
- SimplePdo - Ayudantes de base de datos con consultas preparadas.
- PdoWrapper - Obsoleto; usa SimplePdo para código nuevo.
- Middleware - Cómo usar middleware para simplificar el proceso de agregar encabezados de seguridad.
- Configuración -
.envvs configuración literal, banderas de producción. - IA y experiencia de desarrollo - Mantén la política de seguridad en
SECURITY.mdpara los agentes. - Respuestas - Cómo personalizar las respuestas HTTP con encabezados seguros.
- Solicitudes - Cómo manejar y sanear la entrada del usuario.
- filter_var - Función de PHP para el saneamiento de entradas.
- password_hash - Función de PHP para el hash seguro de contraseñas.
- password_verify - Función de PHP para verificar contraseñas con hash.
Solución de problemas
- Consulta la sección "Ver también" más arriba para obtener información sobre la solución de problemas relacionada con componentes del Framework Flight.
- Si CSP bloquea tus scripts, agrega un nonce (patrón del esqueleto) o permite orígenes específicos — no establezcas
script-src *sin un plan.
Registro de cambios
- Documentación – Esqueleto
App\Middleware, notas Twig CSRF/XSS, SimplePdo, secretos/.envySECURITY.mdpara proyectos amigables con IA. - v3.18.1 - Se agregó la sección Endurecimiento de la configuración de Flight que cubre
flight.allow_method_override,flight.debugy la validación de devolución de llamada JSONP. - v3.1.0 - Se agregaron secciones sobre CORS, Manejo de errores, Saneamiento de entradas, Hash de contraseñas y Limitación de velocidad.
- v2.0 - Se agregó escape para las vistas predeterminadas para prevenir XSS.
Learn/routing
Enrutamiento
Descripción general
El enrutamiento en Flight PHP asigna patrones de URL a funciones de devolución de llamada o métodos de clases, lo que permite un manejo de solicitudes rápido y sencillo. Está diseñado para tener una sobrecarga mínima, ser amigable para principiantes y ser extensible sin dependencias externas.
Comprendiendo el enrutamiento
El enrutamiento es el mecanismo central que conecta las solicitudes HTTP con la lógica de tu aplicación en Flight. Al definir rutas, especificas cómo diferentes URLs activan código específico, ya sea mediante funciones, métodos de clase o acciones de controladores. El sistema de enrutamiento de Flight es flexible, compatible con patrones básicos, parámetros nombrados, expresiones regulares y funciones avanzadas como inyección de dependencias y enrutamiento de recursos. Este enfoque mantiene tu código organizado y fácil de mantener, mientras sigue siendo rápido y simple para principiantes y extensible para usuarios avanzados.
Nota: ¿Quieres entender más sobre el enrutamiento? Consulta la página "¿por qué un framework?" para obtener una explicación más detallada.
Uso básico
Definiendo una ruta simple
El enrutamiento básico en Flight se realiza haciendo coincidir un patrón de URL con una función de devolución de llamada o un arreglo de una clase y un método.
Flight::route('/', function(){
echo 'hello world!';
});
Las rutas se comparan en el orden en que se definen. La primera ruta que coincida con una solicitud será invocada.
Usando funciones como devoluciones de llamada
La devolución de llamada puede ser cualquier objeto que sea invocable. Entonces puedes usar una función regular:
function hello() {
echo 'hello world!';
}
Flight::route('/', 'hello');
Usando clases y métodos como controlador
También puedes usar un método (estático o no) de una clase:
class GreetingController {
public function hello() {
echo 'hello world!';
}
}
Flight::route('/', [ 'GreetingController','hello' ]);
// o
Flight::route('/', [ GreetingController::class, 'hello' ]); // método preferido
// o
Flight::route('/', [ 'GreetingController::hello' ]);
// o
Flight::route('/', [ 'GreetingController->hello' ]);
O creando un objeto primero y luego llamando al método:
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' ]);
Nota: De forma predeterminada, cuando se llama a un controlador dentro del framework, la clase
flight\Enginesiempre se inyecta a menos que especifiques un contenedor de inyección de dependencias
Enrutamiento específico por método
De forma predeterminada, los patrones de ruta se comparan con todos los métodos de solicitud. Puedes responder a métodos específicos colocando un identificador antes de la URL.
Flight::route('GET /', function () {
echo 'I received a GET request.';
});
Flight::route('POST /', function () {
echo 'I received a POST request.';
});
// No puedes usar Flight::get() para rutas, ya que es un método
// para obtener variables, no para crear una ruta.
Flight::post('/', function() { /* código */ });
Flight::patch('/', function() { /* código */ });
Flight::put('/', function() { /* código */ });
Flight::delete('/', function() { /* código */ });
También puedes asignar múltiples métodos a una sola devolución de llamada usando un delimitador |:
Flight::route('GET|POST /', function () {
echo 'I received either a GET or a POST request.';
});
Manejo especial para solicitudes HEAD y OPTIONS
Flight proporciona manejo integrado para solicitudes HTTP HEAD y OPTIONS:
Solicitudes HEAD
- Las solicitudes HEAD se tratan igual que las solicitudes
GET, pero Flight elimina automáticamente el cuerpo de la respuesta antes de enviarlo al cliente. - Esto significa que puedes definir una ruta para
GET, y las solicitudes HEAD a la misma URL devolverán solo los encabezados (sin contenido), como se espera según los estándares HTTP.
Flight::route('GET /info', function() {
echo 'This is some info!';
});
// Una solicitud HEAD a /info devolverá los mismos encabezados, pero sin cuerpo.
Solicitudes OPTIONS
Las solicitudes OPTIONS son manejadas automáticamente por Flight para cualquier ruta definida.
- Cuando se recibe una solicitud OPTIONS, Flight responde con un estado
204 No Contenty un encabezadoAllowque enumera todos los métodos HTTP compatibles para esa ruta. - No necesitas definir una ruta separada para OPTIONS.
// Para una ruta definida como:
Flight::route('GET|POST /users', function() { /* ... */ });
// Una solicitud OPTIONS a /users responderá con:
//
// Estado: 204 No Content
// Allow: GET, POST, HEAD, OPTIONS
Usando el objeto Router
Adicionalmente, puedes obtener el objeto Router que tiene algunos métodos auxiliares para tu uso:
$router = Flight::router();
// mapea todos los métodos igual que Flight::route()
$router->map('/', function() {
echo 'hello world!';
});
// Solicitud GET
$router->get('/users', function() {
echo 'users';
});
$router->post('/users', function() { /* código */});
$router->put('/users/update/@id', function() { /* código */});
$router->delete('/users/@id', function() { /* código */});
$router->patch('/users/@id', function() { /* código */});
Expresiones regulares (Regex)
Puedes usar expresiones regulares en tus rutas:
Flight::route('/user/[0-9]+', function () {
// Esto coincidirá con /user/1234
});
Aunque este método está disponible, se recomienda usar parámetros nombrados, o parámetros nombrados con expresiones regulares, ya que son más legibles y fáciles de mantener.
Parámetros nombrados
Puedes especificar parámetros nombrados en tus rutas que se pasarán a tu función de devolución de llamada. Esto es más por la legibilidad de la ruta que cualquier otra cosa. Consulta la sección a continuación sobre la advertencia importante.
Flight::route('/@name/@id', function (string $name, string $id) {
echo "hello, $name ($id)!";
});
También puedes incluir expresiones regulares con tus parámetros nombrados usando
el delimitador ::
Flight::route('/@name/@id:[0-9]{3}', function (string $name, string $id) {
// Esto coincidirá con /bob/123
// Pero no coincidirá con /bob/12345
});
Nota: No se admite la coincidencia de grupos de expresiones regulares
()con parámetros posicionales. Ej.::'\(
Advertencia importante
Aunque en el ejemplo anterior parece que @name está directamente vinculado a la variable $name, no es así. El orden de los parámetros en la función de devolución de llamada es lo que determina qué se le pasa. Si cambiaras el orden de los parámetros en la función de devolución de llamada, las variables también se intercambiarían. Aquí tienes un ejemplo:
Flight::route('/@name/@id', function (string $id, string $name) {
echo "hello, $name ($id)!";
});
Y si fueras a la siguiente URL: /bob/123, la salida sería hello, 123 (bob)!.
Ten cuidado al configurar tus rutas y tus funciones de devolución de llamada.
Parámetros opcionales
Puedes especificar parámetros nombrados que sean opcionales para la coincidencia envolviendo los segmentos entre paréntesis.
Flight::route(
'/blog(/@year(/@month(/@day)))',
function(?string $year, ?string $month, ?string $day) {
// Esto coincidirá con las siguientes URLs:
// /blog/2012/12/10
// /blog/2012/12
// /blog/2012
// /blog
}
);
Cualquier parámetro opcional que no coincida se pasará como NULL.
Enrutamiento con comodines
La coincidencia se realiza solo en segmentos individuales de la URL. Si deseas coincidir con múltiples
segmentos, puedes usar el comodín *.
Flight::route('/blog/*', function () {
// Esto coincidirá con /blog/2000/02/01
});
Para enrutar todas las solicitudes a una sola devolución de llamada, puedes hacer:
Flight::route('*', function () {
// Hacer algo
});
Manejador de 404 No Encontrado
De forma predeterminada, si no se encuentra una URL, Flight enviará una respuesta HTTP 404 Not Found muy simple y plana.
Si deseas tener una respuesta 404 más personalizada, puedes mapear tu propio método notFound:
Flight::map('notFound', function() {
$url = Flight::request()->url;
// También podrías usar Flight::render() con una plantilla personalizada.
$output = <<<HTML
<h1>Mi 404 No Encontrado Personalizado</h1>
<h3>La página que has solicitado {$url} no se pudo encontrar.</h3>
HTML;
$this->response()
->clearBody()
->status(404)
->write($output)
->send();
});
Manejador de Método No Encontrado
De forma predeterminada, si se encuentra una URL pero el método no está permitido, Flight enviará una respuesta HTTP 405 Method Not Allowed muy simple y plana (Ej.: Method Not Allowed. Allowed Methods are: GET, POST). También incluirá un encabezado Allow con los métodos permitidos para esa URL.
Si deseas tener una respuesta 405 más personalizada, puedes mapear tu propio método methodNotFound:
use flight\net\Route;
Flight::map('methodNotFound', function(Route $route) {
$url = Flight::request()->url;
$methods = implode(', ', $route->methods);
// También podrías usar Flight::render() con una plantilla personalizada.
$output = <<<HTML
<h1>Mi 405 Método No Permitido Personalizado</h1>
<h3>El método que has solicitado para {$url} no está permitido.</h3>
<p>Los Métodos Permitidos son: {$methods}</p>
HTML;
$this->response()
->clearBody()
->status(405)
->setHeader('Allow', $methods)
->write($output)
->send();
});
Uso avanzado
Inyección de dependencias en rutas
Si deseas usar inyección de dependencias mediante un contenedor (PSR-11, PHP-DI, Dice, etc.), el único tipo de rutas donde esto está disponible es creando directamente el objeto tú mismo y usando el contenedor para crear tu objeto, o puedes usar cadenas para definir la clase y el método a llamar. Puedes ir a la página de Inyección de Dependencias para obtener más información.
Aquí tienes un ejemplo rápido:
use flight\database\SimplePdo;
// Greeting.php
class Greeting
{
protected SimplePdo $db;
public function __construct(SimplePdo $db) {
$this->db = $db;
}
public function hello(int $id) {
// hacer algo con $this->db
$name = $this->db->fetchField("SELECT name FROM users WHERE id = ?", [ $id ]);
echo "Hello, world! My name is {$name}!";
}
}
// index.php
// Configura el contenedor con los parámetros que necesites
// Consulta la página de Inyección de Dependencias para más información sobre PSR-11
$dice = new \Dice\Dice();
// ¡No olvides reasignar la variable con '$dice = '!!!!!
$dice = $dice->addRule(SimplePdo::class, [
'shared' => true,
'constructParams' => [
'mysql:host=localhost;dbname=test',
'root',
'password'
]
]);
// Registra el manejador del contenedor
Flight::registerContainerHandler(function($class, $params) use ($dice) {
return $dice->create($class, $params);
});
// Rutas como siempre
Flight::route('/hello/@id', [ 'Greeting', 'hello' ]);
// o
Flight::route('/hello/@id', 'Greeting->hello');
// o
Flight::route('/hello/@id', 'Greeting::hello');
Flight::start();
Pasar la ejecución a la siguiente ruta
Obsoleto
Puedes pasar la ejecución a la siguiente ruta que coincida devolviendo true desde
tu función de devolución de llamada.
Flight::route('/user/@name', function (string $name) {
// Verificar alguna condición
if ($name !== "Bob") {
// Continuar a la siguiente ruta
return true;
}
});
Flight::route('/user/*', function () {
// Esto se llamará
});
Ahora se recomienda usar middleware para manejar casos de uso complejos como este.
Alias de rutas
Al asignar un alias a una ruta, puedes llamar a ese alias más tarde en tu aplicación de forma dinámica para que se genere posteriormente en tu código (ej.: un enlace en una plantilla HTML, o generar una URL de redirección).
Flight::route('/users/@id', function($id) { echo 'user:'.$id; }, false, 'user_view');
// o
Flight::route('/users/@id', function($id) { echo 'user:'.$id; })->setAlias('user_view');
// más adelante en el código, en algún lugar
class UserController {
public function update() {
// código para guardar usuario...
$id = $user['id']; // 5 por ejemplo
$redirectUrl = Flight::getUrl('user_view', [ 'id' => $id ]); // devolverá '/users/5'
Flight::redirect($redirectUrl);
}
}
Esto es especialmente útil si tu URL llega a cambiar. En el ejemplo anterior, digamos que los usuarios se movieron a /admin/users/@id en su lugar.
Con el alias establecido para la ruta, ya no necesitas buscar todas las URLs antiguas en tu código y cambiarlas porque el alias ahora devolverá /admin/users/5 como en el ejemplo anterior.
El alias de rutas también funciona en grupos:
Flight::group('/users', function() {
Flight::route('/@id', function($id) { echo 'user:'.$id; }, false, 'user_view');
// o
Flight::route('/@id', function($id) { echo 'user:'.$id; })->setAlias('user_view');
});
Inspeccionando información de la ruta
Si deseas inspeccionar la información de la ruta coincidente, hay 2 formas de hacerlo:
- Puedes usar la propiedad
executedRouteen el objetoFlight::router(). - Puedes solicitar que el objeto de ruta se pase a tu devolución de llamada pasando
truecomo tercer parámetro en el método de ruta. El objeto de ruta siempre será el último parámetro pasado a tu función de devolución de llamada.
executedRoute
Flight::route('/', function() {
$route = Flight::router()->executedRoute;
// Hacer algo con $route
// Arreglo de métodos HTTP comparados
$route->methods;
// Arreglo de parámetros nombrados
$route->params;
// Expresión regular coincidente
$route->regex;
// Contiene el contenido de cualquier '*' usado en el patrón de URL
$route->splat;
// Muestra la ruta de la URL... si realmente lo necesitas
$route->pattern;
// Muestra qué middleware está asignado a esta
$route->middleware;
// Muestra el alias asignado a esta ruta
$route->alias;
});
Nota: La propiedad
executedRoutesolo se establecerá después de que se haya ejecutado una ruta. Si intentas acceder a ella antes de que se haya ejecutado una ruta, seráNULL. ¡También puedes usar executedRoute en middleware!
Pasar true en la definición de la ruta
Flight::route('/', function(\flight\net\Route $route) {
// Arreglo de métodos HTTP comparados
$route->methods;
// Arreglo de parámetros nombrados
$route->params;
// Expresión regular coincidente
$route->regex;
// Contiene el contenido de cualquier '*' usado en el patrón de URL
$route->splat;
// Muestra la ruta de la URL... si realmente lo necesitas
$route->pattern;
// Muestra qué middleware está asignado a esta
$route->middleware;
// Muestra el alias asignado a esta ruta
$route->alias;
}, true);// <-- Este parámetro true es lo que hace que eso suceda
Agrupación de rutas y middleware
Puede haber ocasiones en las que desees agrupar rutas relacionadas (como /api/v1).
Puedes hacer esto usando el método group:
Flight::group('/api/v1', function () {
Flight::route('/users', function () {
// Coincide con /api/v1/users
});
Flight::route('/posts', function () {
// Coincide con /api/v1/posts
});
});
Incluso puedes anidar grupos de grupos:
Flight::group('/api', function () {
Flight::group('/v1', function () {
// Flight::get() obtiene variables, no establece una ruta. Consulta el contexto del objeto a continuación.
Flight::route('GET /users', function () {
// Coincide con GET /api/v1/users
});
Flight::post('/posts', function () {
// Coincide con POST /api/v1/posts
});
Flight::put('/posts/1', function () {
// Coincide con PUT /api/v1/posts
});
});
Flight::group('/v2', function () {
// Flight::get() obtiene variables, no establece una ruta. Consulta el contexto del objeto a continuación.
Flight::route('GET /users', function () {
// Coincide con GET /api/v2/users
});
});
});
Agrupación con contexto de objeto
Aún puedes usar la agrupación de rutas con el objeto Engine de la siguiente manera:
$app = Flight::app();
$app->group('/api/v1', function (Router $router) {
// usa la variable $router
$router->get('/users', function () {
// Coincide con GET /api/v1/users
});
$router->post('/posts', function () {
// Coincide con POST /api/v1/posts
});
});
Nota: Este es el método preferido para definir rutas y grupos con el objeto
$router.
Agrupación con middleware
También puedes asignar middleware a un grupo de rutas:
Flight::group('/api/v1', function () {
Flight::route('/users', function () {
// Coincide con /api/v1/users
});
}, [ MyAuthMiddleware::class ]); // o [ new MyAuthMiddleware() ] si deseas usar una instancia
Consulta más detalles en la página de middleware de grupo.
Enrutamiento de recursos
Puedes crear un conjunto de rutas para un recurso usando el método resource. Esto creará
un conjunto de rutas para un recurso que sigue las convenciones RESTful.
Para crear un recurso, haz lo siguiente:
Flight::resource('/users', UsersController::class);
Y lo que sucederá en segundo plano es que creará las siguientes rutas:
[
'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'
]
Y tu controlador usará los siguientes métodos:
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
{
}
}
Nota: Puedes ver las rutas recién agregadas con
runwayejecutandophp runway routes.
Personalizando rutas de recursos
Hay algunas opciones para configurar las rutas de recursos.
Base de alias
Puedes configurar aliasBase. De forma predeterminada, el alias es la última parte de la URL especificada.
Por ejemplo, /users/ resultaría en un aliasBase de users. Cuando se crean estas rutas,
los alias son users.index, users.create, etc. Si deseas cambiar el alias, establece aliasBase
al valor que desees.
Flight::resource('/users', UsersController::class, [ 'aliasBase' => 'user' ]);
Only y Except
También puedes especificar qué rutas deseas crear usando las opciones only y except.
// Permitir solo estos métodos y bloquear el resto
Flight::resource('/users', UsersController::class, [ 'only' => [ 'index', 'show' ] ]);
// Bloquear solo estos métodos y permitir el resto
Flight::resource('/users', UsersController::class, [ 'except' => [ 'create', 'store', 'edit', 'update', 'destroy' ] ]);
Básicamente, estas son opciones de lista blanca y lista negra para que puedas especificar qué rutas deseas crear.
Middleware
También puedes especificar middleware que se ejecute en cada una de las rutas creadas por el método resource.
Flight::resource('/users', UsersController::class, [ 'middleware' => [ MyAuthMiddleware::class ] ]);
Respuestas de transmisión (Streaming)
Ahora puedes transmitir respuestas al cliente usando stream() o streamWithHeaders().
Esto es útil para enviar archivos grandes, procesos de larga duración o generar respuestas grandes.
Transmitir una ruta se maneja de manera un poco diferente a una ruta regular.
Nota: Las respuestas de transmisión solo están disponibles si has establecido
flight.v2.output_bufferingenfalse.
Transmitir con encabezados manuales
Puedes transmitir una respuesta al cliente usando el método stream() en una ruta. Si
haces esto, debes establecer todos los encabezados manualmente antes de generar cualquier salida al cliente.
Esto se hace con la función header() de PHP o con el método Flight::response()->setRealHeader().
Flight::route('/@filename', function($filename) {
$response = Flight::response();
// obviamente sanitizarías la ruta y todo eso.
$fileNameSafe = basename($filename);
// Si tienes encabezados adicionales que establecer aquí después de que la ruta se haya ejecutado,
// debes definirlos antes de que se imprima cualquier cosa.
// Todos deben ser una llamada directa a la función header()
// o una llamada a Flight::response()->setRealHeader()
header('Content-Disposition: attachment; filename="'.$fileNameSafe.'"');
// o
$response->setRealHeader('Content-Disposition: attachment; filename="'.$fileNameSafe.'"');
$filePath = '/some/path/to/files/'.$fileNameSafe;
if (!is_readable($filePath)) {
Flight::halt(404, 'File not found');
}
// establece manualmente la longitud del contenido si lo deseas
header('Content-Length: '.filesize($filePath));
// o
$response->setRealHeader('Content-Length: '.filesize($filePath));
// Transmite el archivo al cliente mientras se lee
readfile($filePath);
// Esta es la línea mágica aquí
})->stream();
Transmitir con encabezados
También puedes usar el método streamWithHeaders() para establecer los encabezados antes de comenzar a transmitir.
Flight::route('/stream-users', function() {
// puedes agregar cualquier encabezado adicional aquí
// solo debes usar header() o Flight::response()->setRealHeader()
// sin importar cómo obtengas tus datos, solo como ejemplo...
$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 ',';
}
// Esto es necesario para enviar los datos al cliente
ob_flush();
}
echo '}';
// Así es como estableces los encabezados antes de comenzar a transmitir.
})->streamWithHeaders([
'Content-Type' => 'application/json',
'Content-Disposition' => 'attachment; filename="users.json"',
// código de estado opcional, el valor predeterminado es 200
'status' => 200
]);
Ver también
- Middleware - Uso de middleware con rutas para autenticación, registro, etc.
- Inyección de Dependencias - Simplificando la creación y gestión de objetos en rutas.
- ¿Por qué un Framework? - Comprendiendo los beneficios de usar un framework como Flight.
- Extensión - Cómo extender Flight con tu propia funcionalidad, incluido el método
notFound. - php.net: preg_match - Función de PHP para coincidencia de expresiones regulares.
Solución de problemas
- Los parámetros de ruta se comparan por orden, no por nombre. Asegúrate de que el orden de los parámetros en la devolución de llamada coincida con la definición de la ruta.
- Usar
Flight::get()no define una ruta; usaFlight::route('GET /...')para enrutar o el contexto del objeto Router en grupos (ej.$router->get(...)). - La propiedad executedRoute solo se establece después de que una ruta se ejecuta; es NULL antes de la ejecución.
- La transmisión requiere que la funcionalidad de almacenamiento en búfer de salida heredada de Flight esté deshabilitada (
flight.v2.output_buffering = false). - Para la inyección de dependencias, solo ciertas definiciones de rutas admiten la instanciación basada en contenedor.
404 No Encontrado o Comportamiento Inesperado de Ruta
Si estás viendo un error 404 No Encontrado (pero juras por tu vida que realmente está ahí y no es un error tipográfico), esto en realidad podría ser un problema con que estés devolviendo un valor en tu endpoint de ruta en lugar de solo imprimirlo. La razón de esto es intencional, pero podría sorprender a algunos desarrolladores.
Flight::route('/hello', function(){
// Esto podría causar un error 404 No Encontrado
return 'Hello World';
});
// Lo que probablemente quieras
Flight::route('/hello', function(){
echo 'Hello World';
});
La razón de esto se debe a un mecanismo especial integrado en el enrutador que maneja la salida devuelta como una señal para "ir a la siguiente ruta". Puedes ver el comportamiento documentado en la sección Enrutamiento.
Registro de cambios
- v3: Se agregó enrutamiento de recursos, alias de rutas y soporte de transmisión, grupos de rutas y soporte de middleware.
- v1: La gran mayoría de las características básicas están disponibles.
Learn/learn
Aprende sobre Flight
Flight es un framework para PHP rápido, simple y extensible. Es bastante versátil y puede usarse para construir cualquier tipo de aplicación web. Está creado pensando en la simplicidad y está escrito de una manera fácil de entender y usar, tanto por humanos como por asistentes de codificación con IA.
Nota: Verás ejemplos que usan
Flight::como variable estática y otros que usan el objeto Engine$app->. Ambos funcionan de manera intercambiable.$appy$this->appen un controlador/middleware es el enfoque recomendado por el equipo de Flight (y lo que el esqueleto oficial yAGENTS.mdestandarizan para proyectos nuevos).
Componentes principales
Enrutamiento
Aprende cómo gestionar las rutas de tu aplicación web. Esto también incluye agrupación de rutas, parámetros de ruta y middleware.
Middleware
Aprende cómo usar middleware para filtrar solicitudes y respuestas en tu aplicación.
Autocarga
Aprende cómo autocargar tus propias clases. La capitalización de las carpetas debe coincidir con tus espacios de nombres; el esqueleto usa App\ y carpetas en PascalCase como app/Controller/.
Peticiones
Aprende cómo manejar solicitudes y respuestas en tu aplicación.
Respuestas
Aprende cómo enviar respuestas a tus usuarios.
Plantillas HTML
Aprende cómo renderizar HTML con Twig (predeterminado del esqueleto), Latte u otros motores, no solo las vistas PHP integradas.
Seguridad
Aprende cómo proteger tu aplicación de amenazas de seguridad comunes.
Configuración
Aprende cómo configurar el framework para tu aplicación.
Administrador de eventos
Aprende cómo usar el sistema de eventos para agregar eventos personalizados a tu aplicación.
Extender Flight
Aprende cómo extender el framework agregando tus propios métodos y clases.
Hooks de métodos y filtrado
Aprende cómo agregar hooks de eventos a tus métodos y a los métodos internos del framework.
Contenedor de inyección de dependencias (DIC)
Aprende cómo usar contenedores de inyección de dependencias (DIC) para gestionar las dependencias de tu aplicación.
Clases de utilidad
Collections
Las colecciones se usan para almacenar datos y permitir acceder a ellos como un array o como un objeto para facilitar su uso.
JSON Wrapper
Tiene algunas funciones simples para que la codificación y decodificación de tu JSON sea consistente.
SimplePdo
PDO a veces puede causar más dolores de cabeza de lo necesario. SimplePdo es una clase auxiliar moderna de PDO con métodos convenientes como insert(), update(), delete() y transaction() para facilitar las operaciones de base de datos.
PdoWrapper (Obsoleto)
El wrapper original de PDO está obsoleto a partir de la versión v3.18.0. Por favor, usa SimplePdo en su lugar.
Manejador de archivos subidos
Una clase simple para ayudar a gestionar archivos subidos y moverlos a una ubicación permanente.
Conceptos importantes
¿Por qué un framework?
Aquí hay un artículo corto sobre por qué deberías usar un framework. Es buena idea entender los beneficios de usar un framework antes de empezar a usar uno.
Además, un excelente tutorial ha sido creado por @lubiana. Aunque no entra en gran detalle sobre Flight específicamente, esta guía te ayudará a entender algunos de los conceptos principales que rodean a un framework y por qué son beneficiosos de usar. Puedes encontrar el tutorial aquí.
Flight comparado con otros frameworks
Si estás migrando desde otro framework como Laravel, Slim, Fat-Free o Symfony a Flight, esta página te ayudará a entender las diferencias entre ambos.
Otros temas
Pruebas unitarias
Sigue esta guía para aprender cómo probar unitariamente tu código de Flight para que sea sólido como una roca.
IA y experiencia de desarrollo
Flight está diseñado para combinarse con LLMs de codificación: AGENTS.md, comandos ai:* de Runway y un diseño de esqueleto claro para que los agentes mantengan el patrón.
Migración de v2 a v3
La compatibilidad hacia atrás se ha mantenido en su mayor parte, pero hay algunos cambios que debes tener en cuenta al migrar de v2 a v3.
Learn/unit_testing
Pruebas Unitarias
Resumen
Las pruebas unitarias en Flight te ayudan a garantizar que tu aplicación se comporte como se espera, detectar errores a tiempo y hacer que tu base de código sea más fácil de mantener. Flight está diseñado para funcionar sin problemas con PHPUnit, el framework de pruebas más popular de PHP.
Comprensión
Las pruebas unitarias verifican el comportamiento de pequeñas partes de tu aplicación (como controladores o servicios) de forma aislada. En Flight, esto significa probar cómo tus rutas, controladores y lógica responden a diferentes entradas, sin depender del estado global o de servicios externos reales.
Principios clave:
- Prueba el comportamiento, no la implementación: Céntrate en lo que hace tu código, no en cómo lo hace.
- Evita el estado global: Usa inyección de dependencias en lugar de
Flight::set()oFlight::get(). - Simula servicios externos: Reemplaza cosas como bases de datos o servicios de correo con dobles de prueba.
- Mantén las pruebas rápidas y enfocadas: Las pruebas unitarias no deben acceder a bases de datos o APIs reales.
Uso Básico
Configuración de PHPUnit
- Instala PHPUnit con Composer:
composer require --dev phpunit/phpunit - Crea un directorio
testsen la raíz de tu proyecto. - Agrega un script de prueba a tu
composer.json:"scripts": { "test": "phpunit --configuration phpunit.xml" } - Crea un archivo
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>
Ahora puedes ejecutar tus pruebas con composer test.
Probando un Manejador de Ruta Simple
Supón que tienes una ruta que valida un correo electrónico:
// 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']);
}
}
Una prueba simple para este controlador:
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']);
}
}
Consejos:
- Simula datos POST usando
$app->request()->data. - Evita usar estáticos de
Flight::en tus pruebas: usa la instancia de$app.
Usando Inyección de Dependencias para Controladores Comprobables
Inyecta dependencias (como la base de datos o el servicio de correo) en tus controladores para que sean fáciles de simular en las pruebas:
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']);
}
}
Y una prueba con simulacros (mocks):
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);
}
}
Uso Avanzado
- Simulación (Mocks): Usa los simulacros integrados de PHPUnit o clases anónimas para reemplazar dependencias.
- Prueba de controladores directamente: Instancia controladores con un nuevo
Enginey simula las dependencias. - Evita simular en exceso: Deja que la lógica real se ejecute cuando sea posible; solo simula servicios externos.
Ver También
- Guía de Pruebas Unitarias - Una guía completa sobre las mejores prácticas de pruebas unitarias.
- Contenedor de Inyección de Dependencias - Cómo usar DICs para gestionar dependencias y mejorar la comprobabilidad.
- Extensión - Cómo agregar tus propios ayudantes o sobrescribir clases principales.
- SimplePdo - Simplifica las interacciones con la base de datos y es más fácil de simular en pruebas.
- Solicitudes - Manejo de solicitudes HTTP en Flight.
- Respuestas - Envío de respuestas a los usuarios.
- Pruebas Unitarias y Principios SOLID - Aprende cómo los principios SOLID pueden mejorar tus pruebas unitarias.
Solución de Problemas
- Evita usar estado global (
Flight::set(),$_SESSION, etc.) en tu código y en tus pruebas. - Si tus pruebas son lentas, es posible que estés escribiendo pruebas de integración: simula servicios externos para mantener las pruebas unitarias rápidas.
- Si la configuración de las pruebas es compleja, considera refactorizar tu código para usar inyección de dependencias.
Registro de Cambios
- v3.15.0 - Se agregaron ejemplos de inyección de dependencias y simulacros (mocks).
Learn/flight_vs_symfony
Vuelo vs Symfony
¿Qué es Symfony?
Symfony es un conjunto de componentes reutilizables de PHP y un framework de PHP para proyectos web.
El fundamento estándar sobre el cual se construyen las mejores aplicaciones PHP. Elija cualquiera de los 50 componentes independientes disponibles para sus propias aplicaciones.
Acelere la creación y el mantenimiento de sus aplicaciones web de PHP. Finalice las tareas de codificación repetitivas y disfrute del poder de controlar su código.
Pros en comparación con Vuelo
- Symfony tiene un enorme ecosistema de desarrolladores y módulos que se pueden utilizar para resolver problemas comunes.
- Symfony tiene un ORM completo (Doctrine) que se puede utilizar para interactuar con su base de datos.
- Symfony tiene una gran cantidad de documentación y tutoriales que se pueden utilizar para aprender el framework.
- Symfony tiene podcasts, conferencias, reuniones, videos y otros recursos que se pueden utilizar para aprender el framework.
- Symfony está orientado hacia un desarrollador experimentado que busca construir una aplicación web empresarial con todas las funciones.
Contras en comparación con Vuelo
- Symfony tiene mucho más en marcha bajo el capó que Vuelo. Esto conlleva un costo dramático en términos de rendimiento. Consulte los benchmarks de TechEmpower para obtener más información.
- Vuelo está orientado hacia un desarrollador que busca construir una aplicación web ligera, rápida y fácil de usar.
- Vuelo está orientado hacia la simplicidad y facilidad de uso.
- Una de las características principales de Vuelo es que hace todo lo posible para mantener la compatibilidad hacia atrás.
- Vuelo no tiene dependencias, mientras que Symfony tiene una serie de dependencias
- Vuelo está destinado a desarrolladores que se aventuran en el mundo de los frameworks por primera vez.
- Vuelo también puede realizar aplicaciones a nivel empresarial, pero no tiene tantos ejemplos y tutoriales como Symfony. También requerirá más disciplina por parte del desarrollador para mantener las cosas organizadas y bien estructuradas.
- Vuelo le da al desarrollador más control sobre la aplicación, mientras que Symfony puede introducir algo de magia entre bastidores.
Learn/flight_vs_another_framework
Comparación de Flight con Otro Framework
Si estás migrando de otro framework como Laravel, Slim, Fat-Free o Symfony a Flight, esta página te ayudará a entender las diferencias entre los dos.
Laravel
Laravel es un framework completo que tiene todas las funciones y una increíble comunidad enfocada en el desarrollador, pero a un costo en rendimiento y complejidad.
Ver la comparación entre Laravel y Flight.
Slim
Slim es un micro-framework similar a Flight. Está diseñado para ser ligero y fácil de usar, pero puede ser un poco más complejo que Flight.
Ver la comparación entre Slim y Flight.
Fat-Free
Fat-Free es un framework full-stack en un paquete mucho más pequeño. Aunque tiene todas las herramientas necesarias, tiene una arquitectura de datos que puede hacer que algunos proyectos sean más complejos de lo necesario.
Ver la comparación entre Fat-Free y Flight.
Symfony
Symfony es un framework modular a nivel empresarial que está diseñado para ser flexible y escalable. Para proyectos más pequeños o desarrolladores nuevos, Symfony puede resultar un poco abrumador.
Ver la comparación entre Symfony y Flight.
Learn/pdo_wrapper
Clase Ayudante PDO PdoWrapper
ADVERTENCIA
Obsoleto:
PdoWrapperestá obsoleto desde Flight v3.18.0. No se eliminará en una versión futura, pero se mantendrá para compatibilidad hacia atrás. Por favor, use SimplePdo en su lugar, que ofrece la misma funcionalidad más métodos ayudantes adicionales para operaciones comunes de base de datos.
Resumen
La clase PdoWrapper en Flight es un ayudante amigable para trabajar con bases de datos usando PDO. Simplifica tareas comunes de base de datos, agrega algunos métodos útiles para obtener resultados y devuelve los resultados como Collections para un acceso fácil. También soporta registro de consultas y monitoreo de rendimiento de la aplicación (APM) para casos de uso avanzados.
Comprensión
Trabajar con bases de datos en PHP puede ser un poco verboso, especialmente cuando se usa PDO directamente. PdoWrapper extiende PDO y agrega métodos que hacen que consultar, obtener y manejar resultados sea mucho más fácil. En lugar de manejar declaraciones preparadas y modos de obtención, obtienes métodos simples para tareas comunes, y cada fila se devuelve como una Collection, por lo que puedes usar notación de array u objeto.
Puedes registrar PdoWrapper como un servicio compartido en Flight, y luego usarlo en cualquier lugar de tu app a través de Flight::db().
Uso Básico
Registrando el Ayudante PDO
Primero, registra la clase PdoWrapper con 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
]
]);
Ahora puedes usar Flight::db() en cualquier lugar para obtener tu conexión a la base de datos.
Ejecutando Consultas
runQuery()
function runQuery(string $sql, array $params = []): PDOStatement
Usa esto para INSERTs, UPDATEs, o cuando quieras obtener resultados manualmente:
$db = Flight::db();
$statement = $db->runQuery("SELECT * FROM users WHERE status = ?", ['active']);
while ($row = $statement->fetch()) {
// $row is an array
}
También puedes usarlo para escrituras:
$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
Obtén un solo valor de la base de datos:
$count = Flight::db()->fetchField("SELECT COUNT(*) FROM users WHERE status = ?", ['active']);
fetchRow()
function fetchRow(string $sql, array $params = []): Collection
Obtén una sola fila como una Collection (acceso array/objeto):
$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>
Obtén todas las filas como un array de Collections:
$users = Flight::db()->fetchAll("SELECT * FROM users WHERE status = ?", ['active']);
foreach ($users as $user) {
echo $user['name'];
// or
echo $user->name;
}
Usando Marcadores de Posición IN()
Puedes usar un solo ? en una cláusula IN() y pasar un array o una cadena separada por comas:
$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']);
Uso Avanzado
Registro de Consultas & APM
Si quieres rastrear el rendimiento de las consultas, habilita el seguimiento APM al registrar:
Flight::register('db', \flight\database\PdoWrapper::class, [
'mysql:host=localhost;dbname=cool_db_name', 'user', 'pass', [/* options */], true // last param enables APM
]);
Después de ejecutar consultas, puedes registrarlas manualmente pero el APM las registrará automáticamente si está habilitado:
Flight::db()->logQueries();
Esto activará un evento (flight.db.queries) con métricas de conexión y consulta, que puedes escuchar usando el sistema de eventos de Flight.
Ejemplo Completo
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();
});
Ver También
- Collections - Aprende cómo usar la clase Collection para un acceso fácil a los datos.
Solución de Problemas
- Si obtienes un error sobre la conexión a la base de datos, verifica tu DSN, nombre de usuario, contraseña y opciones.
- Todas las filas se devuelven como Collections—si necesitas un array plano, usa
$collection->getData(). - Para consultas
IN (?), asegúrate de pasar un array o una cadena separada por comas.
Registro de Cambios
- v3.2.0 - Lanzamiento inicial de PdoWrapper con métodos básicos de consulta y obtención.
Learn/dependency_injection_container
Contenedor de Inyección de Dependencias
Descripción General
El Contenedor de Inyección de Dependencias (DIC, por sus siglas en inglés) es una mejora potente que te permite gestionar las dependencias de tu aplicación. También es una de las mayores razones por las que Flight se lleva bien con herramientas de IA para codificación y pruebas unitarias: los controladores reciben lo que necesitan en el constructor en lugar de acceder a variables globales.
Entendiendo
La Inyección de Dependencias (DI) es un concepto clave en los frameworks PHP modernos y se utiliza para gestionar la instanciación y configuración de objetos. Algunos ejemplos de bibliotecas DIC son: flightphp/container, Dice, Pimple, PHP-DI y league/container.
Un DIC es una forma elegante de crear y gestionar tus clases en una ubicación centralizada. Esto es útil cuando necesitas pasar el mismo objeto a múltiples clases (controladores, middleware, comandos, etc.).
El flightphp/skeleton oficial conecta Dice en app/config/services.php, sustituye la instancia compartida de flight\Engine y resuelve destinos de rutas como [App\Controller\HomeController::class, 'index']. Prefiere ese patrón para proyectos nuevos para que humanos y agentes editen los mismos lugares.
Uso Básico
La forma antigua de hacer las cosas podría verse así:
require 'vendor/autoload.php';
// clase para gestionar usuarios desde la base de datos
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());
}
}
// en tu archivo routes.php
$db = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');
$UserController = new UserController($db);
Flight::route('/user/@id', [ $UserController, 'view' ]);
// otras rutas de UserController...
Flight::start();
Puedes ver en el código anterior que estamos creando un nuevo objeto PDO y pasándolo
a nuestra clase UserController. Esto está bien para una aplicación pequeña, pero a medida que tu
aplicación crezca, verás que estás creando o pasando el mismo objeto PDO
en múltiples lugares. Aquí es donde un DIC resulta útil.
Aquí está el mismo ejemplo usando un DIC (usando Dice):
require 'vendor/autoload.php';
// misma clase que arriba. Nada ha cambiado
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());
}
}
// crea un nuevo contenedor
$container = new \Dice\Dice;
// agrega una regla para indicar al contenedor cómo crear un objeto PDO
// ¡no olvides reasignarlo a sí mismo como abajo!
$container = $container->addRule('PDO', [
// shared significa que se devolverá el mismo objeto cada vez
'shared' => true,
'constructParams' => ['mysql:host=localhost;dbname=test', 'user', 'pass' ]
]);
// Esto registra el manejador del contenedor para que Flight sepa usarlo.
Flight::registerContainerHandler(function($class, $params) use ($container) {
return $container->create($class, $params);
});
// ahora podemos usar el contenedor para crear nuestro UserController
Flight::route('/user/@id', [ UserController::class, 'view' ]);
Flight::start();
Apuesto a que podrías estar pensando que se agregó mucho código extra al ejemplo.
La magia viene cuando tienes otro controlador que necesita el objeto PDO.
// Si todos tus controladores tienen un constructor que necesita un objeto PDO
// ¡cada una de las rutas siguientes lo tendrá inyectado automáticamente!
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' ]);
El beneficio adicional de utilizar un DIC es que las pruebas unitarias se vuelven mucho más fáciles. Puedes crear un objeto mock y pasarlo a tu clase. Este es un gran beneficio cuando estás escribiendo pruebas para tu aplicación—y cuando un asistente de IA genera un controlador, la inyección por constructor le brinda un patrón claro y consistente a seguir (guía de pruebas unitarias).
Creando un manejador DIC centralizado
Puedes crear un manejador DIC centralizado en tu archivo de servicios extendiendo tu aplicación. Aquí tienes un ejemplo:
// services.php
// crea un nuevo contenedor
$container = new \Dice\Dice;
// ¡no olvides reasignarlo a sí mismo como abajo!
$container = $container->addRule('PDO', [
// shared significa que se devolverá el mismo objeto cada vez
'shared' => true,
'constructParams' => ['mysql:host=localhost;dbname=test', 'user', 'pass' ]
]);
// ahora podemos crear un método mapeable para crear cualquier objeto.
Flight::map('make', function($class, $params = []) use ($container) {
return $container->create($class, $params);
});
// Esto registra el manejador del contenedor para que Flight sepa usarlo para controladores/middleware
Flight::registerContainerHandler(function($class, $params) {
return Flight::make($class, $params);
});
// digamos que tenemos la siguiente clase de ejemplo que recibe un objeto PDO en el constructor
class EmailCron {
protected PDO $pdo;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
public function send() {
// código que envía un correo electrónico
}
}
// Y finalmente puedes crear objetos usando inyección de dependencias
$emailCron = Flight::make(EmailCron::class);
$emailCron->send();
flightphp/container
Flight tiene un plugin que proporciona un contenedor simple compatible con PSR-11 que puedes usar para manejar tu inyección de dependencias. Aquí tienes un ejemplo rápido de cómo usarlo:
// index.php por ejemplo
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);
// ¡esto se mostrará correctamente!
}
}
Flight::route('GET /', [TestController::class, 'index']);
Flight::start();
Uso Avanzado de flightphp/container
También puedes resolver dependencias de forma recursiva. Aquí tienes un ejemplo:
<?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 {
// Implementación ...
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
También puedes crear tu propio manejador DIC. Esto es útil si tienes un contenedor personalizado que quieras usar y que no sea PSR-11 (Dice). Consulta la sección uso básico para saber cómo hacerlo.
Además, hay algunos valores predeterminados útiles que harán tu vida más fácil al usar Flight.
Instancia de Engine (requerida para la inyección de $app)
Si escribes la indicación de tipo flight\Engine en controladores o middleware, Dice no debe construir un nuevo Engine. Sustituye la misma instancia del bootstrap. Esto es lo que hace el skeleton oficial, y es el patrón que AGENTS.md espera para controladores generados por IA:
// En algún lugar de tu bootstrap / services.php
use flight\Engine;
use flight\database\SimplePdo;
$app = Flight::app(); // o $engine = Flight::app();
$container = new \Dice\Dice;
$container = $container->addRule('*', [
'substitutions' => [
// Crítico: reutiliza el Engine de bootstrap—no dejes que Dice haga `new Engine()`
Engine::class => $app,
// Prefiere SimplePdo para código nuevo
// SimplePdo::class => $db,
// Config::class => $config,
// \Twig\Environment::class => $twig,
]
]);
$app->registerContainerHandler(function ($class, $params) use ($container) {
return $container->create($class, $params);
});
// Helper opcional para código fuera de rutas
$app->map('make', function ($class, $params = []) use ($container) {
return $container->create($class, $params);
});
// app/Controller/MyController.php (estructura del skeleton—la carpeta coincide con el namespace)
namespace App\Controller;
use flight\Engine;
class MyController
{
protected Engine $app;
public function __construct(Engine $app)
{
$this->app = $app;
}
public function index(): void
{
// Sin fachada Flight:: en la capa de aplicación—más fácil de probar y más claro para herramientas de IA
$this->app->render('welcome', ['message' => 'Hello']);
}
}
// app/config/routes.php
use App\Controller\MyController;
$router->get('/', [MyController::class, 'index']);
Si omites la sustitución de Engine, Dice puede construir un segundo Engine y tu controlador no compartirá rutas, configuración ni el render de Twig mapeado desde el bootstrap.
Agregando otros servicios compartidos (SimplePdo, Config, Twig)
use flight\database\SimplePdo;
use flight\Engine;
// Después de crear $db, $config, $twig en services.php:
$substitutions = [
Engine::class => $app,
SimplePdo::class => $db,
// App\Utils\Config::class => $config,
// \Twig\Environment::class => $twig,
];
$container = $container->addRule('*', [
'substitutions' => $substitutions,
]);
Luego los controladores pueden recibir SimplePdo $db (o tu tipo de configuración) en el constructor y nunca llamar a Flight::db(). Eso coincide con la guía de pruebas unitarias y el estilo del skeleton.
Agregando otras clases
Si tienes otras clases que quieras agregar al contenedor, con Dice es fácil ya que serán resueltas automáticamente por el contenedor. Aquí tienes un ejemplo:
$container = new \Dice\Dice;
// Si no necesitas inyectar dependencias en tus clases
// ¡no necesitas definir nada!
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 también puede usar cualquier contenedor compatible con PSR-11. Esto significa que puedes usar cualquier contenedor que implemente la interfaz PSR-11. Aquí tienes un ejemplo usando el contenedor PSR-11 de League:
require 'vendor/autoload.php';
use flight\database\SimplePdo;
// misma idea de UserController que arriba, indicando SimplePdo en lugar de PDO crudo
$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();
Esto puede ser un poco más verboso que el ejemplo anterior con Dice, pero de todos modos hace el trabajo con los mismos beneficios.
Véase También
- Instalación - Estructura del skeleton y dónde se encuentra
services.php. - Autocarga - Namespaces
App\y mayúsculas/minúsculas de las carpetas. - Extendiendo Flight - Aprende cómo puedes agregar inyección de dependencias a tus propias clases extendiendo el framework.
- Configuración - Aprende cómo configurar Flight para tu aplicación.
- Enrutamiento - Aprende cómo definir rutas para tu aplicación y cómo funciona la inyección de dependencias con los controladores.
- Middleware - Aprende cómo crear middleware para tu aplicación y cómo funciona la inyección de dependencias con el middleware.
- Pruebas Unitarias - Por qué la inyección por constructor supera a las variables globales de
Flight::. - IA y Experiencia de Desarrollo - Un patrón de DI para humanos y agentes.
- SimplePdo - Helper de base de datos preferido para inyección.
Solución de Problemas
- Si tienes problemas con tu contenedor, asegúrate de estar pasando los nombres de clase correctos al contenedor.
- Controladores que indican
Enginepero reciben una aplicación "vacía": agrega la sustitución de Engine (ver arriba). Dice no debe hacernewde un segundo Engine. - Clase no encontrada para
App\Controller\…: verifica las mayúsculas/minúsculas de la carpeta bajoapp/Controller/—consulta Autocarga. - El manejador debe devolver el objeto creado desde
registerContainerHandler(no llames aFlight::make()sinreturn).
Registro de Cambios
- Documentación – Documentar el skeleton Dice + sustituciones de Engine, SimplePdo y la estructura
App\Controllerpara proyectos amigables con IA. - v3.7.0 - Se agregó la capacidad de registrar un manejador DIC en Flight.
Learn/middleware
Middleware
Resumen
Flight soporta middleware de rutas y grupos de rutas. El middleware es una parte de tu aplicación donde se ejecuta código antes (o después) de la devolución de llamada de la ruta. Esta es una excelente manera de agregar verificaciones de autenticación de API en tu código, o para validar que el usuario tiene permiso para acceder a la ruta.
Entendimiento
El middleware puede simplificar enormemente tu aplicación. En lugar de herencia compleja de clases abstractas o sobrescrituras de métodos, el middleware te permite controlar tus rutas asignando tu lógica de aplicación personalizada a ellas. Puedes pensar en el middleware como un sándwich. Tienes pan por fuera, y luego capas de ingredientes como lechuga, tomates, carnes y queso. Luego imagina que cada solicitud es como tomar un bocado del sándwich donde comes las capas externas primero y avanzas hacia el centro.
Aquí hay una visualización de cómo funciona el middleware. Luego te mostraremos un ejemplo práctico de cómo funciona esto.
Solicitud de usuario en URL /api ---->
Middleware->before() ejecutado ----->
Callable/método adjunto a /api ejecutado y respuesta generada ------>
Middleware->after() ejecutado ----->
Usuario recibe respuesta del servidor
Y aquí hay un ejemplo práctico:
Usuario navega a URL /dashboard
LoggedInMiddleware->before() se ejecuta
before() verifica una sesión de inicio de sesión válida
si sí, no hace nada y continúa la ejecución
si no, redirige al usuario a /login
Callable/método adjunto a /api ejecutado y respuesta generada
LoggedInMiddleware->after() no tiene nada definido, así que deja que la ejecución continúe
Usuario recibe HTML del dashboard del servidor
Orden de Ejecución
Las funciones de middleware se ejecutan en el orden en que se agregan a la ruta. La ejecución es similar a cómo Slim Framework maneja esto.
Los métodos before() se ejecutan en el orden agregado, y los métodos after() se ejecutan en orden inverso.
Ej: Middleware1->before(), Middleware2->before(), Middleware2->after(), Middleware1->after().
Uso Básico
Puedes usar middleware como cualquier método callable, incluyendo una función anónima o una clase (recomendado)
Función Anónima
Aquí hay un ejemplo simple:
Flight::route('/path', function() { echo ' Here I am!'; })->addMiddleware(function() {
echo 'Middleware first!';
});
Flight::start();
// Esto imprimirá "Middleware first! Here I am!"
Nota: Cuando uses una función anónima, el único método que se interpreta es un método
before(). No puedes definir comportamientoafter()con una clase anónima.
Usando Clases
El middleware puede (y debe) registrarse como una clase. Si necesitas la funcionalidad "after", debes usar una clase.
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);
// también ->addMiddleware([ $MyMiddleware, $MyMiddleware2 ]);
Flight::start();
// Esto mostrará "Middleware first! Here I am! Middleware last!"
También puedes definir solo el nombre de la clase de middleware y se instanciará la clase.
Flight::route('/path', function() { echo ' Here I am! '; })->addMiddleware(MyMiddleware::class);
Nota: Si pasas solo el nombre del middleware, se ejecutará automáticamente por el contenedor de inyección de dependencias y el middleware se ejecutará con los parámetros que necesita. Si no tienes un contenedor de inyección de dependencias registrado, pasará por defecto la instancia de
flight\Engineen el__construct(Engine $app).
Usando Rutas con Parámetros
Si necesitas parámetros de tu ruta, se pasarán en un solo array a tu función de middleware. (function($params) { ... } o public function before($params) { ... }). La razón de esto es que puedes estructurar tus parámetros en grupos y en algunos de esos grupos, tus parámetros pueden aparecer en un orden diferente, lo que rompería la función de middleware al referirse al parámetro incorrecto. De esta manera, puedes acceder a ellos por nombre en lugar de por posición.
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 puede o no ser pasado
$jobId = $params['jobId'] ?? 0;
// tal vez si no hay ID de trabajo, no necesitas buscar nada.
if($jobId === 0) {
return;
}
// realiza una búsqueda de algún tipo en tu base de datos
$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) {
// Este grupo de abajo aún obtiene el middleware padre
// Pero los parámetros se pasan en un solo array
// en el middleware.
$router->group('/job/@jobId', function(Router $router) {
$router->get('', [ JobController::class, 'view' ]);
$router->put('', [ JobController::class, 'update' ]);
$router->delete('', [ JobController::class, 'delete' ]);
// más rutas...
});
}, [ RouteSecurityMiddleware::class ]);
Agrupando Rutas con Middleware
Puedes agregar un grupo de rutas, y luego cada ruta en ese grupo tendrá el mismo middleware también. Esto es útil si necesitas agrupar un montón de rutas por, digamos, un middleware de Auth para verificar la clave API en el encabezado.
// agregado al final del método de grupo
Flight::group('/api', function() {
// Esta ruta "vacía" coincidirá realmente con /api
Flight::route('', function() { echo 'api'; }, false, 'api');
// Esto coincidirá con /api/users
Flight::route('/users', function() { echo 'users'; }, false, 'users');
// Esto coincidirá con /api/users/1234
Flight::route('/users/@id', function($id) { echo 'user:'.$id; }, false, 'user_view');
}, [ new ApiAuthMiddleware() ]);
Si quieres aplicar un middleware global a todas tus rutas, puedes agregar un grupo "vacío":
// agregado al final del método de grupo
Flight::group('', function() {
// Esto sigue siendo /users
Flight::route('/users', function() { echo 'users'; }, false, 'users');
// Y esto sigue siendo /users/1234
Flight::route('/users/@id', function($id) { echo 'user:'.$id; }, false, 'user_view');
}, [ ApiAuthMiddleware::class ]); // o [ new ApiAuthMiddleware() ], lo mismo
Casos de Uso Comunes
Validación de Clave API
Si quisieras proteger tus rutas /api verificando que la clave API sea correcta, puedes manejarlo fácilmente con 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);
// realiza una búsqueda en tu base de datos para la clave 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' ]);
// más rutas...
}, [ ApiMiddleware::class ]);
¡Ahora todas tus rutas API están protegidas por este middleware de validación de clave API que has configurado! Si pones más rutas en el grupo del router, tendrán instantáneamente la misma protección!
Validación de Inicio de Sesión
¿Quieres proteger algunas rutas para que solo estén disponibles para usuarios que han iniciado sesión? ¡Eso se puede lograr fácilmente con 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' ]);
// más rutas...
}, [ LoggedInMiddleware::class ]);
Validación de Parámetro de Ruta
¿Quieres proteger a tus usuarios de cambiar valores en la URL para acceder a datos que no deberían? ¡Eso se puede resolver con 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'];
// realiza una búsqueda de algún tipo en tu base de datos
$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' ]);
// más rutas...
}, [ RouteSecurityMiddleware::class ]);
Manejo de la Ejecución de Middleware
Supongamos que tienes un middleware de autenticación y quieres redirigir al usuario a una página de inicio de sesión si no está autenticado. Tienes un par de opciones a tu disposición:
- Puedes devolver false desde la función de middleware y Flight devolverá automáticamente un error 403 Forbidden, pero sin personalización.
- Puedes redirigir al usuario a una página de inicio de sesión usando
Flight::redirect(). - Puedes crear un error personalizado dentro del middleware y detener la ejecución de la ruta.
Simple y Directo
Aquí hay un ejemplo simple de return false; :
class MyMiddleware {
public function before($params) {
$hasUserKey = Flight::session()->exists('user');
if ($hasUserKey === false) {
return false;
}
// ya que es verdadero, todo sigue adelante
}
}
Ejemplo de Redirección
Aquí hay un ejemplo de redirigir al usuario a una página de inicio de sesión:
class MyMiddleware {
public function before($params) {
$hasUserKey = Flight::session()->exists('user');
if ($hasUserKey === false) {
Flight::redirect('/login');
exit;
}
}
}
Ejemplo de Error Personalizado
Supongamos que necesitas lanzar un error JSON porque estás construyendo una API. Puedes hacerlo así:
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);
// o
Flight::json(['error' => 'You must be logged in to access this page.'], 403);
exit;
// o
Flight::halt(403, json_encode(['error' => 'You must be logged in to access this page.']);
}
}
}
Ver También
- Routing - Cómo mapear rutas a controladores y renderizar vistas.
- Requests - Entendiendo cómo manejar solicitudes entrantes.
- Responses - Cómo personalizar respuestas HTTP.
- Dependency Injection - Simplificando la creación y gestión de objetos en rutas.
- Why a Framework? - Entendiendo los beneficios de usar un framework como Flight.
- Middleware Execution Strategy Example
Solución de Problemas
- Si tienes una redirección en tu middleware, pero tu aplicación no parece estar redirigiendo, asegúrate de agregar una declaración
exit;en tu middleware.
Registro de Cambios
- v3.1: Agregado soporte para middleware.
Learn/filtering
Filtrado
Resumen
Flight te permite filtrar métodos mapeados antes y después de que se llamen.
Comprensión
No hay ganchos predefinidos que necesites memorizar. Puedes filtrar cualquiera de los métodos predeterminados del framework, así como cualquier método personalizado que hayas mapeado.
Una función de filtro se ve así:
/**
* @param array $params Los parámetros pasados al método que se está filtrando.
* @param string $output (solo v2 con búfer de salida) La salida del método que se está filtrando.
* @return bool Devuelve true/void o no devuelvas nada para continuar la cadena, false para romper la cadena.
*/
function (array &$params, string &$output): bool {
// Código de filtro
}
Usando las variables pasadas, puedes manipular los parámetros de entrada y/o la salida.
Puedes hacer que un filtro se ejecute antes de un método haciendo:
Flight::before('start', function (array &$params, string &$output): bool {
// Haz algo
});
Puedes hacer que un filtro se ejecute después de un método haciendo:
Flight::after('start', function (array &$params, string &$output): bool {
// Haz algo
});
Puedes agregar tantos filtros como quieras a cualquier método. Se llamarán en el orden en que se declaren.
Aquí hay un ejemplo del proceso de filtrado:
// Mapa un método personalizado
Flight::map('hello', function (string $name) {
return "Hello, $name!";
});
// Agrega un filtro antes
Flight::before('hello', function (array &$params, string &$output): bool {
// Manipula el parámetro
$params[0] = 'Fred';
return true;
});
// Agrega un filtro después
Flight::after('hello', function (array &$params, string &$output): bool {
// Manipula la salida
$output .= " Have a nice day!";
return true;
});
// Invoca el método personalizado
echo Flight::hello('Bob');
Esto debería mostrar:
Hello Fred! Have a nice day!
Si has definido múltiples filtros, puedes romper la cadena devolviendo false en cualquiera de tus funciones de filtro:
Flight::before('start', function (array &$params, string &$output): bool {
echo 'one';
return true;
});
Flight::before('start', function (array &$params, string &$output): bool {
echo 'two';
// Esto terminará la cadena
return false;
});
// Esto no se llamará
Flight::before('start', function (array &$params, string &$output): bool {
echo 'three';
return true;
});
Nota: Los métodos principales como
mapyregisterno se pueden filtrar porque se llaman directamente y no se invocan dinámicamente. Consulta Extending Flight para obtener más información.
Ver también
Solución de problemas
- Asegúrate de devolver
falsedesde tus funciones de filtro si quieres que la cadena se detenga. Si no devuelves nada, la cadena continuará.
Registro de cambios
- v2.0 - Lanzamiento inicial.
Learn/requests
Solicitudes
Resumen
Flight encapsula la solicitud HTTP en un solo objeto, que se puede acceder haciendo:
$request = Flight::request();
Comprensión
Las solicitudes HTTP son uno de los aspectos centrales para entender sobre el ciclo de vida de HTTP. Un usuario realiza una acción en un navegador web o un cliente HTTP, y envían una serie de encabezados, cuerpo, URL, etc. a su proyecto. Puede capturar estos encabezados (el idioma del navegador, qué tipo de compresión pueden manejar, el agente de usuario, etc.) y capturar el cuerpo y la URL que se envía a su aplicación Flight. Estas solicitudes son esenciales para que su app entienda qué hacer a continuación.
Uso básico
PHP tiene varios super globals incluyendo $_GET, $_POST, $_REQUEST, $_SERVER, $_FILES y $_COOKIE. Flight abstrae estos en colecciones prácticas Collections. Puede acceder a las propiedades query, data, cookies y files como arrays u objetos.
Nota: Se DESACONSEJA EN GRAN MEDIDA usar estos super globals en su proyecto y deben referenciarse a través del objeto
request().
Nota: No hay abstracción disponible para
$_ENV.
$_GET
Puede acceder al array $_GET a través de la propiedad query:
// GET /search?keyword=something
Flight::route('/search', function(){
$keyword = Flight::request()->query['keyword'];
// o
$keyword = Flight::request()->query->keyword;
echo "You are searching for: $keyword";
// consultar una base de datos o algo más con el $keyword
});
$_POST
Puede acceder al array $_POST a través de la propiedad data:
Flight::route('POST /submit', function(){
$name = Flight::request()->data['name'];
$email = Flight::request()->data['email'];
// o
$name = Flight::request()->data->name;
$email = Flight::request()->data->email;
echo "You submitted: $name, $email";
// guardar en una base de datos o algo más con el $name y $email
});
$_COOKIE
Puede acceder al array $_COOKIE a través de la propiedad cookies:
Flight::route('GET /login', function(){
$savedLogin = Flight::request()->cookies['myLoginCookie'];
// o
$savedLogin = Flight::request()->cookies->myLoginCookie;
// verificar si realmente está guardado o no y si lo está, iniciar sesión automáticamente
if($savedLogin) {
Flight::redirect('/dashboard');
return;
}
});
Para obtener ayuda sobre cómo establecer nuevos valores de cookies, vea overclokk/cookie
$_SERVER
Hay un método abreviado disponible para acceder al array $_SERVER a través del método getVar():
$host = Flight::request()->getVar('HTTP_HOST');
$_FILES
Puede acceder a los archivos subidos a través de la propiedad files:
// acceso crudo a la propiedad $_FILES. Vea abajo para el enfoque recomendado
$uploadedFile = Flight::request()->files['myFile'];
// o
$uploadedFile = Flight::request()->files->myFile;
Vea Uploaded File Handler para más información.
Procesamiento de subidas de archivos
v3.12.0
Puede procesar subidas de archivos usando el framework con algunos métodos de ayuda. Básicamente se reduce a extraer los datos del archivo de la solicitud y moverlo a una nueva ubicación.
Flight::route('POST /upload', function(){
// Si tenía un campo de entrada como <input type="file" name="myFile">
$uploadedFileData = Flight::request()->getUploadedFiles();
$uploadedFile = $uploadedFileData['myFile'];
$uploadedFile->moveTo('/path/to/uploads/' . $uploadedFile->getClientFilename());
});
Si tiene múltiples archivos subidos, puede iterar a través de ellos:
Flight::route('POST /upload', function(){
// Si tenía un campo de entrada como <input type="file" name="myFiles[]">
$uploadedFiles = Flight::request()->getUploadedFiles()['myFiles'];
foreach ($uploadedFiles as $uploadedFile) {
$uploadedFile->moveTo('/path/to/uploads/' . $uploadedFile->getClientFilename());
}
});
Nota de seguridad: Siempre valide y sanitice la entrada del usuario, especialmente al tratar con subidas de archivos. Siempre valide el tipo de extensiones que permitirá subir, pero también debe validar los "magic bytes" del archivo para asegurar que realmente sea el tipo de archivo que el usuario afirma que es. Hay artículos y bibliotecas disponibles para ayudar con esto.
Cuerpo de la solicitud
Para obtener el cuerpo crudo de la solicitud HTTP, por ejemplo al tratar con solicitudes POST/PUT, puede hacer:
Flight::route('POST /users/xml', function(){
$xmlBody = Flight::request()->getBody();
// hacer algo con el XML que fue enviado.
});
Cuerpo JSON
Si recibe una solicitud con el tipo de contenido application/json y los datos de ejemplo {"id": 123}, estará disponible desde la propiedad data:
$id = Flight::request()->data->id;
Encabezados de la solicitud
Puede acceder a los encabezados de la solicitud usando el método getHeader() o getHeaders():
// Tal vez necesite el encabezado Authorization
$host = Flight::request()->getHeader('Authorization');
// o
$host = Flight::request()->header('Authorization');
// Si necesita obtener todos los encabezados
$headers = Flight::request()->getHeaders();
// o
$headers = Flight::request()->headers();
Método de la solicitud
Puede acceder al método de la solicitud usando la propiedad method o el método getMethod():
$method = Flight::request()->method; // realmente poblado por getMethod()
$method = Flight::request()->getMethod();
Nota: El método getMethod() primero extrae el método de $_SERVER['REQUEST_METHOD'], luego puede ser sobrescrito por $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] si existe o $_REQUEST['_method'] si existe.
Propiedades del objeto de solicitud
El objeto de solicitud proporciona las siguientes propiedades:
- body - El cuerpo crudo de la solicitud HTTP
- url - La URL solicitada
- base - El subdirectorio padre de la URL
- method - El método de la solicitud (GET, POST, PUT, DELETE)
- referrer - La URL de referencia
- ip - Dirección IP del cliente
- ajax - Si la solicitud es una solicitud AJAX
- scheme - El protocolo del servidor (http, https)
- user_agent - Información del navegador
- type - El tipo de contenido
- length - La longitud del contenido
- query - Parámetros de la cadena de consulta
- data - Datos POST o datos JSON
- cookies - Datos de cookies
- files - Archivos subidos
- secure - Si la conexión es segura
- accept - Parámetros de aceptación HTTP
- proxy_ip - Dirección IP del proxy del cliente. Escanea el array
$_SERVERen busca deHTTP_CLIENT_IP,HTTP_X_FORWARDED_FOR,HTTP_X_FORWARDED,HTTP_X_CLUSTER_CLIENT_IP,HTTP_FORWARDED_FOR,HTTP_FORWARDEDen ese orden. - host - El nombre de host de la solicitud
- servername - El SERVER_NAME de
$_SERVER
Métodos de ayuda
Hay algunos métodos de ayuda para ensamblar partes de una URL o tratar con ciertos encabezados.
URL completa
Puede acceder a la URL completa de la solicitud usando el método getFullUrl():
$url = Flight::request()->getFullUrl();
// https://example.com/some/path?foo=bar
URL base
Puede acceder a la URL base usando el método getBaseUrl():
// http://example.com/path/to/something/cool?query=yes+thanks
$url = Flight::request()->getBaseUrl();
// https://example.com
// Note, no trailing slash.
Análisis de consultas
Puede pasar una URL al método parseQuery() para analizar la cadena de consulta en un array asociativo:
$query = Flight::request()->parseQuery('https://example.com/some/path?foo=bar');
// ['foo' => 'bar']
Negociación de tipos de aceptación de contenido
v3.17.2
Puede usar el método negotiateContentType() para determinar el mejor tipo de contenido para responder basado en el encabezado Accept enviado por el cliente.
// Ejemplo de encabezado Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
// Lo de abajo define lo que soporta.
$availableTypes = ['application/json', 'application/xml'];
$typeToServe = Flight::request()->negotiateContentType($availableTypes);
if ($typeToServe === 'application/json') {
// Servir respuesta JSON
} elseif ($typeToServe === 'application/xml') {
// Servir respuesta XML
} else {
// Por defecto algo más o lanzar un error
}
Nota: Si ninguno de los tipos disponibles se encuentra en el encabezado
Accept, el método retornaránull. Si no hay encabezadoAcceptdefinido, el método retornará el primer tipo en el array$availableTypes.
Ver también
- Routing - Vea cómo mapear rutas a controladores y renderizar vistas.
- Responses - Cómo personalizar respuestas HTTP.
- Why a Framework? - Cómo las solicitudes encajan en el panorama general.
- Collections - Trabajando con colecciones de datos.
- Uploaded File Handler - Manejo de subidas de archivos.
Solución de problemas
request()->ipyrequest()->proxy_ippueden ser diferentes si su servidor web está detrás de un proxy, balanceador de carga, etc.
Registro de cambios
- v3.17.2 - Agregado negotiateContentType()
- v3.12.0 - Agregada capacidad para manejar subidas de archivos a través del objeto de solicitud.
- v1.0 - Lanzamiento inicial.
Learn/why_frameworks
¿Por qué un Framework?
Algunos programadores se oponen vehementemente a utilizar frameworks. Argumentan que los frameworks son pesados, lentos y difíciles de aprender. Dicen que los frameworks son innecesarios y que puedes escribir un código mejor sin ellos. Ciertamente se pueden hacer algunos puntos válidos sobre las desventajas de usar frameworks. Sin embargo, también existen muchas ventajas al utilizar frameworks.
Razones para usar un Framework
Aquí hay algunas razones por las cuales podrías considerar utilizar un framework:
- Desarrollo Rápido: Los frameworks proporcionan mucha funcionalidad de serie. Esto significa que puedes construir aplicaciones web más rápidamente. No necesitas escribir tanto código porque el framework proporciona mucha de la funcionalidad que necesitas.
- Consistencia: Los frameworks ofrecen una forma consistente de hacer las cosas. Esto facilita tu comprensión de cómo funciona el código y también facilita que otros desarrolladores entiendan tu código. Si lo tienes guion por guion, podrías perder consistencia entre guiones, especialmente si estás trabajando con un equipo de desarrolladores.
- Seguridad: Los frameworks ofrecen funciones de seguridad que ayudan a proteger tus aplicaciones web de amenazas de seguridad comunes. Esto significa que no tienes que preocuparte tanto por la seguridad porque el framework se encarga de gran parte de ello.
- Comunidad: Los frameworks cuentan con grandes comunidades de desarrolladores que contribuyen al framework. Esto significa que puedes obtener ayuda de otros desarrolladores cuando tengas preguntas o problemas. También significa que hay muchos recursos disponibles para ayudarte a aprender cómo utilizar el framework.
- Mejores Prácticas: Los frameworks están construidos utilizando mejores prácticas. Esto significa que puedes aprender del framework y usar las mismas mejores prácticas en tu propio código. Esto puede ayudarte a ser un mejor programador. A veces no sabes lo que no sabes y eso puede perjudicarte al final.
- Extensibilidad: Los frameworks están diseñados para ser extendidos. Esto significa que puedes agregar tu propia funcionalidad al framework. Esto te permite construir aplicaciones web adaptadas a tus necesidades específicas.
Flight es un micro-framework. Esto significa que es pequeño y ligero. No proporciona tanta funcionalidad como frameworks más grandes como Laravel o Symfony. Sin embargo, proporciona mucha de la funcionalidad que necesitas para construir aplicaciones web. También es fácil de aprender y usar. Esto lo convierte en una buena elección para construir aplicaciones web rápidamente y fácilmente. Si eres nuevo en los frameworks, Flight es un gran framework para principiantes con el que empezar. Te ayudará a entender las ventajas de usar frameworks sin abrumarte con demasiada complejidad. Después de tener algo de experiencia con Flight, será más fácil pasar a frameworks más complejos como Laravel o Symfony, sin embargo, Flight aún puede crear una aplicación robusta y exitosa.
¿Qué es el Enrutamiento?
El enrutamiento es el núcleo del framework Flight, ¿pero qué es exactamente? Enrutamiento es el proceso de tomar una URL y emparejarla con una función específica en tu código.
Esto es cómo puedes hacer que tu sitio web haga cosas diferentes basadas en la URL que se solicita. Por ejemplo, es posible que desees mostrar el perfil de un usuario cuando
visitan /usuario/1234, pero mostrar una lista de todos los usuarios cuando visitan /usuarios. Todo esto se hace a través del enrutamiento.
Podría funcionar algo así:
- Un usuario va a tu navegador y escribe
http://ejemplo.com/usuario/1234. - El servidor recibe la solicitud y mira la URL y la pasa a tu código de aplicación de Flight.
- Digamos que en tu código de Flight tienes algo así como
Flight::route('/usuario/@id', [ 'ControladorUsuario', 'verPerfilUsuario' ]);. Tu código de la aplicación de Flight mira la URL y ve que coincide con una ruta que has definido, y luego ejecuta el código que has definido para esa ruta. - El enrutador de Flight luego ejecutará y llamará el método
verPerfilUsuario($id)en la claseControladorUsuario, pasando el1234como el argumento$iden el método. - El código en tu método
verPerfilUsuario()se ejecutará y hará lo que le hayas indicado. Podrías terminar imprimiendo algo de HTML para la página del perfil del usuario, o si se trata de una API RESTful, podrías imprimir una respuesta JSON con la información del usuario. - Flight envuelve esto en un bonito lazo, genera los encabezados de respuesta y lo envía de vuelta al navegador del usuario.
- ¡El usuario se llena de alegría y se da un cálido abrazo a sí mismo!
¿Y por qué es importante?
¡Tener un enrutador centralizado adecuado puede realmente hacer tu vida mucho más fácil! Al principio, podría ser difícil verlo. Aquí hay algunas razones por las cuales:
- Enrutamiento Centralizado: Puedes mantener todas tus rutas en un solo lugar. Esto facilita ver qué rutas tienes y qué hacen. También facilita cambiarlas si es necesario.
- Parámetros de Ruta: Puedes usar parámetros de ruta para pasar datos a tus métodos de ruta. Esta es una excelente manera de mantener tu código limpio y organizado.
- Grupos de Rutas: Puedes agrupar rutas juntas. Esto es excelente para mantener tu código organizado y para aplicar middleware a un grupo de rutas.
- Alias de Ruta: Puedes asignar un alias a una ruta, para que la URL pueda generarse dinámicamente más tarde en tu código (como una plantilla, por ejemplo). Ej: en lugar de codificar
/usuario/1234en tu código, podrías en su lugar hacer referencia al aliasvista_usuarioy pasar elidcomo parámetro. Esto es útil en caso de que decidas cambiarlo a/admin/usuario/1234más adelante. No tendrías que cambiar todas tus URLs codificadas, solo la URL vinculada a la ruta. - Middleware de Ruta: Puedes agregar middleware a tus rutas. El middleware es increíblemente potente para agregar comportamientos específicos a tu aplicación como autenticar que cierto usuario pueda acceder a una ruta o grupo de rutas.
Seguro que estás familiarizado con la forma guion por guion de crear un sitio web. Podrías tener un archivo llamado index.php que tiene un montón de declaraciones if
para verificar la URL y luego ejecutar una función específica basada en la URL. Esto es una forma de enrutamiento, pero no es muy organizado y puede
salirse de control rápidamente. El sistema de enrutamiento de Flight es una forma mucho más organizada y poderosa de manejar el enrutamiento.
¿Esto?
// /usuario/ver_perfil.php?id=1234
if ($_GET['id']) {
$id = $_GET['id'];
verPerfilUsuario($id);
}
// /usuario/editar_perfil.php?id=1234
if ($_GET['id']) {
$id = $_GET['id'];
editarPerfilUsuario($id);
}
// etc...
¿O esto?
// index.php
Flight::route('/usuario/@id', [ 'ControladorUsuario', 'verPerfilUsuario' ]);
Flight::route('/usuario/@id/editar', [ 'ControladorUsuario', 'editarPerfilUsuario' ]);
// En tal vez tu app/controladores/ControladorUsuario.php
class ControladorUsuario {
public function verPerfilUsuario($id) {
// hacer algo
}
public function editarPerfilUsuario($id) {
// hacer algo
}
}
¡Espero que comiences a ver los beneficios de usar un sistema de enrutamiento centralizado. Es mucho más fácil de gestionar y entender a largo plazo!
Solicitudes y Respuestas
Flight proporciona una forma simple y fácil de manejar solicitudes y respuestas. Esto es lo básico de lo que hace un framework web. Toma una solicitud de un navegador del usuario, la procesa, y luego envía una respuesta. Así es como puedes construir aplicaciones web que hagan cosas como mostrar el perfil de un usuario, permitir a un usuario iniciar sesión o permitir a un usuario publicar una nueva publicación en un blog.
Solicitudes
Una solicitud es lo que un navegador del usuario envía a tu servidor cuando visita tu sitio web. Esta solicitud contiene información sobre lo que el usuario quiere hacer. Por ejemplo, podría contener información sobre qué URL quiere visitar el usuario, qué datos quiere enviar el usuario a tu servidor, o qué tipo de datos quiere recibir del servidor. Es importante tener en cuenta que una solicitud es de solo lectura. No puedes cambiar la solicitud, pero puedes leer de ella.
Flight proporciona una forma simple de acceder a información sobre la solicitud. Puedes acceder a información sobre la solicitud utilizando el método Flight::request()
Este método devuelve un objeto Request que contiene información sobre la solicitud. Puedes usar este objeto para acceder a información sobre la solicitud,
como la URL, el método o los datos que el usuario envió a tu servidor.
Respuestas
Una respuesta es lo que tu servidor envía de vuelta al navegador del usuario cuando visita tu sitio web. Esta respuesta contiene información sobre lo que tu servidor quiere hacer. Por ejemplo, podría contener información sobre qué tipo de datos tu servidor quiere enviar al usuario, qué tipo de datos tu servidor quiere recibir del usuario, o qué tipo de datos tu servidor quiere almacenar en la computadora del usuario.
Flight proporciona una forma simple de enviar una respuesta al navegador del usuario. Puedes enviar una respuesta usando el método Flight::response()
Este método toma un objeto Response como argumento y envía la respuesta al navegador del usuario. Puedes usar este objeto para enviar una respuesta al navegador del usuario,
como HTML, JSON o un archivo. Flight te ayuda a generar automáticamente algunas partes de la respuesta para facilitar las cosas, pero en última instancia tú tienes
control sobre lo que envías de vuelta al usuario.
Learn/responses
Respuestas
Resumen
Flight ayuda a generar parte de los encabezados de respuesta por ti, pero tú tienes la mayoría del control sobre lo que envías de vuelta al usuario. La mayoría del tiempo accederás directamente al objeto response(), pero Flight tiene algunos métodos auxiliares para configurar algunos de los encabezados de respuesta por ti.
Comprensión
Después de que el usuario envíe su solicitud a tu aplicación, necesitas generar una respuesta adecuada para ellos. Te han enviado información como el idioma que prefieren, si pueden manejar ciertos tipos de compresión, su agente de usuario, etc., y después de procesar todo, es hora de enviarles una respuesta adecuada. Esto puede ser configurar encabezados, generar un cuerpo de HTML o JSON para ellos, o redirigirlos a una página.
Uso básico
Envío de un cuerpo de respuesta
Flight usa ob_start() para almacenar en búfer la salida. Esto significa que puedes usar echo o print para enviar una respuesta al usuario y Flight la capturará y la enviará de vuelta al usuario con los encabezados apropiados.
// Esto enviará "Hello, World!" al navegador del usuario
Flight::route('/', function() {
echo "Hello, World!";
});
// HTTP/1.1 200 OK
// Content-Type: text/html
//
// Hello, World!
Como alternativa, puedes llamar al método write() para agregar al cuerpo también.
// Esto enviará "Hello, World!" al navegador del usuario
Flight::route('/', function() {
// verboso, pero a veces hace el trabajo cuando lo necesitas
Flight::response()->write("Hello, World!");
// si quieres recuperar el cuerpo que has configurado en este punto
// puedes hacerlo así
$body = Flight::response()->getBody();
});
JSON
Flight proporciona soporte para enviar respuestas JSON y JSONP. Para enviar una respuesta JSON, pasa algunos datos para que se codifiquen en JSON:
Flight::route('/@companyId/users', function(int $companyId) {
// de alguna manera extrae tus usuarios de una base de datos por ejemplo
$users = Flight::db()->fetchAll("SELECT id, first_name, last_name FROM users WHERE company_id = ?", [ $companyId ]);
Flight::json($users);
});
// [{"id":1,"first_name":"Bob","last_name":"Jones"}, /* more users */ ]
Nota: Por defecto, Flight enviará un encabezado
Content-Type: application/jsoncon la respuesta. También usará las banderasJSON_THROW_ON_ERRORyJSON_UNESCAPED_SLASHESal codificar el JSON.
JSON con código de estado
También puedes pasar un código de estado como segundo argumento:
Flight::json(['id' => 123], 201);
JSON con impresión bonita
También puedes pasar un argumento en la última posición para habilitar la impresión bonita:
Flight::json(['id' => 123], 200, true, 'utf-8', JSON_PRETTY_PRINT);
Cambiar el orden de los argumentos JSON
Flight::json() es un método muy antiguo, pero el objetivo de Flight es mantener la compatibilidad hacia atrás para los proyectos. En realidad es muy simple si quieres rehacer el orden de los argumentos para usar una sintaxis más simple, puedes simplemente remapear el método JSON como cualquier otro método de Flight:
Flight::map('json', function($data, $code = 200, $options = 0) {
// ahora no tienes que usar `true, 'utf-8'` al usar el método json()!
Flight::_json($data, $code, true, 'utf-8', $options);
}
// Y ahora se puede usar así
Flight::json(['id' => 123], 200, JSON_PRETTY_PRINT);
JSON y detención de ejecución
v3.10.0
Si quieres enviar una respuesta JSON y detener la ejecución, puedes usar el método jsonHalt(). Esto es útil para casos en los que estás verificando quizás algún tipo de autorización y si el usuario no está autorizado, puedes enviar una respuesta JSON inmediatamente, limpiar el contenido del cuerpo existente y detener la ejecución.
Flight::route('/users', function() {
$authorized = someAuthorizationCheck();
// Verifica si el usuario está autorizado
if($authorized === false) {
Flight::jsonHalt(['error' => 'Unauthorized'], 401);
// no se necesita exit; aquí.
}
// Continúa con el resto de la ruta
});
Antes de v3.10.0, tendrías que hacer algo como esto:
Flight::route('/users', function() {
$authorized = someAuthorizationCheck();
// Verifica si el usuario está autorizado
if($authorized === false) {
Flight::halt(401, json_encode(['error' => 'Unauthorized']));
}
// Continúa con el resto de la ruta
});
Limpieza de un cuerpo de respuesta
Si quieres limpiar el cuerpo de la respuesta, puedes usar el método clearBody:
Flight::route('/', function() {
if($someCondition) {
Flight::response()->write("Hello, World!");
} else {
Flight::response()->clearBody();
}
});
El caso de uso anterior probablemente no es común, sin embargo podría ser más común si se usara en un middleware.
Ejecución de un callback en el cuerpo de la respuesta
Puedes ejecutar un callback en el cuerpo de la respuesta usando el método addResponseBodyCallback:
Flight::route('/users', function() {
$db = Flight::db();
$users = $db->fetchAll("SELECT * FROM users");
Flight::render('users_table', ['users' => $users]);
});
// Esto comprime con gzip todas las respuestas para cualquier ruta
Flight::response()->addResponseBodyCallback(function($body) {
return gzencode($body, 9);
});
Puedes agregar múltiples callbacks y se ejecutarán en el orden en que se agregaron. Dado que esto puede aceptar cualquier llamable, puede aceptar un array de clase [ $class, 'method' ], una closure $strReplace = function($body) { str_replace('hi', 'there', $body); };, o un nombre de función 'minify' si tuvieras una función para minificar tu código HTML por ejemplo.
Nota: Los callbacks de ruta no funcionarán si estás usando la opción de configuración flight.v2.output_buffering.
Callback de ruta específica
Si quisieras que esto solo se aplique a una ruta específica, podrías agregar el callback en la ruta misma:
Flight::route('/users', function() {
$db = Flight::db();
$users = $db->fetchAll("SELECT * FROM users");
Flight::render('users_table', ['users' => $users]);
// Esto comprime con gzip solo la respuesta para esta ruta
Flight::response()->addResponseBodyCallback(function($body) {
return gzencode($body, 9);
});
});
Opción de middleware
También puedes usar middleware para aplicar el callback a todas las rutas a través de middleware:
// MinifyMiddleware.php
class MinifyMiddleware {
public function before() {
// Aplica el callback aquí en el objeto response().
Flight::response()->addResponseBodyCallback(function($body) {
return $this->minify($body);
});
}
protected function minify(string $body): string {
// minifica el cuerpo de alguna manera
return $body;
}
}
// index.php
Flight::group('/users', function() {
Flight::route('', function() { /* ... */ });
Flight::route('/@id', function($id) { /* ... */ });
}, [ new MinifyMiddleware() ]);
Códigos de estado
Puedes configurar el código de estado de la respuesta usando el método status:
Flight::route('/@id', function($id) {
if($id == 123) {
Flight::response()->status(200);
echo "Hello, World!";
} else {
Flight::response()->status(403);
echo "Forbidden";
}
});
Si quieres obtener el código de estado actual, puedes usar el método status sin argumentos:
Flight::response()->status(); // 200
Configuración de un encabezado de respuesta
Puedes configurar un encabezado como el tipo de contenido de la respuesta usando el método header:
// Esto enviará "Hello, World!" al navegador del usuario en texto plano
Flight::route('/', function() {
Flight::response()->header('Content-Type', 'text/plain');
// o
Flight::response()->setHeader('Content-Type', 'text/plain');
echo "Hello, World!";
});
Redirección
Puedes redirigir la solicitud actual usando el método redirect() y pasando una nueva 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; // esto es necesario para que la funcionalidad a continuación no se ejecute
}
// agrega el nuevo usuario...
Flight::db()->runQuery("INSERT INTO users ....");
Flight::redirect('/admin/dashboard');
});
Nota: Por defecto, Flight envía un código de estado HTTP 303 ("See Other"). Puedes configurar opcionalmente un código personalizado:
Flight::redirect('/new/location', 301); // permanente
Detención de la ejecución de la ruta
Puedes detener el framework y salir inmediatamente en cualquier punto llamando al método halt:
Flight::halt();
También puedes especificar un código de estado HTTP y mensaje opcionales:
Flight::halt(200, 'Be right back...');
Llamar a halt descartará cualquier contenido de respuesta hasta ese punto y detendrá toda la ejecución. Si quieres detener el framework y generar la respuesta actual, usa el método stop:
Flight::stop($httpStatusCode = null);
Nota:
Flight::stop()tiene un comportamiento extraño, como que generará la respuesta pero continuará ejecutando tu script, lo cual podría no ser lo que buscas. Puedes usarexitoreturndespués de llamar aFlight::stop()para prevenir la ejecución adicional, pero en general se recomienda usarFlight::halt().
Esto guardará la clave y el valor del encabezado en el objeto de respuesta. Al final del ciclo de vida de la solicitud, construirá los encabezados y enviará una respuesta.
Uso avanzado
Envío de un encabezado inmediatamente
Puede haber veces en las que necesites hacer algo personalizado con el encabezado y necesites enviar el encabezado en esa misma línea de código con la que estás trabajando. Si estás configurando una ruta transmitida, esto es lo que necesitarías. Eso se logra a través de response()->setRealHeader().
Flight::route('/', function() {
Flight::response()->setRealHeader('Content-Type: text/plain');
echo 'Streaming response...';
sleep(5);
echo 'Done!';
})->stream();
JSONP
Para solicitudes JSONP, puedes pasar opcionalmente el nombre del parámetro de consulta que estás usando para definir tu función de callback:
Flight::jsonp(['id' => 123], 'q');
Entonces, al hacer una solicitud GET usando ?q=my_func, deberías recibir la salida:
my_func({"id":123});
Si no pasas un nombre de parámetro de consulta, se usará jsonp por defecto.
Nota: Si aún estás usando solicitudes JSONP en 2025 y más allá, únete al chat y cuéntanos por qué! ¡Nos encanta escuchar algunas buenas historias de batalla/horror!
Limpieza de datos de respuesta
Puedes limpiar el cuerpo de la respuesta y los encabezados usando el método clear(). Esto limpiará cualquier encabezado asignado a la respuesta, limpiará el cuerpo de la respuesta y configurará el código de estado en 200.
Flight::response()->clear();
Limpieza solo del cuerpo de respuesta
Si solo quieres limpiar el cuerpo de la respuesta, puedes usar el método clearBody():
// Esto aún mantendrá cualquier encabezado configurado en el objeto response().
Flight::response()->clearBody();
Caché HTTP
Flight proporciona soporte integrado para caché a nivel HTTP. Si se cumple la condición de caché, Flight devolverá una respuesta HTTP 304 Not Modified. La próxima vez que el cliente solicite el mismo recurso, se le indicará que use su versión en caché localmente.
Caché a nivel de ruta
Si quieres cachear toda tu respuesta, puedes usar el método cache() y pasar el tiempo para cachear.
// Esto cacheará la respuesta por 5 minutos
Flight::route('/news', function () {
Flight::response()->cache(time() + 300);
echo 'This content will be cached.';
});
// Alternativamente, puedes usar una cadena que pasarías
// al método strtotime()
Flight::route('/news', function () {
Flight::response()->cache('+5 minutes');
echo 'This content will be cached.';
});
Última modificación
Puedes usar el método lastModified y pasar una marca de tiempo UNIX para configurar la fecha y hora en que una página fue modificada por última vez. El cliente continuará usando su caché hasta que el valor de última modificación cambie.
Flight::route('/news', function () {
Flight::lastModified(1234567890);
echo 'This content will be cached.';
});
ETag
El caché ETag es similar a Last-Modified, excepto que puedes especificar cualquier id que quieras para el recurso:
Flight::route('/news', function () {
Flight::etag('my-unique-id');
echo 'This content will be cached.';
});
Ten en cuenta que llamar a cualquiera de lastModified o etag configurará y verificará ambos el valor de caché. Si el valor de caché es el mismo entre solicitudes, Flight enviará inmediatamente una respuesta HTTP 304 y detendrá el procesamiento.
Descarga de un archivo
v3.12.0
Hay un método auxiliar para transmitir un archivo al usuario final. Puedes usar el método download y pasar la ruta.
Flight::route('/download', function () {
Flight::download('/path/to/file.txt');
// A partir de v3.17.1 puedes especificar un nombre de archivo personalizado para la descarga
Flight::download('/path/to/file.txt', 'custom_name.txt');
});
Ver también
- Enrutamiento - Cómo mapear rutas a controladores y renderizar vistas.
- Solicitudes - Comprender cómo manejar solicitudes entrantes.
- Middleware - Usar middleware con rutas para autenticación, registro, etc.
- ¿Por qué un framework? - Comprender los beneficios de usar un framework como Flight.
- Extensión - Cómo extender Flight con tu propia funcionalidad.
Solución de problemas
- Si tienes problemas con las redirecciones que no funcionan, asegúrate de agregar un
return;al método. stop()yhalt()no son lo mismo.halt()detendrá la ejecución inmediatamente, mientras questop()permitirá que la ejecución continúe.
Registro de cambios
- v3.17.1 - Agregado
$fileNameal métododownloadFile(). - v3.12.0 - Agregado método auxiliar
downloadFile. - v3.10.0 - Agregado
jsonHalt. - v1.0 - Lanzamiento inicial.
Learn/events
Gestor de Eventos
a partir de v3.15.0
Resumen
Los eventos te permiten registrar y activar comportamientos personalizados en tu aplicación. Con la adición de Flight::onEvent() y Flight::triggerEvent(), ahora puedes engancharte en momentos clave del ciclo de vida de tu app o definir tus propios eventos (como notificaciones y correos electrónicos) para hacer tu código más modular y extensible. Estos métodos son parte de los métodos mapeables de Flight, lo que significa que puedes sobrescribir su comportamiento para adaptarlo a tus necesidades.
Comprensión
Los eventos te permiten separar diferentes partes de tu aplicación para que no dependan demasiado unas de otras. Esta separación—a menudo llamada desacoplamiento—hace que tu código sea más fácil de actualizar, extender o depurar. En lugar de escribir todo en un gran bloque, puedes dividir tu lógica en piezas más pequeñas e independientes que respondan a acciones específicas (eventos).
Imagina que estás construyendo una app de blog:
- Cuando un usuario publica un comentario, podrías querer:
- Guardar el comentario en la base de datos.
- Enviar un correo electrónico al propietario del blog.
- Registrar la acción por seguridad.
Sin eventos, todo esto se amontonarían en una sola función. Con eventos, puedes dividirlo: una parte guarda el comentario, otra activa un evento como 'comment.posted', y oyentes separados manejan el correo electrónico y el registro. Esto mantiene tu código más limpio y te permite agregar o eliminar características (como notificaciones) sin tocar la lógica principal.
Casos de Uso Comunes
En la mayoría de los casos, los eventos son buenos para cosas que son opcionales, pero no una parte absolutamente central de tu sistema. Por ejemplo, lo siguiente es bueno tenerlo, pero si fallan por alguna razón, tu aplicación debería seguir funcionando:
- Registro: Registrar acciones como inicios de sesión o errores sin ensuciar tu código principal.
- Notificaciones: Enviar correos electrónicos o alertas cuando algo sucede.
- Actualizaciones de Caché: Refrescar cachés o notificar a otros sistemas sobre cambios.
Sin embargo, supongamos que tienes una función de contraseña olvidada. Eso debería ser parte de tu funcionalidad principal y no un evento porque si ese correo no se envía, tu usuario no puede restablecer su contraseña y usar tu aplicación.
Uso Básico
El sistema de eventos de Flight se construye alrededor de dos métodos principales: Flight::onEvent() para registrar oyentes de eventos y Flight::triggerEvent() para activar eventos. Aquí te explico cómo usarlos:
Registrando Oyentes de Eventos
Para escuchar un evento, usa Flight::onEvent(). Este método te permite definir qué debería suceder cuando ocurre un evento.
Flight::onEvent(string $event, callable $callback): void
$event: Un nombre para tu evento (por ejemplo,'user.login').$callback: La función a ejecutar cuando se active el evento.
Te "suscribes" a un evento diciéndole a Flight qué hacer cuando suceda. El callback puede aceptar argumentos pasados desde la activación del evento.
El sistema de eventos de Flight es síncrono, lo que significa que cada oyente de evento se ejecuta en secuencia, uno después del otro. Cuando activas un evento, todos los oyentes registrados para ese evento se ejecutarán hasta completarse antes de que tu código continúe. Esto es importante entenderlo ya que difiere de los sistemas de eventos asíncronos donde los oyentes podrían ejecutarse en paralelo o en un momento posterior.
Ejemplo Simple
Flight::onEvent('user.login', function ($username) {
echo "¡Bienvenido de nuevo, $username!";
// puedes enviar un correo si el inicio de sesión es desde una nueva ubicación
});
Aquí, cuando se active el evento 'user.login', saludará al usuario por su nombre y podría incluir lógica para enviar un correo si es necesario.
Nota: El callback puede ser una función, una función anónima o un método de una clase.
Activando Eventos
Para hacer que un evento suceda, usa Flight::triggerEvent(). Esto le dice a Flight que ejecute todos los oyentes registrados para ese evento, pasando cualquier dato que proporciones.
Flight::triggerEvent(string $event, ...$args): void
$event: El nombre del evento que estás activando (debe coincidir con un evento registrado)....$args: Argumentos opcionales para enviar a los oyentes (puede ser cualquier número de argumentos).
Ejemplo Simple
$username = 'alice';
Flight::triggerEvent('user.login', $username);
Esto activa el evento 'user.login' y envía 'alice' al oyente que definimos anteriormente, lo que producirá: ¡Bienvenido de nuevo, alice!.
- Si no hay oyentes registrados, no pasa nada—tu app no se romperá.
- Usa el operador de propagación (
...) para pasar múltiples argumentos de manera flexible.
Deteniendo Eventos
Si un oyente devuelve false, no se ejecutarán oyentes adicionales para ese evento. Esto te permite detener la cadena de eventos basada en condiciones específicas. Recuerda, el orden de los oyentes importa, ya que el primero en devolver false detendrá el resto de la ejecución.
Ejemplo:
Flight::onEvent('user.login', function ($username) {
if (isBanned($username)) {
logoutUser($username);
return false; // Detiene oyentes subsiguientes
}
});
Flight::onEvent('user.login', function ($username) {
sendWelcomeEmail($username); // esto nunca se envía
});
Sobrescribiendo Métodos de Eventos
Flight::onEvent() y Flight::triggerEvent() están disponibles para ser extendidos, lo que significa que puedes redefinir cómo funcionan. Esto es genial para usuarios avanzados que quieran personalizar el sistema de eventos, como agregar registro o cambiar cómo se despachan los eventos.
Ejemplo: Personalizando onEvent
Flight::map('onEvent', function (string $event, callable $callback) {
// Registrar cada registro de evento
error_log("Nuevo oyente de evento agregado para: $event");
// Llamar al comportamiento predeterminado (asumiendo un sistema de eventos interno)
Flight::_onEvent($event, $callback);
});
Ahora, cada vez que registres un evento, se registrará antes de proceder.
¿Por Qué Sobrescribir?
- Agregar depuración o monitoreo.
- Restringir eventos en ciertos entornos (por ejemplo, deshabilitar en pruebas).
- Integrar con una biblioteca de eventos diferente.
Dónde Poner Tus Eventos
Si eres nuevo en los conceptos de eventos en tu proyecto, podrías preguntarte: ¿dónde registro todos estos eventos en mi app? La simplicidad de Flight significa que no hay una regla estricta—puedes ponerlos donde tenga sentido para tu proyecto. Sin embargo, mantenerlos organizados te ayuda a mantener tu código a medida que tu app crece. Aquí hay algunas opciones prácticas y mejores prácticas, adaptadas a la naturaleza ligera de Flight:
Opción 1: En Tu Archivo Principal index.php
Para apps pequeñas o prototipos rápidos, puedes registrar eventos directamente en tu archivo index.php junto con tus rutas. Esto mantiene todo en un solo lugar, lo cual está bien cuando la simplicidad es tu prioridad.
require 'vendor/autoload.php';
// Registrar eventos
Flight::onEvent('user.login', function ($username) {
error_log("$username logged in at " . date('Y-m-d H:i:s'));
});
// Definir rutas
Flight::route('/login', function () {
$username = 'bob';
Flight::triggerEvent('user.login', $username);
echo "Logged in!";
});
Flight::start();
- Pros: Simple, sin archivos extra, genial para proyectos pequeños.
- Cons: Puede volverse desordenado a medida que tu app crece con más eventos y rutas.
Opción 2: Un Archivo Separado events.php
Para una app un poco más grande, considera mover los registros de eventos a un archivo dedicado como app/config/events.php. Incluye este archivo en tu index.php antes de tus rutas. Esto imita cómo se organizan a menudo las rutas en app/config/routes.php en proyectos de 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();
- Pros: Mantiene
index.phpenfocado en el enrutamiento, organiza los eventos lógicamente, fácil de encontrar y editar. - Cons: Agrega un poco de estructura, lo que podría parecer excesivo para apps muy pequeñas.
Opción 3: Cerca de Dónde Se Activan
Otro enfoque es registrar eventos cerca de donde se activan, como dentro de un controlador o definición de ruta. Esto funciona bien si un evento es específico de una parte de tu app.
Flight::route('/signup', function () {
// Registrar evento aquí
Flight::onEvent('user.registered', function ($email) {
echo "Welcome email sent to $email!";
});
$email = 'jane@example.com';
Flight::triggerEvent('user.registered', $email);
echo "Signed up!";
});
- Pros: Mantiene el código relacionado junto, bueno para características aisladas.
- Cons: Dispersa los registros de eventos, haciendo más difícil ver todos los eventos de una vez; riesgo de registros duplicados si no se tiene cuidado.
Mejor Práctica para Flight
- Empieza Simple: Para apps pequeñas, pon los eventos en
index.php. Es rápido y se alinea con el minimalismo de Flight. - Crece Inteligente: A medida que tu app se expande (por ejemplo, más de 5-10 eventos), usa un archivo
app/config/events.php. Es un paso natural, como organizar rutas, y mantiene tu código ordenado sin agregar marcos complejos. - Evita la Sobreingeniería: No crees una clase o directorio "gestor de eventos" completo a menos que tu app sea enorme—Flight prospera en la simplicidad, así que manténlo ligero.
Consejo: Agrupa por Propósito
En events.php, agrupa eventos relacionados (por ejemplo, todos los eventos relacionados con usuarios juntos) con comentarios para mayor claridad:
// app/config/events.php
// Eventos de Usuario
Flight::onEvent('user.login', function ($username) {
error_log("$username logged in");
});
Flight::onEvent('user.registered', function ($email) {
echo "Welcome to $email!";
});
// Eventos de Página
Flight::onEvent('page.updated', function ($pageId) {
Flight::cache()->delete("page_$pageId");
});
Esta estructura escala bien y se mantiene amigable para principiantes.
Ejemplos del Mundo Real
Vamos a recorrer algunos escenarios del mundo real para mostrar cómo funcionan los eventos y por qué son útiles.
Ejemplo 1: Registrando un Inicio de Sesión de Usuario
// Paso 1: Registrar un oyente
Flight::onEvent('user.login', function ($username) {
$time = date('Y-m-d H:i:s');
error_log("$username logged in at $time");
});
// Paso 2: Activarlo en tu app
Flight::route('/login', function () {
$username = 'bob'; // Pretende que esto viene de un formulario
Flight::triggerEvent('user.login', $username);
echo "Hi, $username!";
});
Por Qué Es Útil: El código de inicio de sesión no necesita saber sobre el registro—solo activa el evento. Puedes agregar más oyentes después (por ejemplo, enviar un correo de bienvenida) sin cambiar la ruta.
Ejemplo 2: Notificando Sobre Nuevos Usuarios
// Oyente para nuevos registros
Flight::onEvent('user.registered', function ($email, $name) {
// Simular envío de correo
echo "Email sent to $email: Welcome, $name!";
});
// Activar cuando alguien se registra
Flight::route('/signup', function () {
$email = 'jane@example.com';
$name = 'Jane';
Flight::triggerEvent('user.registered', $email, $name);
echo "Thanks for signing up!";
});
Por Qué Es Útil: La lógica de registro se enfoca en crear el usuario, mientras que el evento maneja las notificaciones. Podrías agregar más oyentes (por ejemplo, registrar el registro) después.
Ejemplo 3: Limpiando un Caché
// Oyente para limpiar un caché
Flight::onEvent('page.updated', function ($pageId) {
// si usas el plugin flightphp/cache
Flight::cache()->delete("page_$pageId");
echo "Cache cleared for page $pageId.";
});
// Activar cuando se edita una página
Flight::route('/edit-page/(@id)', function ($pageId) {
// Pretende que actualizamos la página
Flight::triggerEvent('page.updated', $pageId);
echo "Page $pageId updated.";
});
Por Qué Es Útil: El código de edición no se preocupa por el caché—solo señala la actualización. Otras partes de la app pueden reaccionar según sea necesario.
Mejores Prácticas
- Nombra Eventos Claramente: Usa nombres específicos como
'user.login'o'page.updated'para que sea obvio qué hacen. - Mantén Oyentes Simples: No pongas tareas lentas o complejas en oyentes—mantén tu app rápida.
- Prueba Tus Eventos: Actívalos manualmente para asegurar que los oyentes funcionen como se espera.
- Usa Eventos con Sabiduría: Son geniales para desacoplar, pero demasiados pueden hacer que tu código sea difícil de seguir—úsalos cuando tenga sentido.
El sistema de eventos en Flight PHP, con Flight::onEvent() y Flight::triggerEvent(), te da una manera simple pero poderosa de construir aplicaciones flexibles. Al permitir que diferentes partes de tu app se comuniquen entre sí a través de eventos, puedes mantener tu código organizado, reutilizable y fácil de expandir. Ya sea que estés registrando acciones, enviando notificaciones o gestionando actualizaciones, los eventos te ayudan a hacerlo sin enredar tu lógica. Además, con la capacidad de sobrescribir estos métodos, tienes la libertad de adaptar el sistema a tus necesidades. Empieza pequeño con un solo evento y observa cómo transforma la estructura de tu app!
Eventos Integrados
Flight PHP viene con algunos eventos integrados que puedes usar para engancharte en el ciclo de vida del framework. Estos eventos se activan en puntos específicos del ciclo de solicitud/respuesta, permitiéndote ejecutar lógica personalizada cuando ocurren ciertas acciones.
Lista de Eventos Integrados
- flight.request.received:
function(Request $request)Activado cuando se recibe, analiza y procesa una solicitud. - flight.error:
function(Throwable $exception)Activado cuando ocurre un error durante el ciclo de vida de la solicitud. - flight.redirect:
function(string $url, int $status_code)Activado cuando se inicia una redirección. - flight.cache.checked:
function(string $cache_key, bool $hit, float $executionTime)Activado cuando se verifica el caché para una clave específica y si hubo acierto o fallo en el caché. - flight.middleware.before:
function(Route $route)Activado después de que se ejecute el middleware before. - flight.middleware.after:
function(Route $route)Activado después de que se ejecute el middleware after. - flight.middleware.executed:
function(Route $route, $middleware, string $method, float $executionTime)Activado después de que se ejecute cualquier middleware. - flight.route.matched:
function(Route $route)Activado cuando se coincide una ruta, pero aún no se ejecuta. - flight.route.executed:
function(Route $route, float $executionTime)Activado después de que se ejecute y procese una ruta.$executionTimees el tiempo que tomó ejecutar la ruta (llamar al controlador, etc.). - flight.view.rendered:
function(string $template_file_path, float $executionTime)Activado después de que se renderice una vista.$executionTimees el tiempo que tomó renderizar la plantilla. Nota: Si sobrescribes el métodorender, necesitarás reactivar este evento. - flight.response.sent:
function(Response $response, float $executionTime)Activado después de que se envíe una respuesta al cliente.$executionTimees el tiempo que tomó construir la respuesta.
Ver También
- Extending Flight - Cómo extender y personalizar la funcionalidad principal de Flight.
- Cache - Ejemplo de usar eventos para limpiar el caché cuando se actualiza una página.
Solución de Problemas
- Si no ves que se llamen tus oyentes de eventos, asegúrate de registrarlos antes de activar los eventos. El orden de registro importa.
Registro de Cambios
- v3.15.0 - Agregados eventos a Flight.
Learn/templates
# Vistas HTML y Plantillas
## Resumen
Flight proporciona algunas funcionalidades básicas de plantillas HTML por defecto. El uso de plantillas es una forma muy efectiva de separar la lógica de tu aplicación de la capa de presentación. Un motor dedicado (Twig, Latte, etc.) también brinda a las [herramientas de codificación IA](/learn/ai) una sintaxis familiar y restringida, por lo que es menos probable que vuelquen lógica de negocio en tu HTML.
## Comprensión
Cuando estás construyendo una aplicación, es probable que tengas HTML que quieras devolver al usuario final. PHP por sí mismo es un lenguaje de plantillas, pero es _muy_ fácil mezclar lógica de negocio como llamadas a bases de datos, llamadas a API, etc., dentro de tu archivo HTML y hacer que las pruebas y el desacoplamiento sean un proceso muy difícil. Al empujar los datos hacia una plantilla y permitir que la plantilla se renderice sola, resulta mucho más fácil desacoplar y probar unitariamente tu código. ¡Nos lo agradecerás si usas plantillas!
## Uso Básico
Flight te permite cambiar el motor de vistas predeterminado simplemente mapeando `render` (o registrando una clase de vista). Desplázate hacia abajo para ver Twig, Latte, Smarty, Blade y más.
> **Predeterminado del skeleton:** El [flightphp/skeleton](https://github.com/flightphp/skeleton) oficial usa **solo Twig** en `app/views/` (`*.twig`). Los controladores llaman a `$this->app->render('welcome', $data)` (extensión opcional). Esa es una elección de la aplicación para proyectos nuevos, no un requisito del núcleo de Flight. Latte y otros motores siguen siendo totalmente compatibles.
### Twig
<span class="badge bg-info">predeterminado del skeleton</span>
[Twig](https://twig.symfony.com/) es un motor de plantillas flexible, rápido y seguro utilizado por Symfony y muchos otros proyectos PHP. Las herramientas de codificación IA tienden a conocer muy bien Twig, y además escapa la salida automáticamente por defecto, lo que ayuda a proteger contra XSS.
#### Instalación
```bash
composer require twig/twig
(Ya incluido cuando ejecutas composer create-project flightphp/skeleton.)
Configuración Básica
Sobrescribe el método render para usar Twig en lugar del renderizador PHP predeterminado:
// sobrescribe el método render para usar Twig en lugar del renderizador PHP predeterminado
Flight::map('render', function(string $template, array $data): void {
$loader = new \Twig\Loader\FilesystemLoader(Flight::get('flight.views.path'));
$twig = new \Twig\Environment($loader, [
// Donde Twig almacena sus plantillas compiladas
'cache' => __DIR__ . '/../cache/twig',
'auto_reload' => true,
]);
// Permitir "welcome" o "welcome.twig"
if (substr($template, -5) !== '.twig') {
$template .= '.twig';
}
echo $twig->render($template, $data);
});
En el skeleton, esta configuración se encuentra en app/config/services.php (entorno Twig compartido, ruta de caché, globales como base_url / nonce CSP). Prefiere inyectar Engine y llamar a $app->render() desde los controladores para que el código siga siendo amigable con la IA y con las pruebas.
Usando Twig en Flight
Ahora que puedes renderizar con Twig, puedes hacer algo como esto:
{# 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
]);
});
Cuando visitas /Bob en tu navegador, la salida sería:
<html>
<head>
<title>Home Page - My App</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Hello, Bob!</h1>
</body>
</html>
Lectura Adicional
Un ejemplo más completo del uso de Twig con diseños (layouts) se muestra en la sección plugins asombrosos de esta documentación. Para métricas de tiempo de renderizado en la barra de Tracy, consulta el panel de Twig en Tracy Extensions.
Puedes aprender más sobre todas las capacidades de Twig leyendo la documentación oficial.
Latte
gran alternativa
Latte es un motor completo con una sintaxis similar a PHP. Sigue siendo una excelente opción para aplicaciones Flight; el skeleton simplemente estandariza Twig como un solo predeterminado compartido (especialmente útil cuando las herramientas de IA generan plantillas).
Instalación
composer require latte/latte
Configuración Básica
La idea principal es sobrescribir el método render para usar Latte en lugar del renderizador PHP predeterminado.
// sobrescribe el método render para usar Latte en lugar del renderizador PHP predeterminado
Flight::map('render', function(string $template, array $data, ?string $block): void {
$latte = new Latte\Engine;
// Donde Latte almacena específicamente su caché
$latte->setTempDirectory(__DIR__ . '/../cache/');
$finalPath = Flight::get('flight.views.path') . $template;
$latte->render($finalPath, $data, $block);
});
Usando Latte en Flight
Ahora que puedes renderizar con Latte, puedes hacer algo como esto:
<!-- 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
]);
});
Cuando visitas /Bob en tu navegador, la salida sería:
<html>
<head>
<title>Home Page - My App</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Hello, Bob!</h1>
</body>
</html>
Lectura Adicional
Un ejemplo más complejo del uso de Latte con diseños (layouts) se muestra en la sección plugins asombrosos de esta documentación.
Puedes aprender más sobre todas las capacidades de Latte, incluyendo las capacidades de traducción e idiomas, leyendo la documentación oficial.
Motor de Vistas Integrado
obsoleto
Nota: Aunque sigue siendo la funcionalidad predeterminada y todavía funciona técnicamente.
Para mostrar una plantilla de vista, llama al método render con el nombre del archivo de plantilla y datos opcionales de la plantilla:
Flight::render('hello.php', ['name' => 'Bob']);
Los datos de la plantilla que pasas se inyectan automáticamente en la plantilla y se pueden referenciar como una variable local. Los archivos de plantilla son simplemente archivos PHP. Si el contenido del archivo de plantilla hello.php es:
Hello, <?= $name ?>!
La salida sería:
Hello, Bob!
También puedes establecer manualmente variables de vista usando el método set:
Flight::view()->set('name', 'Bob');
La variable name ahora está disponible en todas tus vistas. Así que simplemente puedes hacer:
Flight::render('hello');
Ten en cuenta que al especificar el nombre de la plantilla en el método render, puedes omitir la extensión .php.
Por defecto, Flight buscará un directorio views para los archivos de plantilla. Puedes establecer una ruta alternativa para tus plantillas configurando lo siguiente:
Flight::set('flight.views.path', '/path/to/views');
Diseños (Layouts)
Es común que los sitios web tengan un único archivo de plantilla de diseño con contenido intercambiable. Para renderizar contenido que se usará en un diseño, puedes pasar un parámetro opcional al método render.
Flight::render('header', ['heading' => 'Hello'], 'headerContent');
Flight::render('body', ['body' => 'World'], 'bodyContent');
Tu vista tendrá entonces variables guardadas llamadas headerContent y bodyContent. Luego puedes renderizar tu diseño haciendo:
Flight::render('layout', ['title' => 'Home Page']);
Si los archivos de plantilla se ven así:
header.php:
<h1><?= $heading ?></h1>
body.php:
<div><?= $body ?></div>
layout.php:
<html>
<head>
<title><?= $title ?></title>
</head>
<body>
<?= $headerContent ?>
<?= $bodyContent ?>
</body>
</html>
La salida sería:
<html>
<head>
<title>Home Page</title>
</head>
<body>
<h1>Hello</h1>
<div>World</div>
</body>
</html>
Smarty
Así es como usarías el motor de plantillas Smarty para tus vistas:
// Cargar la librería Smarty
require './Smarty/libs/Smarty.class.php';
// Registrar Smarty como la clase de vista
// También pasar una función de devolución de llamada para configurar Smarty al cargar
Flight::register('view', Smarty::class, [], function (Smarty $smarty) {
$smarty->setTemplateDir('./templates/');
$smarty->setCompileDir('./templates_c/');
$smarty->setConfigDir('./config/');
$smarty->setCacheDir('./cache/');
});
// Asignar datos de plantilla
Flight::view()->assign('name', 'Bob');
// Mostrar la plantilla
Flight::view()->display('hello.tpl');
Para completar, también deberías sobrescribir el método predeterminado de renderizado de Flight:
Flight::map('render', function(string $template, array $data): void {
Flight::view()->assign($data);
Flight::view()->display($template);
});
Blade
Así es como usarías el motor de plantillas Blade para tus vistas:
Primero, necesitas instalar la librería BladeOne mediante Composer:
composer require eftec/bladeone
Luego, puedes configurar BladeOne como la clase de vista en Flight:
<?php
// Cargar la librería BladeOne
use eftec\bladeone\BladeOne;
// Registrar BladeOne como la clase de vista
// También pasar una función de devolución de llamada para configurar BladeOne al cargar
Flight::register('view', BladeOne::class, [], function (BladeOne $blade) {
$views = __DIR__ . '/../views';
$cache = __DIR__ . '/../cache';
$blade->setPath($views);
$blade->setCompiledPath($cache);
});
// Asignar datos de plantilla
Flight::view()->share('name', 'Bob');
// Mostrar la plantilla
echo Flight::view()->run('hello', []);
Para completar, también deberías sobrescribir el método predeterminado de renderizado de Flight:
<?php
Flight::map('render', function(string $template, array $data): void {
echo Flight::view()->run($template, $data);
});
En este ejemplo, el archivo de plantilla hello.blade.php podría verse así:
<?php
Hello, {{ $name }}!
La salida sería:
Hello, Bob!
Ver También
- Instalación - Estructura del skeleton (
app/views/*.twig) para proyectos nuevos. - Extensión - Cómo sobrescribir el método
renderpara usar un motor de plantillas diferente. - Enrutamiento - Cómo mapear rutas a controladores y renderizar vistas.
- Respuestas - Cómo personalizar las respuestas HTTP.
- Seguridad - Auto-escape y XSS.
- IA y Experiencia de Desarrollo - Por qué un motor de vistas predeterminado ayuda a los agentes de codificación.
- ¿Por qué un Framework? - Cómo encajan las plantillas en el panorama general.
Solución de Problemas
- Si tienes una redirección en tu middleware, pero tu aplicación no parece redirigir, asegúrate de agregar una declaración
exit;en tu middleware. - Si Twig no puede encontrar una plantilla, verifica
flight.views.pathy que el archivo exista en esa ruta con la extensión esperada (skeleton:app/views/).
Historial de Cambios
- Docs – Twig documentado como el predeterminado oficial del skeleton; Latte sigue siendo una alternativa de primera clase.
- v2.0 - Versión inicial.
Learn/simple_pdo
Clase Auxiliar SimplePdo para PDO
Resumen
La clase SimplePdo en Flight es un auxiliar moderno y rico en funciones para trabajar con bases de datos usando PDO. Extiende PdoWrapper y agrega métodos auxiliares convenientes para operaciones comunes de base de datos como insert(), update(), delete() y transacciones. Simplifica las tareas de base de datos, devuelve resultados como Collections para un acceso fácil, y soporta registro de consultas y monitoreo de rendimiento de aplicaciones (APM) para casos de uso avanzados.
Comprensión
La clase SimplePdo está diseñada para hacer que trabajar con bases de datos en PHP sea mucho más fácil. En lugar de manejar declaraciones preparadas, modos de obtención y operaciones SQL verbosas, obtienes métodos limpios y simples para tareas comunes. Cada fila se devuelve como una Collection, por lo que puedes usar tanto notación de arreglo ($row['name']) como notación de objeto ($row->name).
Esta clase es un superconjunto de PdoWrapper, lo que significa que incluye toda la funcionalidad de PdoWrapper más métodos auxiliares adicionales que hacen que tu código sea más limpio y mantenible. Si estás usando actualmente PdoWrapper, actualizar a SimplePdo es directo ya que extiende PdoWrapper.
Puedes registrar SimplePdo como un servicio compartido en Flight, y luego usarlo en cualquier lugar de tu aplicación a través de Flight::db().
Uso Básico
Registro de SimplePdo
Primero, registra la clase SimplePdo con 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
]
]);
NOTA
Si no especificas
PDO::ATTR_DEFAULT_FETCH_MODE,SimplePdolo establecerá automáticamente enPDO::FETCH_ASSOCpor ti.
Ahora puedes usar Flight::db() en cualquier lugar para obtener tu conexión a la base de datos.
Ejecución de Consultas
runQuery()
function runQuery(string $sql, array $params = []): PDOStatement
Usa esto para INSERT, UPDATE, o cuando quieras obtener resultados manualmente:
$db = Flight::db();
$statement = $db->runQuery("SELECT * FROM users WHERE status = ?", ['active']);
while ($row = $statement->fetch()) {
// $row es un arreglo
}
También puedes usarlo para escrituras:
$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
Obtén un solo valor de la base de datos:
$count = Flight::db()->fetchField("SELECT COUNT(*) FROM users WHERE status = ?", ['active']);
fetchRow()
function fetchRow(string $sql, array $params = []): ?Collection
Obtén una sola fila como una Collection (acceso a arreglo/objeto):
$user = Flight::db()->fetchRow("SELECT * FROM users WHERE id = ?", [123]);
echo $user['name'];
// o
echo $user->name;
CONSEJO
SimplePdoagrega automáticamenteLIMIT 1a las consultas defetchRow()si no está presente, haciendo que tus consultas sean más eficientes.
fetchAll()
function fetchAll(string $sql, array $params = []): array<Collection>
Obtén todas las filas como un arreglo de Collections:
$users = Flight::db()->fetchAll("SELECT * FROM users WHERE status = ?", ['active']);
foreach ($users as $user) {
echo $user['name'];
// o
echo $user->name;
}
fetchColumn()
function fetchColumn(string $sql, array $params = []): array
Obtén una sola columna como un arreglo:
$ids = Flight::db()->fetchColumn("SELECT id FROM users WHERE active = ?", [1]);
// Devuelve: [1, 2, 3, 4, 5]
fetchPairs()
function fetchPairs(string $sql, array $params = []): array
Obtén resultados como pares clave-valor (primera columna como clave, segunda como valor):
$userNames = Flight::db()->fetchPairs("SELECT id, name FROM users");
// Devuelve: [1 => 'John', 2 => 'Jane', 3 => 'Bob']
Usando Marcadores de Posición IN()
Puedes usar un solo ? en una cláusula IN() y pasar un arreglo:
$ids = [1, 2, 3];
$users = Flight::db()->fetchAll("SELECT * FROM users WHERE id IN (?)", [$ids]);
Métodos Auxiliares
Una de las principales ventajas de SimplePdo sobre PdoWrapper es la adición de métodos auxiliares convenientes para operaciones comunes de base de datos.
insert()
function insert(string $table, array $data): string
Inserta una o más filas y devuelve el último ID de inserción.
Inserción única:
$id = Flight::db()->insert('users', [
'name' => 'John',
'email' => 'john@example.com'
]);
Inserción masiva:
$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
Actualiza filas y devuelve el número de filas afectadas:
$affected = Flight::db()->update(
'users',
['name' => 'Jane', 'email' => 'jane@example.com'],
'id = ?',
[1]
);
NOTA
El
rowCount()de SQLite devuelve el número de filas donde los datos realmente cambiaron. Si actualizas una fila con los mismos valores que ya tiene,rowCount()devolverá 0. Esto difiere del comportamiento de MySQL cuando se usaPDO::MYSQL_ATTR_FOUND_ROWS.
delete()
function delete(string $table, string $where, array $whereParams = []): int
Elimina filas y devuelve el número de filas eliminadas:
$deleted = Flight::db()->delete('users', 'id = ?', [1]);
transaction()
function transaction(callable $callback): mixed
Ejecuta un callback dentro de una transacción. La transacción se confirma automáticamente en caso de éxito o se revierte en caso de error:
$result = Flight::db()->transaction(function($db) {
$db->insert('users', ['name' => 'John']);
$db->insert('logs', ['action' => 'user_created']);
return $db->lastInsertId();
});
Si se lanza alguna excepción dentro del callback, la transacción se revierte automáticamente y la excepción se relanza.
Uso Avanzado
Registro de Consultas y APM
Si quieres rastrear el rendimiento de las consultas, habilita el seguimiento de APM al registrar:
Flight::register('db', \flight\database\SimplePdo::class, [
'mysql:host=localhost;dbname=cool_db_name',
'user',
'pass',
[/* opciones de PDO */],
[
'trackApmQueries' => true,
'maxQueryMetrics' => 1000
]
]);
Después de ejecutar consultas, puedes registrarlas manualmente, pero el APM las registrará automáticamente si está habilitado:
Flight::db()->logQueries();
Esto activará un evento (flight.db.queries) con métricas de conexión y consultas, que puedes escuchar usando el sistema de eventos de Flight.
Ejemplo Completo
Flight::route('/users', function () {
// Obtener todos los usuarios
$users = Flight::db()->fetchAll('SELECT * FROM users');
// Transmitir todos los usuarios
$statement = Flight::db()->runQuery('SELECT * FROM users');
while ($user = $statement->fetch()) {
echo $user['name'];
}
// Obtener un solo usuario
$user = Flight::db()->fetchRow('SELECT * FROM users WHERE id = ?', [123]);
// Obtener un solo valor
$count = Flight::db()->fetchField('SELECT COUNT(*) FROM users');
// Obtener una sola columna
$ids = Flight::db()->fetchColumn('SELECT id FROM users');
// Obtener pares clave-valor
$userNames = Flight::db()->fetchPairs('SELECT id, name FROM users');
// Sintaxis especial IN()
$users = Flight::db()->fetchAll('SELECT * FROM users WHERE id IN (?)', [[1,2,3,4,5]]);
// Insertar un nuevo usuario
$id = Flight::db()->insert('users', [
'name' => 'Bob',
'email' => 'bob@example.com'
]);
// Inserción masiva de usuarios
Flight::db()->insert('users', [
['name' => 'Bob', 'email' => 'bob@example.com'],
['name' => 'Jane', 'email' => 'jane@example.com']
]);
// Actualizar un usuario
$affected = Flight::db()->update('users', ['name' => 'Bob'], 'id = ?', [123]);
// Eliminar un usuario
$deleted = Flight::db()->delete('users', 'id = ?', [123]);
// Usar una transacción
$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();
});
});
Migración desde PdoWrapper
Si estás usando actualmente PdoWrapper, migrar a SimplePdo es directo:
-
Actualiza tu registro:
// Antiguo Flight::register('db', \flight\database\PdoWrapper::class, [ /* ... */ ]); // Nuevo Flight::register('db', \flight\database\SimplePdo::class, [ /* ... */ ]); -
Todos los métodos existentes de
PdoWrapperfuncionan enSimplePdo- No hay cambios que rompan la compatibilidad. Tu código existente continuará funcionando. -
Opcionalmente usa los nuevos métodos auxiliares - Comienza a usar
insert(),update(),delete()ytransaction()para simplificar tu código.
Ver También
- Collections - Aprende cómo usar la clase Collection para un acceso fácil a los datos.
- PdoWrapper - La clase auxiliar PDO legacy (deprecada).
Solución de Problemas
- Si obtienes un error sobre la conexión a la base de datos, verifica tu DSN, nombre de usuario, contraseña y opciones.
- Todas las filas se devuelven como Collections—si necesitas un arreglo plano, usa
$collection->getData(). - Para consultas
IN (?), asegúrate de pasar un arreglo. - Si estás experimentando problemas de memoria con el registro de consultas en procesos de larga duración, ajusta la opción
maxQueryMetrics.
Registro de Cambios
- v3.18.0 - Lanzamiento inicial de SimplePdo con métodos auxiliares para insert, update, delete y transacciones.
Learn/collections
Colecciones
Resumen
La clase Collection en Flight es una utilidad práctica para gestionar conjuntos de datos. Te permite acceder y manipular datos usando tanto notación de array como de objeto, haciendo tu código más limpio y flexible.
Entendiendo
Un Collection es básicamente un envoltorio alrededor de un array, pero con poderes adicionales. Puedes usarlo como un array, recorrerlo, contar sus elementos e incluso acceder a ellos como si fueran propiedades de objeto. Esto es especialmente útil cuando deseas pasar datos estructurados en tu aplicación, o cuando quieres que tu código sea un poco más legible.
Las colecciones implementan varias interfaces de PHP:
ArrayAccess(para que puedas usar sintaxis de array)Iterator(para que puedas iterar conforeach)Countable(para que puedas usarcount())JsonSerializable(para que puedas convertir fácilmente a JSON)
Uso Básico
Creando una Colección
Puedes crear una colección simplemente pasando un array a su constructor:
use flight\util\Collection;
$data = [
'name' => 'Flight',
'version' => 3,
'features' => ['routing', 'views', 'extending']
];
$collection = new Collection($data);
Accediendo a los Elementos
Puedes acceder a los elementos usando la notación de array o de objeto:
// Notación de array
echo $collection['name']; // Salida: FlightPHP
// Notación de objeto
echo $collection->version; // Salida: 3
Si intentas acceder a una clave que no existe, obtendrás null en lugar de un error.
Estableciendo Elementos
Puedes establecer elementos usando cualquiera de las dos notaciones también:
// Notación de array
$collection['author'] = 'Mike Cao';
// Notación de objeto
$collection->license = 'MIT';
Comprobando y Eliminando Elementos
Comprueba si un elemento existe:
if (isset($collection['name'])) {
// Haz algo
}
if (isset($collection->version)) {
// Haz algo
}
Elimina un elemento:
unset($collection['author']);
unset($collection->license);
Iterando Sobre una Colección
Las colecciones son iterables, por lo que puedes usarlas en un bucle foreach:
foreach ($collection as $key => $value) {
echo "$key: $value\n";
}
Contando Elementos
Puedes contar el número de elementos en una colección:
echo count($collection); // Salida: 4
Obteniendo Todas las Claves o Datos
Obtén todas las claves:
$keys = $collection->keys(); // ['name', 'version', 'features', 'license']
Obtén todos los datos como un array:
$data = $collection->getData();
Limpiando la Colección
Elimina todos los elementos:
$collection->clear();
Serialización JSON
Las colecciones pueden convertirse fácilmente a JSON:
echo json_encode($collection);
// Salida: {"name":"FlightPHP","version":3,"features":["routing","views","extending"],"license":"MIT"}
Uso Avanzado
Puedes reemplazar el array de datos interno por completo si es necesario:
$collection->setData(['foo' => 'bar']);
Las colecciones son especialmente útiles cuando deseas pasar datos estructurados entre componentes, o cuando quieres proporcionar una interfaz más orientada a objetos para los datos de array.
Ver También
- Solicitudes - Aprende cómo manejar solicitudes HTTP y cómo las colecciones pueden usarse para gestionar datos de solicitudes.
- SimplePdo - Ayudante de base de datos que devuelve filas de consultas como colecciones.
Solución de Problemas
- Si intentas acceder a una clave que no existe, obtendrás
nullen lugar de un error. - Recuerda que las colecciones no son recursivas: los arrays anidados no se convierten automáticamente en colecciones.
- Si necesitas restablecer la colección, usa
$collection->clear()o$collection->setData([]).
Historial de Cambios
- v3.0 - Mejora de los type hints y soporte para PHP 8+.
- v1.0 - Lanzamiento inicial de la clase Collection.
Learn/flight_vs_fat_free
Flight vs Fat-Free
¿Qué es Fat-Free?
Fat-Free (conocido cariñosamente como F3) es un micro-framework de PHP potente pero fácil de usar, diseñado para ayudarte a crear aplicaciones web dinámicas y robustas, ¡rápidamente!
Flight se compara con Fat-Free en muchos aspectos y probablemente sea el primo más cercano en términos de características y simplicidad. Fat-Free tiene muchas características que Flight no tiene, pero también tiene muchas características que Flight sí tiene. Fat-Free está empezando a mostrar su edad y ya no es tan popular como solía ser.
Las actualizaciones son cada vez menos frecuentes y la comunidad no está tan activa como antes. El código es bastante simple, pero a veces la falta de disciplina en la sintaxis puede dificultar su lectura y comprensión. Funciona para PHP 8.3, pero el código en sí todavía parece vivir en PHP 5.3.
Ventajas en comparación con Flight
- Fat-Free tiene unas pocas estrellas más en GitHub que Flight.
- Fat-Free tiene una documentación decente, pero carece de claridad en algunas áreas.
- Fat-Free tiene algunos recursos dispersos, como tutoriales de YouTube y artículos en línea, que se pueden usar para aprender el framework.
- Fat-Free tiene algunos plugins útiles integrados que a veces son de ayuda.
- Fat-Free tiene un ORM integrado llamado Mapper que se puede usar para interactuar con tu base de datos. Flight tiene active-record.
- Fat-Free tiene Sesiones, Caché y localización integrados. Flight requiere que uses bibliotecas de terceros, pero esto está cubierto en la documentación.
- Fat-Free tiene un pequeño grupo de plugins creados por la comunidad que se pueden usar para extender el framework. Flight tiene algunos cubiertos en las páginas de documentación y ejemplos.
- Fat-Free, al igual que Flight, no tiene dependencias.
- Fat-Free, al igual que Flight, está orientado a darle al desarrollador control sobre su aplicación y una experiencia de desarrollo simple.
- Fat-Free mantiene compatibilidad hacia atrás como lo hace Flight (en parte porque las actualizaciones son cada vez menos frecuentes).
- Fat-Free, al igual que Flight, está pensado para desarrolladores que se aventuran por primera vez en el mundo de los frameworks.
- Fat-Free tiene un motor de plantillas integrado que es más robusto que el motor de plantillas de Flight. Flight recomienda Latte para lograr esto.
- Fat-Free tiene un comando de tipo CLI 'route' único donde puedes crear aplicaciones CLI dentro del propio Fat-Free y tratarlo casi como una solicitud
GET. Flight logra esto con runway.
Desventajas en comparación con Flight
- Fat-Free tiene algunas pruebas de implementación e incluso tiene su propia clase test que es muy básica. Sin embargo, no está 100% probado unitariamente como lo está Flight.
- Tienes que usar un motor de búsqueda como Google para poder buscar en el sitio de documentación.
- Flight tiene modo oscuro en su sitio de documentación. (mic drop)
- Fat-Free tiene algunos módulos que están lamentablemente sin mantenimiento.
- Flight tiene SimplePdo para el acceso a bases de datos, que es un poco más simple que la clase
DB\SQLintegrada de Fat-Free (y preferido sobre el obsoleto PdoWrapper). - Flight tiene un plugin de permisos que se puede usar para asegurar tu aplicación. Fat-Free requiere que uses una biblioteca de terceros.
- Flight tiene un ORM llamado active-record que se siente más como un ORM que el Mapper de Fat-Free. El beneficio adicional de
active-recordes que puedes definir relaciones entre registros para uniones automáticas, mientras que el Mapper de Fat-Free requiere que crees vistas SQL. - Sorprendentemente, Fat-Free no tiene un namespace raíz. Flight está completamente namespaceado para no colisionar con tu propio código. La clase
Cachees la mayor infractora aquí. - Fat-Free no tiene middleware. En su lugar, hay ganchos
beforerouteyafterrouteque se pueden usar para filtrar solicitudes y respuestas en los controladores. - Fat-Free no puede agrupar rutas.
- Fat-Free tiene un manejador de contenedor de inyección de dependencias, pero la documentación es increíblemente escasa sobre cómo usarlo.
- La depuración puede volverse un poco complicada ya que básicamente todo se almacena en lo que se llama el
HIVE.
Learn/extending
Extendiendo
Resumen
Flight está diseñado para ser un framework extensible. El framework viene con un conjunto de métodos y componentes predeterminados, pero te permite mapear tus propios métodos, registrar tus propias clases o incluso sobrescribir clases y métodos existentes.
Entendiendo
Hay 2 formas en que puedes extender la funcionalidad de Flight:
- Mapeo de Métodos - Esto se usa para crear métodos personalizados simples que puedes llamar desde cualquier lugar de tu aplicación. Estos se usan típicamente para funciones de utilidad que quieres poder llamar desde cualquier parte de tu código.
- Registro de Clases - Esto se usa para registrar tus propias clases con Flight. Esto se usa típicamente para clases que tienen dependencias o requieren configuración.
También puedes sobrescribir métodos existentes del framework para alterar su comportamiento predeterminado y adaptarlo mejor a las necesidades de tu proyecto.
Si estás buscando un DIC (Contenedor de Inyección de Dependencias), ve a la página de Contenedor de Inyección de Dependencias.
Uso Básico
Sobrescribiendo Métodos del Framework
Flight te permite sobrescribir su funcionalidad predeterminada para adaptarla a tus propias necesidades, sin tener que modificar ningún código. Puedes ver todos los métodos que puedes sobrescribir a continuación.
Por ejemplo, cuando Flight no puede coincidir una URL con una ruta, invoca el método notFound
que envía una respuesta genérica HTTP 404. Puedes sobrescribir este comportamiento
usando el método map:
Flight::map('notFound', function() {
// Mostrar página personalizada 404
include 'errors/404.html';
});
Flight también te permite reemplazar componentes principales del framework. Por ejemplo, puedes reemplazar la clase Router predeterminada con tu propia clase personalizada:
// crear tu clase Router personalizada
class MyRouter extends \flight\net\Router {
// sobrescribir métodos aquí
// por ejemplo, un atajo para solicitudes GET para eliminar
// la característica de pasar ruta
public function get($pattern, $callback, $alias = '') {
return parent::get($pattern, $callback, false, $alias);
}
}
// Registrar tu clase personalizada
Flight::register('router', MyRouter::class);
// Cuando Flight carga la instancia de Router, cargará tu clase
$myRouter = Flight::router();
$myRouter->get('/hello', function() {
echo "Hello World!";
}, 'hello_alias');
Sin embargo, los métodos del framework como map y register no pueden ser sobrescritos. Obtendrás
un error si intentas hacerlo (nuevamente, ve a continuación para una lista de métodos).
Métodos del Framework Mapeables
A continuación se muestra el conjunto completo de métodos para el framework. Consiste en métodos principales, que son métodos estáticos regulares, y métodos extensibles, que son métodos mapeados que pueden ser filtrados o sobrescritos.
Métodos Principales
Estos métodos son principales para el framework y no pueden ser sobrescritos.
Flight::map(string $name, callable $callback, bool $pass_route = false) // Crea un método personalizado del framework.
Flight::register(string $name, string $class, array $params = [], ?callable $callback = null) // Registra una clase a un método del framework.
Flight::unregister(string $name) // Desregistra una clase de un método del framework.
Flight::before(string $name, callable $callback) // Agrega un filtro antes de un método del framework.
Flight::after(string $name, callable $callback) // Agrega un filtro después de un método del framework.
Flight::path(string $path) // Agrega una ruta para la carga automática de clases.
Flight::get(string $key) // Obtiene una variable establecida por Flight::set().
Flight::set(string $key, mixed $value) // Establece una variable dentro del motor de Flight.
Flight::has(string $key) // Verifica si una variable está establecida.
Flight::clear(array|string $key = []) // Limpia una variable.
Flight::init() // Inicializa el framework a sus configuraciones predeterminadas.
Flight::app() // Obtiene la instancia del objeto de aplicación
Flight::request() // Obtiene la instancia del objeto de solicitud
Flight::response() // Obtiene la instancia del objeto de respuesta
Flight::router() // Obtiene la instancia del objeto de enrutador
Flight::view() // Obtiene la instancia del objeto de vista
Métodos Extensibles
Flight::start() // Inicia el framework.
Flight::stop() // Detiene el framework y envía una respuesta.
Flight::halt(int $code = 200, string $message = '') // Detiene el framework con un código de estado y mensaje opcionales.
Flight::route(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') // Mapea un patrón de URL a un callback.
Flight::post(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') // Mapea un patrón de URL de solicitud POST a un callback.
Flight::put(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') // Mapea un patrón de URL de solicitud PUT a un callback.
Flight::patch(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') // Mapea un patrón de URL de solicitud PATCH a un callback.
Flight::delete(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') // Mapea un patrón de URL de solicitud DELETE a un callback.
Flight::group(string $pattern, callable $callback) // Crea agrupación para URLs, el patrón debe ser una cadena.
Flight::getUrl(string $name, array $params = []) // Genera una URL basada en un alias de ruta.
Flight::redirect(string $url, int $code) // Redirige a otra URL.
Flight::download(string $filePath) // Descarga un archivo.
Flight::render(string $file, array $data, ?string $key = null) // Renderiza un archivo de plantilla.
Flight::error(Throwable $error) // Envía una respuesta HTTP 500.
Flight::notFound() // Envía una respuesta HTTP 404.
Flight::etag(string $id, string $type = 'string') // Realiza caché HTTP ETag.
Flight::lastModified(int $time) // Realiza caché HTTP de última modificación.
Flight::json(mixed $data, int $code = 200, bool $encode = true, string $charset = 'utf8', int $option) // Envía una respuesta JSON.
Flight::jsonp(mixed $data, string $param = 'jsonp', int $code = 200, bool $encode = true, string $charset = 'utf8', int $option) // Envía una respuesta JSONP.
Flight::jsonHalt(mixed $data, int $code = 200, bool $encode = true, string $charset = 'utf8', int $option) // Envía una respuesta JSON y detiene el framework.
Flight::onEvent(string $event, callable $callback) // Registra un oyente de eventos.
Flight::triggerEvent(string $event, ...$args) // Activa un evento.
Cualquier método personalizado agregado con map y register también puede ser filtrado. Para ejemplos sobre cómo filtrar estos métodos, ve la guía de Filtrado de Métodos.
Clases del Framework Extensibles
Hay varias clases en las que puedes sobrescribir funcionalidad extendiéndolas y registrando tu propia clase. Estas clases son:
Flight::app() // Clase de aplicación - extiende la clase flight\Engine
Flight::request() // Clase de solicitud - extiende la clase flight\net\Request
Flight::response() // Clase de respuesta - extiende la clase flight\net\Response
Flight::router() // Clase de enrutador - extiende la clase flight\net\Router
Flight::view() // Clase de vista - extiende la clase flight\template\View
Flight::eventDispatcher() // Clase de despachador de eventos - extiende la clase flight\core\Dispatcher
Mapeo de Métodos Personalizados
Para mapear tu propio método personalizado simple, usa la función map:
// Mapear tu método
Flight::map('hello', function (string $name) {
echo "hello $name!";
});
// Llamar a tu método personalizado
Flight::hello('Bob');
Aunque es posible crear métodos personalizados simples, se recomienda solo crear funciones estándar en PHP. Esto tiene autocompletado en IDE y es más fácil de leer. El equivalente del código anterior sería:
function hello(string $name) {
echo "hello $name!";
}
hello('Bob');
Esto se usa más cuando necesitas pasar variables a tu método para obtener un valor
esperado. Usar el método register() como a continuación es más para pasar configuración
y luego llamar a tu clase preconfigurada.
Registro de Clases Personalizadas
Para registrar tu propia clase y configurarla, usa la función register. La ventaja que esto tiene sobre map() es que puedes reutilizar la misma clase cuando llamas a esta función (sería útil con Flight::db() para compartir la misma instancia).
// Registrar tu clase
Flight::register('user', User::class);
// Obtener una instancia de tu clase
$user = Flight::user();
El método register también te permite pasar parámetros al constructor de tu clase. Entonces, cuando cargues tu clase personalizada, vendrá preinicializada. Puedes definir los parámetros del constructor pasando un array adicional. Aquí hay un ejemplo de carga de una conexión a la base de datos:
// Registrar clase con parámetros del constructor
Flight::register('db', PDO::class, ['mysql:host=localhost;dbname=test', 'user', 'pass']);
// Obtener una instancia de tu clase
// Esto creará un objeto con los parámetros definidos
//
// new PDO('mysql:host=localhost;dbname=test','user','pass');
//
$db = Flight::db();
// y si lo necesitas más adelante en tu código, solo llamas al mismo método nuevamente
class SomeController {
public function __construct() {
$this->db = Flight::db();
}
}
Si pasas un parámetro de callback adicional, se ejecutará inmediatamente después de la construcción de la clase. Esto te permite realizar cualquier procedimiento de configuración para tu nuevo objeto. La función de callback toma un parámetro, una instancia del nuevo objeto.
// El callback recibirá el objeto que fue construido
Flight::register(
'db',
PDO::class,
['mysql:host=localhost;dbname=test', 'user', 'pass'],
function (PDO $db) {
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
);
Por defecto, cada vez que cargues tu clase obtendrás una instancia compartida.
Para obtener una nueva instancia de una clase, simplemente pasa false como parámetro:
// Instancia compartida de la clase
$shared = Flight::db();
// Nueva instancia de la clase
$new = Flight::db(false);
Nota: Ten en cuenta que los métodos mapeados tienen precedencia sobre las clases registradas. Si declaras ambos usando el mismo nombre, solo se invocará el método mapeado.
Ejemplos
Aquí hay algunos ejemplos de cómo puedes extender Flight con funcionalidad que no está incorporada en el núcleo.
Registro de Logs
Flight no tiene un sistema de registro de logs incorporado, sin embargo, es realmente fácil usar una biblioteca de registro con Flight. Aquí hay un ejemplo usando la biblioteca Monolog:
// services.php
// Registrar el logger con 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));
});
Ahora que está registrado, puedes usarlo en tu aplicación:
// En tu controlador o ruta
Flight::log()->warning('This is a warning message');
Esto registrará un mensaje en el archivo de log que especificaste. ¿Qué pasa si quieres registrar algo cuando ocurre
un error? Puedes usar el método error:
// En tu controlador o ruta
Flight::map('error', function(Throwable $ex) {
Flight::log()->error($ex->getMessage());
// Mostrar tu página de error personalizada
include 'errors/500.html';
});
También podrías crear un sistema básico de APM (Monitoreo de Rendimiento de Aplicación)
usando los métodos before y after:
// En tu archivo 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');
// También podrías agregar tus encabezados de solicitud o respuesta
// para registrarlos también (ten cuidado ya que esto sería mucho
// datos si tienes muchas solicitudes)
Flight::log()->info('Request Headers: ' . json_encode(Flight::request()->headers));
Flight::log()->info('Response Headers: ' . json_encode(Flight::response()->headers));
});
Caché
Flight no tiene un sistema de caché incorporado, sin embargo, es realmente fácil usar una biblioteca de caché con Flight. Aquí hay un ejemplo usando la biblioteca PHP File Cache:
// services.php
// Registrar el caché con Flight
Flight::register('cache', \flight\Cache::class, [ __DIR__ . '/../cache/' ], function(\flight\Cache $cache) {
$cache->setDevMode(ENVIRONMENT === 'development');
});
Ahora que está registrado, puedes usarlo en tu aplicación:
// En tu controlador o ruta
$data = Flight::cache()->get('my_cache_key');
if (empty($data)) {
// Realizar algún procesamiento para obtener los datos
$data = [ 'some' => 'data' ];
Flight::cache()->set('my_cache_key', $data, 3600); // caché por 1 hora
}
Instanciación Fácil de Objetos DIC
Si estás usando un DIC (Contenedor de Inyección de Dependencias) en tu aplicación, puedes usar Flight para ayudarte a instanciar tus objetos. Aquí hay un ejemplo usando la biblioteca Dice:
// services.php
// crear un nuevo contenedor
$container = new \Dice\Dice;
// no olvides reasignarlo a sí mismo como a continuación!
$container = $container->addRule('PDO', [
// shared significa que el mismo objeto se retornará cada vez
'shared' => true,
'constructParams' => ['mysql:host=localhost;dbname=test', 'user', 'pass' ]
]);
// ahora podemos crear un método mapeable para crear cualquier objeto.
Flight::map('make', function($class, $params = []) use ($container) {
return $container->create($class, $params);
});
// Esto registra el manejador del contenedor para que Flight sepa usarlo para controladores/middleware
Flight::registerContainerHandler(function($class, $params) {
Flight::make($class, $params);
});
// supongamos que tenemos la siguiente clase de ejemplo que toma un objeto PDO en el constructor
class EmailCron {
protected PDO $pdo;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
public function send() {
// código que envía un email
}
}
// Y finalmente puedes crear objetos usando inyección de dependencias
$emailCron = Flight::make(EmailCron::class);
$emailCron->send();
¿Genial, verdad?
Ver También
- Contenedor de Inyección de Dependencias - Cómo usar un DIC con Flight.
- Caché de Archivos - Ejemplo de uso de una biblioteca de caché con Flight.
Solución de Problemas
- Recuerda que los métodos mapeados tienen precedencia sobre las clases registradas. Si declaras ambos usando el mismo nombre, solo se invocará el método mapeado.
Registro de Cambios
- v2.0 - Lanzamiento Inicial.
Learn/json
Envoltorio JSON
Resumen
La clase Json en Flight proporciona una manera simple y consistente de codificar y decodificar datos JSON en su aplicación. Envuelve las funciones JSON nativas de PHP con un mejor manejo de errores y algunos valores predeterminados útiles, haciendo que sea más fácil y seguro trabajar con JSON.
Entendiendo
Trabajar con JSON es extremadamente común en las aplicaciones PHP modernas, especialmente al construir APIs o manejar solicitudes AJAX. La clase Json centraliza toda la codificación y decodificación de JSON, por lo que no tiene que preocuparse por casos extremos extraños o errores crípticos de las funciones integradas de PHP.
Características clave:
- Manejo consistente de errores (lanza excepciones en caso de fallo)
- Opciones predeterminadas para codificación/decodificación (como barras invertidas no escapadas)
- Métodos de utilidad para impresión legible y validación
Uso Básico
Codificando Datos a JSON
Para convertir datos PHP a una cadena JSON, use Json::encode():
use flight\util\Json;
$data = [
'framework' => 'Flight',
'version' => 3,
'features' => ['routing', 'views', 'extending']
];
$json = Json::encode($data);
echo $json;
// Salida: {"framework":"Flight","version":3,"features":["routing","views","extending"]}
Si la codificación falla, obtendrá una excepción con un mensaje de error útil.
Impresión Legible
¿Quiere que su JSON sea legible para humanos? Use prettyPrint():
echo Json::prettyPrint($data);
/*
{
"framework": "Flight",
"version": 3,
"features": [
"routing",
"views",
"extending"
]
}
*/
Decodificando Cadenas JSON
Para convertir una cadena JSON de vuelta a datos PHP, use Json::decode():
$json = '{"framework":"Flight","version":3}';
$data = Json::decode($json);
echo $data->framework; // Salida: Flight
Si desea un array asociativo en lugar de un objeto, pase true como el segundo argumento:
$data = Json::decode($json, true);
echo $data['framework']; // Salida: Flight
Si la decodificación falla, obtendrá una excepción con un mensaje de error claro.
Validando JSON
Verifique si una cadena es JSON válido:
if (Json::isValid($json)) {
// ¡Es válido!
} else {
// No es JSON válido
}
Obteniendo el Último Error
Si desea verificar el último mensaje de error JSON (de las funciones nativas de PHP):
$error = Json::getLastError();
if ($error !== '') {
echo "Último error JSON: $error";
}
Uso Avanzado
Puede personalizar las opciones de codificación y decodificación si necesita más control (vea opciones de json_encode de PHP):
// Codificar con la opción JSON_HEX_TAG
$json = Json::encode($data, JSON_HEX_TAG);
// Decodificar con profundidad personalizada
$data = Json::decode($json, false, 1024);
Véase También
- Collections - Para trabajar con datos estructurados que se pueden convertir fácilmente a JSON.
- Configuration - Cómo configurar su aplicación Flight.
- Extending - Cómo agregar sus propias utilidades o sobrescribir clases principales.
Solución de Problemas
- Si la codificación o decodificación falla, se lanza una excepción—envuelva sus llamadas en try/catch si desea manejar los errores de manera elegante.
- Si obtiene resultados inesperados, verifique sus datos en busca de referencias circulares o caracteres no UTF-8.
- Use
Json::isValid()para verificar si una cadena es JSON válido antes de decodificar.
Registro de Cambios
- v3.16.0 - Agregada la clase de utilidad de envoltorio JSON.
Learn/flight_vs_slim
Flight vs Slim
¿Qué es Slim?
Slim es un microframework de PHP que te ayuda a escribir rápidamente aplicaciones web y APIs simples pero potentes.
Gran parte de la inspiración para algunas de las características de la v3 de Flight provino realmente de Slim. Agrupar rutas y ejecutar middleware en un orden específico son dos características inspiradas en Slim. Slim v3 se lanzó orientado a la simplicidad, pero ha habido reseñas mixtas con respecto a la v4.
Ventajas en comparación con Flight
- Slim tiene una comunidad más grande de desarrolladores, que a su vez crean módulos útiles para ayudarte a no reinventar la rueda.
- Slim sigue muchas interfaces y estándares comunes en la comunidad de PHP, lo que aumenta la interoperabilidad.
- Slim tiene documentación y tutoriales decentes que se pueden usar para aprender el framework (aunque nada comparado con Laravel o Symfony).
- Slim tiene varios recursos como tutoriales de YouTube y artículos en línea que se pueden usar para aprender el framework.
- Slim te permite usar los componentes que quieras para manejar las características principales de enrutamiento, ya que es compatible con PSR-7.
Desventajas en comparación con Flight
- Sorprendentemente, Slim no es tan rápido como cabría esperar para un microframework. Consulta los benchmarks de TechEmpower para más información.
- Flight está orientado a un desarrollador que busca construir una aplicación web ligera, rápida y fácil de usar.
- Flight no tiene dependencias, mientras que Slim tiene algunas dependencias que debes instalar.
- Flight está orientado a la simplicidad y la facilidad de uso.
- Una de las características principales de Flight es que hace todo lo posible por mantener la compatibilidad hacia atrás. Slim v3 a v4 fue un cambio radical.
- Flight está pensado para desarrolladores que se aventuran por primera vez en el mundo de los frameworks.
- Flight también puede manejar aplicaciones a nivel empresarial, pero no tiene tantos ejemplos y tutoriales como Slim. También requerirá más disciplina por parte del desarrollador para mantener las cosas organizadas y bien estructuradas.
- Flight le da al desarrollador más control sobre la aplicación, mientras que Slim puede introducir algo de magia detrás de escena.
- Flight tiene SimplePdo para el acceso a bases de datos (preferido sobre el obsoleto PdoWrapper). Slim requiere que uses una biblioteca de terceros.
- Flight tiene un plugin de permisos que se puede usar para asegurar tu aplicación. Slim requiere que uses una biblioteca de terceros.
- Flight tiene un ORM llamado active-record que se puede usar para interactuar con tu base de datos. Slim requiere que uses una biblioteca de terceros.
- Flight tiene una aplicación CLI llamada runway que se puede usar para ejecutar tu aplicación desde la línea de comandos. Slim no la tiene.
Learn/autoloading
Autocarga
Resumen
La autocarga es un concepto en PHP donde especificas un directorio o directorios desde los cuales cargar clases. Es mucho más beneficioso que usar require o include para cargar clases. También es un requisito para usar paquetes de Composer.
Hacer bien la autocarga también es importante para el desarrollo asistido por IA: los agentes colocan archivos donde apunta el espacio de nombres. Si la mayúscula/minúscula de las carpetas y la del espacio de nombres no coinciden, aparecen errores de clase no encontrada en Linux incluso cuando las cosas "funcionaban" en un disco Mac que no distingue mayúsculas de minúsculas.
Entendiendo
Por defecto, cualquier clase Flight se autocarga automáticamente gracias a Composer. Para las clases de tu aplicación tienes dos enfoques comunes:
- Composer PSR-4 (lo que usa el esqueleto oficial): mapea un prefijo de espacio de nombres a un directorio en
composer.json, luego ejecutacomposer dump-autoload. Flight::path(): apunta el cargador de Flight a directorios (útil para aplicaciones simples o cuando no usas Composer para el código de la aplicación).
Usar un autocargador simplifica mucho tu código. En lugar de un muro de include / require al principio de cada archivo, las clases se cargan cuando las usas por primera vez.
Sensibilidad a mayúsculas/minúsculas (lee esto dos veces)
Los espacios de nombres deben coincidir con la estructura de directorios y con las mayúsculas/minúsculas de esos directorios.
| Funciona | Se rompe en Linux |
|---|---|
App\Controller\HomeController → app/Controller/HomeController.php |
App\Controller\… con la carpeta app/controllers/ |
app\controllers\MyController → app/controllers/MyController.php |
Mezclar App\ con controllers en minúsculas |
Los espacios de nombres de PHP no distinguen mayúsculas/minúsculas en algunos contextos, pero Composer y el sistema de archivos no. El esqueleto oficial estandariza en:
- Composer:
"App\\": "app/" - Carpetas:
Controller,Middleware,Model,Utils(PascalCase), nocontrollers/middlewares
La documentación anterior y los ejemplos de la comunidad a veces usaban app\controllers en minúsculas. Eso sigue funcionando si tus carpetas están en minúsculas, pero los nuevos proyectos de esqueleto usan App\ + carpetas PascalCase. Elige una convención por proyecto y mantente fiel a ella para que los humanos y las herramientas de IA no inventen una segunda estructura.
Esqueleto (recomendado para nuevos proyectos)
Después de composer create-project flightphp/skeleton, el código de la aplicación se autocarga mediante Composer—no se requiere Flight::path() para las clases 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 resuelve App\Controller\… mediante el contenedor
$router->get('/', [HomeController::class, 'index']);
Consulta Instalación para el árbol completo y IA y experiencia de desarrollo para ver cómo AGENTS.md documenta esta estructura para asistentes de codificación.
Uso básico (Flight::path())
Supongamos que tenemos una estructura de directorios como la siguiente:
# Ruta de ejemplo
/home/user/project/my-flight-project/
├── app
│ ├── cache
│ ├── config
│ ├── controllers - contiene los controladores de este proyecto
│ ├── translations
│ ├── UTILS - contiene clases solo para esta aplicación (esto está todo en mayúsculas a propósito para un ejemplo más adelante)
│ └── views
└── public
└── css
└── js
└── index.php
Puede que hayas notado que esto es similar a un árbol de aplicación típico (el propio sitio de documentación usa una estructura organizada). La carpeta controllers en minúsculas aquí es una elección válida: simplemente no es el valor predeterminado actual del esqueleto.
Puedes especificar cada directorio desde el que cargar de esta manera:
/**
* public/index.php
*/
// Añade una ruta al autocargador
Flight::path(__DIR__.'/../app/controllers/');
Flight::path(__DIR__.'/../app/utils/');
/**
* app/controllers/MyController.php
*/
// no se requiere espacio de nombres
// Se recomienda que todas las clases autocargadas utilicen PascalCase (cada palabra en mayúscula inicial, sin espacios)
class MyController {
public function index() {
// hacer algo
}
}
Espacios de nombres con Flight::path()
Si tienes espacios de nombres, en realidad se vuelve muy fácil implementar esto. Debes usar el método Flight::path() para especificar el directorio raíz (no la raíz del documento ni la carpeta public/) de tu aplicación.
/**
* public/index.php
*/
// Añade una ruta al autocargador
Flight::path(__DIR__.'/../');
Ahora así es como podría verse tu controlador. Mira el ejemplo a continuación, pero presta atención a los comentarios para obtener información importante.
/**
* app/controllers/MyController.php
*/
// los espacios de nombres son obligatorios
// los espacios de nombres son iguales a la estructura de directorios
// los espacios de nombres deben seguir las mismas mayúsculas/minúsculas que la estructura de directorios
// los espacios de nombres y los directorios no pueden tener guiones bajos (a menos que se establezca Loader::setV2ClassLoading(false))
namespace app\controllers;
// Se recomienda que todas las clases autocargadas utilicen PascalCase (cada palabra en mayúscula inicial, sin espacios)
// A partir de 3.7.2, puedes usar Pascal_Snake_Case para los nombres de tus clases ejecutando Loader::setV2ClassLoading(false);
class MyController {
public function index() {
// hacer algo
}
}
Y si quisieras autocargar una clase en tu directorio de utilidades, básicamente harías lo mismo:
/**
* app/UTILS/ArrayHelperUtil.php
*/
// el espacio de nombres debe coincidir con la estructura de directorios y con las mayúsculas/minúsculas (nota que el directorio UTILS está todo en mayúsculas
// como en el árbol de archivos anterior)
namespace app\UTILS;
class ArrayHelperUtil {
public function changeArrayCase(array $array) {
// hacer algo
}
}
Espacio de nombres estilo esqueleto (mismas reglas, diferente uso de mayúsculas)
/**
* app/Controller/MyController.php
*/
namespace App\Controller;
class MyController {
// ...
}
La regla no cambió—solo cambia el uso de mayúsculas/minúsculas elegido por el esqueleto para carpetas/espacios de nombres. Cualquiera que sea el uso de mayúsculas/minúsculas de tus carpetas, tu línea namespace debe coincidir.
Guiones bajos en nombres de clases
A partir de 3.7.2, puedes usar Pascal_Snake_Case para los nombres de tus clases ejecutando Loader::setV2ClassLoading(false);.
Esto te permitirá usar guiones bajos en los nombres de tus clases.
No se recomienda, pero está disponible para quienes lo necesiten.
use flight\core\Loader;
/**
* public/index.php
*/
// Añade una ruta al autocargador
Flight::path(__DIR__.'/../app/controllers/');
Flight::path(__DIR__.'/../app/utils/');
Loader::setV2ClassLoading(false);
/**
* app/controllers/My_Controller.php
*/
// no se requiere espacio de nombres
class My_Controller {
public function index() {
// hacer algo
}
}
Ver también
- Instalación - Árbol del esqueleto y valores predeterminados
App\para nuevos proyectos. - Enrutamiento - Cómo mapear rutas a controladores y renderizar vistas.
- Inyección de dependencias - Cómo obtienen los controladores
Enginey servicios. - IA y experiencia de desarrollo - Mantén a los agentes alineados con tu estructura mediante
AGENTS.md. - ¿Por qué un framework? - Comprende los beneficios de usar un framework como Flight.
Solución de problemas
Si no logras entender por qué no se encuentran tus clases con espacios de nombres, recuerda: con Flight::path(), apunta a la raíz del proyecto (o la base correcta para tu espacio de nombres), no solo a una carpeta anidada que olvidaste reflejar en el espacio de nombres.
Con Composer PSR-4, ejecuta composer dump-autoload después de cambiar las asignaciones en composer.json.
En CI de Linux o producción, un uso incorrecto de mayúsculas/minúsculas en carpetas es una falla muy común de «funciona en mi máquina».
Clase no encontrada (la autocarga no funciona)
Podría haber un par de razones para que esto no ocurra. A continuación se muestran algunos ejemplos.
Nombre de archivo incorrecto
La más común es que el nombre de la clase no coincida con el nombre del archivo.
Si tienes una clase llamada MyClass, entonces el archivo debe llamarse MyClass.php. Si tienes una clase llamada MyClass y el archivo se llama myclass.php, el autocargador no podrá encontrarla.
Espacio de nombres o mayúsculas/minúsculas de carpeta incorrectos
Si estás usando espacios de nombres, entonces el espacio de nombres debe coincidir con la estructura de directorios incluyendo las mayúsculas/minúsculas.
// ...código...
// si tu MyController está en app/Controller (esqueleto) y tiene el espacio de nombres App\Controller
// esto no funcionará:
Flight::route('/hello', 'MyController->hello');
// Estilo esqueleto:
use App\Controller\MyController;
Flight::route('/hello', [ MyController::class, 'hello' ]);
// Estructura anterior en minúsculas (solo si tus carpetas son realmente app/controllers):
use app\controllers\MyController;
Flight::route('/hello', [ MyController::class, 'hello' ]);
// o completamente calificado:
Flight::route('/hello', [ 'App\Controller\MyController', 'hello' ]);
path() no definido (código de aplicación sin Composer)
Si dependes de Flight::path() en lugar de Composer para las clases de la aplicación, define la ruta antes de las rutas que hagan referencia a esas clases (a menudo al inicio del arranque / public/index.php):
// Añade una ruta al autocargador (raíz del proyecto para aplicaciones con espacios de nombres)
Flight::path(__DIR__.'/../');
El esqueleto oficial utiliza principalmente Composer PSR-4 para App\, por lo que normalmente no necesitarás Flight::path() para controladores y modelos allí.
Historial de cambios
- Documentación: documenta el esqueleto
App\+ carpetas PascalCase y las trampas de mayúsculas/minúsculas para humanos y herramientas de IA. - v3.7.2 - Puedes usar Pascal_Snake_Case para los nombres de tus clases ejecutando
Loader::setV2ClassLoading(false); - v2.0 - Se añadió la funcionalidad de autocarga.
Learn/uploaded_file
Manejador de Archivos Subidos
Resumen
La clase UploadedFile en Flight facilita y asegura el manejo de subidas de archivos en su aplicación. Envuelve los detalles del proceso de subida de archivos de PHP, proporcionando una forma simple y orientada a objetos para acceder a la información de los archivos y mover los archivos subidos.
Comprensión
Cuando un usuario sube un archivo a través de un formulario, PHP almacena la información sobre el archivo en el superglobal $_FILES. En Flight, rara vez interactúa directamente con $_FILES. En su lugar, el objeto Request de Flight (accesible a través de Flight::request()) proporciona un método getUploadedFiles() que devuelve un array de objetos UploadedFile, haciendo que el manejo de archivos sea mucho más conveniente y robusto.
La clase UploadedFile proporciona métodos para:
- Obtener el nombre original del archivo, el tipo MIME, el tamaño y la ubicación temporal
- Verificar errores de subida
- Mover el archivo subido a una ubicación permanente
Esta clase le ayuda a evitar errores comunes con las subidas de archivos, como el manejo de errores o la movimiento de archivos de manera segura.
Uso Básico
Acceso a Archivos Subidos desde una Solicitud
La forma recomendada de acceder a los archivos subidos es a través del objeto de solicitud:
Flight::route('POST /upload', function() {
// Para un campo de formulario llamado <input type="file" name="myFile">
$uploadedFiles = Flight::request()->getUploadedFiles();
$file = $uploadedFiles['myFile'];
// Ahora puede usar los métodos de UploadedFile
if ($file->getError() === UPLOAD_ERR_OK) {
$file->moveTo('/path/to/uploads/' . $file->getClientFilename());
echo "File uploaded successfully!";
} else {
echo "Upload failed: " . $file->getError();
}
});
Manejo de Subidas Múltiples de Archivos
Si su formulario usa name="myFiles[]" para subidas múltiples, obtendrá un array de objetos UploadedFile:
Flight::route('POST /upload', function() {
// Para un campo de formulario llamado <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 "Uploaded: " . $file->getClientFilename() . "<br>";
} else {
echo "Failed to upload: " . $file->getClientFilename() . "<br>";
}
}
});
Creación Manual de una Instancia de UploadedFile
Normalmente, no creará un UploadedFile manualmente, pero puede hacerlo si es necesario:
use flight\net\UploadedFile;
$file = new UploadedFile(
$_FILES['myfile']['name'],
$_FILES['myfile']['type'],
$_FILES['myfile']['size'],
$_FILES['myfile']['tmp_name'],
$_FILES['myfile']['error']
);
Acceso a la Información del Archivo
Puede obtener fácilmente detalles sobre el archivo subido:
echo $file->getClientFilename(); // Nombre original del archivo desde la computadora del usuario
echo $file->getClientMediaType(); // Tipo MIME (por ejemplo, image/png)
echo $file->getSize(); // Tamaño del archivo en bytes
echo $file->getTempName(); // Ruta temporal del archivo en el servidor
echo $file->getError(); // Código de error de subida (0 significa sin error)
Mover el Archivo Subido
Después de validar el archivo, muévelo a una ubicación permanente:
try {
$file->moveTo('/path/to/uploads/' . $file->getClientFilename());
echo "File uploaded successfully!";
} catch (Exception $e) {
echo "Upload failed: " . $e->getMessage();
}
El método moveTo() lanzará una excepción si algo sale mal (como un error de subida o un problema de permisos).
Manejo de Errores de Subida
Si hubo un problema durante la subida, puede obtener un mensaje de error legible por humanos:
if ($file->getError() !== UPLOAD_ERR_OK) {
// Puede usar el código de error o capturar la excepción de moveTo()
echo "There was an error uploading the file.";
}
Ver También
- Requests - Aprenda cómo acceder a archivos subidos desde solicitudes HTTP y vea más ejemplos de subida de archivos.
- Configuration - Cómo configurar límites de subida y directorios en PHP.
- Extending - Cómo personalizar o extender las clases principales de Flight.
Solución de Problemas
- Siempre verifique
$file->getError()antes de mover el archivo. - Asegúrese de que su directorio de subida sea escribible por el servidor web.
- Si
moveTo()falla, verifique el mensaje de excepción para obtener detalles. - Las configuraciones de PHP
upload_max_filesizeypost_max_sizepueden limitar las subidas de archivos. - Para subidas múltiples de archivos, siempre itere a través del array de objetos
UploadedFile.
Registro de Cambios
- v3.12.0 - Se agregó la clase
UploadedFileal objeto de solicitud para un manejo de archivos más fácil.
Guides/unit_testing
Pruebas Unitarias en Flight PHP con PHPUnit
Esta guía introduce las pruebas unitarias en Flight PHP usando PHPUnit, pensada para principiantes que quieren entender por qué las pruebas unitarias importan y cómo aplicarlas de manera práctica. Nos centraremos en probar el comportamiento—asegurando que tu aplicación hace lo que esperas, como enviar un correo electrónico o guardar un registro—en lugar de cálculos triviales. Comenzaremos con un manejador de rutas simple y avanzaremos hacia un controlador más complejo, incorporando inyección de dependencias (DI) y simulando (mock) servicios de terceros.
¿Por qué realizar pruebas unitarias?
Las pruebas unitarias aseguran que tu código se comporte como se espera, detectando errores antes de que lleguen a producción. Son especialmente valiosas en Flight, donde el enrutamiento ligero y la flexibilidad pueden llevar a interacciones complejas. Para desarrolladores en solitario o equipos, las pruebas unitarias actúan como una red de seguridad, documentando el comportamiento esperado y previniendo regresiones cuando revisas el código más tarde. También mejoran el diseño: el código difícil de probar a menudo señala clases demasiado complejas o fuertemente acopladas.
A diferencia de ejemplos simplistas (p. ej., probar x * y = z), nos centraremos en comportamientos del mundo real, como validar entrada, guardar datos o desencadenar acciones como correos electrónicos. Nuestro objetivo es hacer que las pruebas sean accesibles y significativas.
Principios Generales de Guía
- Prueba el comportamiento, no la implementación: Concéntrate en los resultados (p. ej., "correo enviado" o "registro guardado") en lugar de los detalles internos. Esto hace que las pruebas sean robustas frente a la refactorización.
- Deja de usar
Flight::: Los métodos estáticos de Flight son terriblemente convenientes, pero dificultan las pruebas. Debes acostumbrarte a usar la variable$appde$app = Flight::app();.$apptiene todos los mismos métodos queFlight::. Aún podrás usar$app->route()o$this->app->json()en tu controlador, etc. También debes usar el enrutador real de Flight con$router = $app->router()y luego puedes usar$router->get(),$router->post(),$router->group(), etc. Consulta Enrutamiento. - Mantén las pruebas rápidas: Las pruebas rápidas fomentan la ejecución frecuente. Evita operaciones lentas como llamadas a bases de datos en pruebas unitarias. Si tienes una prueba lenta, es una señal de que estás escribiendo una prueba de integración, no una prueba unitaria. Las pruebas de integración son cuando realmente involucras bases de datos reales, llamadas HTTP reales, envío de correos reales, etc. Tienen su lugar, pero son lentas y pueden ser inestables, lo que significa que a veces fallan por una razón desconocida.
- Usa nombres descriptivos: Los nombres de las pruebas deben describir claramente el comportamiento que se está probando. Esto mejora la legibilidad y el mantenimiento.
- Evita las variables globales como la peste: Minimiza el uso de
$app->set()y$app->get(), ya que actúan como estado global, requiriendo mocks en cada prueba. Prefiere DI o un contenedor de DI (consulta Contenedor de Inyección de Dependencias). Incluso usar el método$app->map()es técnicamente un "global" y debe evitarse en favor de DI. Usa una librería de sesión como flightphp/session para que puedas simular el objeto de sesión en tus pruebas. No llames a$_SESSIONdirectamente en tu código, ya que eso inyecta una variable global en tu código, dificultando las pruebas. - Usa inyección de dependencias: Inyecta dependencias (p. ej.,
PDO, mailers) en los controladores para aislar la lógica y simplificar la simulación (mock). Si tienes una clase con demasiadas dependencias, considera refactorizarla en clases más pequeñas que cada una tenga una única responsabilidad siguiendo los principios SOLID. - Simula servicios de terceros: Simula bases de datos, clientes HTTP (cURL) o servicios de correo para evitar llamadas externas. Prueba una o dos capas de profundidad, pero deja que tu lógica central se ejecute. Por ejemplo, si tu aplicación envía un mensaje de texto, NO quieres enviar un mensaje de texto real cada vez que ejecutas tus pruebas porque esos cargos se acumularán (y será más lento). En su lugar, simula el servicio de mensajes de texto y solo verifica que tu código llamó al servicio de mensajes de texto con los parámetros correctos.
- Apunta a una alta cobertura, no a la perfección: Una cobertura de línea del 100% es buena, pero en realidad no significa que todo en tu código esté probado como debería (investiga sobre cobertura de ramas/rutas en PHPUnit). Prioriza los comportamientos críticos (p. ej., registro de usuarios, respuestas de API y captura de respuestas fallidas).
- Usa controladores para las rutas: En tus definiciones de rutas, usa controladores en lugar de closures. El
flight\Engine $appse inyecta en cada controlador a través del constructor por defecto. En las pruebas, usa$app = new Flight\Engine()para instanciar Flight dentro de una prueba, inyéctalo en tu controlador y llama métodos directamente (p. ej.,$controller->register()). Consulta Extendiendo Flight y Enrutamiento. - Elige un estilo de simulación (mock) y mantente consistente: PHPUnit soporta varios estilos de simulación (p. ej., profecía, mocks incorporados), o puedes usar clases anónimas que tienen sus propios beneficios como el autocompletado de código, romperse si cambias la definición del método, etc. Solo sé consistente en todas tus pruebas. Consulta Objetos simulados de PHPUnit.
- Usa visibilidad
protectedpara métodos/propiedades que quieras probar en subclases: Esto te permite sobrescribirlos en subclases de prueba sin hacerlos públicos, lo cual es especialmente útil para mocks de clases anónimas.
Configuración de PHPUnit
Primero, configura PHPUnit en tu proyecto Flight PHP usando Composer para facilitar las pruebas. Consulta la guía de inicio de PHPUnit para más detalles.
-
En el directorio de tu proyecto, ejecuta:
composer require --dev phpunit/phpunitEsto instala la última versión de PHPUnit como una dependencia de desarrollo.
-
Crea un directorio
testsen la raíz de tu proyecto para los archivos de prueba. -
Agrega un script de prueba a
composer.jsonpor conveniencia:// otro contenido de composer.json "scripts": { "test": "phpunit --configuration phpunit.xml" } -
Crea un archivo
phpunit.xmlen la raíz:<?xml version="1.0" encoding="UTF-8"?> <phpunit bootstrap="vendor/autoload.php"> <testsuites> <testsuite name="Flight Tests"> <directory>tests</directory> </testsuite> </testsuites> </phpunit>
Ahora, cuando tus pruebas estén creadas, puedes ejecutar composer test para ejecutar las pruebas.
Probando un Manejador de Rutas Simple
Comencemos con una ruta básica que valida la entrada de correo electrónico de un usuario. Probaremos su comportamiento: devolver un mensaje de éxito para correos válidos y un error para los inválidos. Para la validación de correo, usamos 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);
}
}
Para probar esto, crea un archivo de prueba. Consulta Pruebas Unitarias y Principios SOLID para más información sobre cómo estructurar pruebas:
// 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'; // Simular datos 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'; // Simular datos 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']);
}
}
Puntos clave:
- Simulamos los datos POST usando la clase de solicitud. No uses variables globales como
$_POST,$_GET, etc., ya que esto hace que las pruebas sean más complicadas (tienes que restablecer siempre esos valores o otras pruebas podrían fallar). - Todos los controladores, por defecto, tendrán la instancia de
flight\Engineinyectada en ellos incluso sin tener un contenedor DIC configurado. Esto hace que sea mucho más fácil probar los controladores directamente. - No hay uso de
Flight::en absoluto, lo que hace que el código sea más fácil de probar. - Las pruebas verifican el comportamiento: estado y mensaje correctos para correos válidos/inválidos.
Ejecuta composer test para verificar que la ruta se comporte como se espera. Para más información sobre solicitudes y respuestas en Flight, consulta la documentación relevante.
Uso de la Inyección de Dependencias para Controladores Comprobables
Para escenarios más complejos, usa inyección de dependencias (DI) para que los controladores sean comprobables. Evita las globales de Flight (p. ej., Flight::set(), Flight::map(), Flight::register()) ya que actúan como estado global, requiriendo mocks para cada prueba. En su lugar, usa el contenedor DI de Flight, DICE, PHP-DI o DI manual.
Usemos flight\database\SimplePdo en lugar de PDO crudo. Este helper es mucho más fácil de simular y probar unitariamente (y se prefiere sobre el obsoleto PdoWrapper).
Aquí hay un controlador que guarda un usuario en una base de datos y envía un correo de bienvenida:
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)) {
// añadir el return aquí ayuda a que las pruebas unitarias detengan la ejecución
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']);
}
}
Puntos clave:
- El controlador depende de una instancia de
SimplePdoy de unaMailerInterface(un servicio de correo simulado de terceros). - Las dependencias se inyectan a través del constructor, evitando variables globales.
Probando el Controlador con Mocks (Simulaciones)
Ahora, probemos el comportamiento de UserController: validar correos, guardar en la base de datos y enviar correos. Simularemos la base de datos y el mailer para aislar el controlador.
// tests/UserControllerDICTest.php
use flight\database\SimplePdo;
use PHPUnit\Framework\TestCase;
class UserControllerDICTest extends TestCase {
public function testValidEmailSavesAndSendsEmail() {
// A veces es necesario mezclar estilos de simulación
// Aquí usamos el mock incorporado de PHPUnit para PDOStatement
$statementMock = $this->createMock(PDOStatement::class);
$statementMock->method('execute')->willReturn(true);
// Usando una clase anónima para simular SimplePdo
$mockDb = new class($statementMock) extends SimplePdo {
protected $statementMock;
public function __construct($statementMock) {
$this->statementMock = $statementMock;
}
// Cuando lo simulamos de esta manera, no estamos haciendo realmente una llamada a la base de datos.
// Podemos configurar esto además para alterar el mock de PDOStatement y simular fallos, etc.
public function runQuery(string $sql, array $params = []): PDOStatement {
return $this->statementMock;
}
};
$mockMailer = new class implements MailerInterface {
public $sentEmail = null;
public function sendWelcome($email): bool {
$this->sentEmail = $email;
return true;
}
};
$app = new Engine();
$app->request()->data->email = 'test@example.com';
$controller = new UserControllerDIC($app, $mockDb, $mockMailer);
$controller->register();
$response = $app->response()->getBody();
$result = json_decode($response, true);
$this->assertEquals('success', $result['status']);
$this->assertEquals('User registered', $result['message']);
$this->assertEquals('test@example.com', $mockMailer->sentEmail);
}
public function testInvalidEmailSkipsSaveAndEmail() {
$mockDb = new class() extends SimplePdo {
// Un constructor vacío omite el constructor padre
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';
// Necesitamos mapear jsonHalt para evitar la salida
$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']);
}
}
Puntos clave:
- Simulamos
SimplePdoyMailerInterfacepara evitar llamadas reales a la base de datos o al correo. - Las pruebas verifican el comportamiento: los correos válidos desencadenan inserciones en la base de datos y envíos de correo; los correos inválidos omiten ambos.
- Simula dependencias de terceros (p. ej.,
SimplePdo,MailerInterface), dejando que la lógica del controlador se ejecute.
Simulando demasiado
Ten cuidado de no simular demasiado tu código. Te doy un ejemplo a continuación de por qué esto podría ser malo usando nuestro UserController. Cambiaremos esa verificación a un método llamado isEmailValid (usando filter_var) y las otras nuevas adiciones a un método separado llamado 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)) {
// añadir el return aquí ayuda a que las pruebas unitarias detengan la ejecución
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);
}
}
Y ahora la prueba unitaria sobresimulada que en realidad no prueba nada:
use PHPUnit\Framework\TestCase;
class UserControllerTest extends TestCase {
public function testValidEmailSavesAndSendsEmail() {
$app = new Engine();
$app->request()->data->email = 'test@example.com';
// estamos omitiendo la inyección de dependencias extra aquí porque es "fácil"
$controller = new class($app) extends UserControllerDICV2 {
protected $app;
// Omitimos las dependencias en el constructor
public function __construct($app) {
$this->app = $app;
}
// Simplemente forzaremos que esto sea válido.
protected function isEmailValid($email) {
return true; // Siempre devuelve true, omitiendo la validación real
}
// Omitimos las llamadas reales a la base de datos y al correo
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']);
}
}
¡Hurra, tenemos pruebas unitarias y están pasando! Pero espera, ¿y si realmente cambio el funcionamiento interno de isEmailValid o registerUser? Mis pruebas seguirán pasando porque he simulado toda la funcionalidad. Déjame mostrarte lo que quiero decir.
// UserControllerDICV2.php
class UserControllerDICV2 {
// ... otros métodos ...
protected function isEmailValid($email) {
// Lógica cambiada
$validEmail = filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
// Ahora debería tener solo un dominio específico
$validDomain = strpos($email, '@example.com') !== false;
return $validEmail && $validDomain;
}
}
Si ejecutara mis pruebas unitarias anteriores, ¡aún pasarían! Pero debido a que no estaba probando el comportamiento (dejando que parte del código se ejecute realmente), potencialmente he codificado un error a punto de ocurrir en producción. La prueba debe modificarse para tener en cuenta el nuevo comportamiento, y también lo contrario cuando el comportamiento no es el que esperamos.
Ejemplo Completo
Puedes encontrar un ejemplo completo de un proyecto Flight PHP con pruebas unitarias en GitHub: n0nag0n/flight-unit-tests-guide. Para una comprensión más profunda, consulta Pruebas Unitarias y Principios SOLID.
Errores Comunes
- Sobresimulación (Over-Mocking): No simules cada dependencia; deja que algo de lógica (p. ej., la validación del controlador) se ejecute para probar el comportamiento real. Consulta Pruebas Unitarias y Principios SOLID.
- Estado global: Usar variables globales de PHP (p. ej.,
$_SESSION,$_COOKIE) en gran medida hace que las pruebas sean frágiles. Lo mismo ocurre conFlight::. Refactoriza para pasar las dependencias explícitamente. - Configuración compleja: Si la configuración de la prueba es engorrosa, tu clase puede tener demasiadas dependencias o responsabilidades, violando los principios SOLID.
Escalando con Pruebas Unitarias
Las pruebas unitarias brillan en proyectos más grandes o al revisar código después de meses. Documentan el comportamiento y detectan regresiones, ahorrándote tener que reaprender tu aplicación. Para desarrolladores en solitario, prueba las rutas críticas (p. ej., registro de usuarios, procesamiento de pagos). Para equipos, las pruebas aseguran un comportamiento consistente en todas las contribuciones. Consulta ¿Por qué frameworks? para más información sobre los beneficios de usar frameworks y pruebas.
¡Contribuye con tus propios consejos de pruebas al repositorio de documentación de Flight PHP!
Escrito por n0nag0n 2025
Guides/blog
Cómo crear un blog simple con Flight PHP
Esta guía te guía a través de la creación de un blog básico usando el framework PHP Flight. Configurarás un proyecto, definirás rutas, gestionarás publicaciones con JSON y las renderizarás con el motor de plantillas Latte, todo mostrando la simplicidad y flexibilidad de Flight. Al final, tendrás un blog funcional con una página de inicio, páginas individuales de publicación y un formulario de creación.
Requisitos previos
- PHP 7.4+: Instalado en tu sistema.
- Composer: Para la gestión de dependencias.
- Editor de texto: Cualquier editor como VS Code o PHPStorm.
- Conocimientos básicos de PHP y desarrollo web.
Paso 1: Configura tu proyecto
Comienza creando un nuevo directorio de proyecto e instalando Flight mediante Composer.
-
Crea un directorio:
mkdir flight-blog cd flight-blog -
Instala Flight:
composer require flightphp/core -
Crea un directorio público: Flight usa un único punto de entrada (
index.php). Crea una carpetapublic/para ello:mkdir public -
index.phpbásico: Creapublic/index.phpcon una ruta simple de “hola mundo”:<?php require '../vendor/autoload.php'; Flight::route('/', function () { echo 'Hello, Flight!'; }); Flight::start(); -
Ejecuta el servidor integrado: Prueba tu configuración con el servidor de desarrollo de PHP:
php -S localhost:8000 -t public/Visita
http://localhost:8000para ver “Hello, Flight!”.
Paso 2: Organiza la estructura de tu proyecto
Para una configuración limpia, estructura tu proyecto así:
flight-blog/
├── app/
│ ├── config/
│ └── views/
├── data/
├── public/
│ └── index.php
├── vendor/
└── composer.json
app/config/: Archivos de configuración (por ejemplo, eventos, rutas).app/views/: Plantillas para renderizar páginas.data/: Archivo JSON para almacenar publicaciones del blog.public/: Raíz web conindex.php.
Paso 3: Instala y configura Latte
Latte es un motor de plantillas ligero que se integra bien con Flight.
-
Instala Latte:
composer require latte/latte -
Configura Latte en Flight: Actualiza
public/index.phppara registrar Latte como el motor de vistas:<?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(); -
Crea una plantilla de diseño: En
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> -
Crea una plantilla de inicio: En
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}Reinicia el servidor si lo has cerrado y visita
http://localhost:8000para ver la página renderizada. -
Crea un archivo de datos:
Usa un archivo JSON para simular una base de datos por simplicidad.
En
data/posts.json:[ { "slug": "first-post", "title": "My First Post", "content": "This is my very first blog post with Flight PHP!" } ]
Paso 4: Define las rutas
Separa tus rutas en un archivo de configuración para una mejor organización.
-
Crea
routes.php: Enapp/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']); }); -
Actualiza
index.php: Incluye el archivo de rutas:<?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();
Paso 5: Almacena y recupera publicaciones del blog
Agrega los métodos para cargar y guardar publicaciones.
-
Agrega un método de publicaciones: En
index.php, agrega un método para cargar publicaciones:Flight::map('posts', function () { $file = __DIR__ . '/../data/posts.json'; return json_decode(file_get_contents($file), true); }); -
Actualiza las rutas: Modifica
app/config/routes.phppara usar las publicaciones:<?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']); });
Paso 6: Crea plantillas
Actualiza tus plantillas para mostrar las publicaciones.
-
Página de publicación (
app/views/post.latte):{extends 'layout.latte'} {block content} <h2>{$post['title']}</h2> <div class="post-content"> <p>{$post['content']}</p> </div> {/block}
Paso 7: Agrega creación de publicaciones
Maneja el envío del formulario para agregar nuevas publicaciones.
-
Crea el formulario (
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} -
Agrega la ruta POST: En
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('/'); }); -
Pruébalo:
- Visita
http://localhost:8000/create. - Envía una nueva publicación (por ejemplo, “Second Post” con algo de contenido).
- Revisa la página de inicio para verla listada.
- Visita
Paso 8: Mejora con manejo de errores
Sobrescribe el método notFound para una mejor experiencia de error 404.
En index.php:
Flight::map('notFound', function () {
Flight::view()->render('404.latte', ['title' => 'Page Not Found']);
});
Crea app/views/404.latte:
{extends 'layout.latte'}
{block content}
<h2>404 - {$title}</h2>
<p>Sorry, that page doesn't exist!</p>
{/block}
Próximos pasos
- Agrega estilos: Usa CSS en tus plantillas para una mejor apariencia.
- Base de datos: Reemplaza
posts.jsoncon una base de datos como SQLite usando SimplePdo. - Validación: Agrega comprobaciones para slugs duplicados o entradas vacías.
- Middleware: Implementa autenticación para la creación de publicaciones.
Conclusión
¡Has creado un blog simple con Flight PHP! Esta guía demuestra características principales como el enrutamiento, las plantillas con Latte y el manejo de envíos de formularios, todo mientras se mantiene ligero. Explora la documentación de Flight para obtener más funciones avanzadas y llevar tu blog más lejos.
License
Licencia MIT (MIT)
==================
Derechos de autor © `2024` `@mikecao, @n0nag0n`
Se concede permiso, de forma gratuita, a cualquier persona que obtenga
una copia de este software y archivos de documentación asociados
(el “Software”), para utilizar el Software sin restricciones,
incluidos, entre otros, los derechos de usar, copiar, modificar,
fusionar, publicar, distribuir, sublicenciar y/o vender copias del
Software, y permitir a las personas a las que se les haya provisto
el Software que lo hagan, sujeto a las siguientes condiciones:
El aviso de derechos de autor anterior y este aviso de permiso deben
estar incluidos en todas las copias o partes sustanciales del Software.
EL SOFTWARE SE PROPORCIONA "TAL CUAL", SIN GARANTÍA DE NINGÚN TIPO,
EXPRESA O IMPLÍCITA, INCLUIDAS, ENTRE OTRAS, LAS GARANTÍAS DE
COMERCIABILIDAD, IDONEIDAD PARA UN PROPÓSITO PARTICULAR Y NO
INFRACCIÓN. EN NINGÚN CASO LOS AUTORES O TITULARES DE LOS DERECHOS
DE AUTOR SERÁN RESPONSABLES DE NINGUNA RECLAMACIÓN, DAÑOS U OTRA
RESPONSABILIDAD, YA SEA EN UNA ACCIÓN DE CONTRATO, AGRAVIO O DE CUALQUIER
OTRA ÍNDOLE, DERIVADA DE, FUERA DE O EN RELACIÓN CON EL SOFTWARE O EL USO
U OTRO TIPO DE TRATAMIENTO EN EL SOFTWARE.About
Flight PHP Framework
Flight es un framework rápido, simple y extensible para PHP—diseñado para desarrolladores que quieren hacer las cosas rápidamente, sin complicaciones. Ya sea que estés construyendo una aplicación web clásica, una API ultrarrápida, o trabajando con asistentes de codificación IA, el bajo consumo de recursos y el diseño sencillo de Flight lo convierten en una opción perfecta. Flight está pensado para ser ligero, pero también puede manejar los requisitos de arquitecturas empresariales.
¿Por qué elegir Flight?
- Fácil para principiantes: Flight es un excelente punto de partida para nuevos desarrolladores PHP. Su estructura clara y sintaxis simple te ayudan a aprender desarrollo web sin perderte entre código repetitivo.
- Amado por profesionales: Los desarrolladores experimentados aprecian Flight por su flexibilidad y control. Puedes escalar desde un pequeño prototipo hasta una aplicación completa sin necesidad de cambiar de framework.
- Compatible con versiones anteriores: Valoramos tu tiempo. Flight v3 es una mejora de v2, manteniendo casi toda la misma API. Creemos en la evolución, no en la revolución—no más "romper el mundo" cada vez que sale una versión mayor.
- Sin dependencias: El núcleo de Flight está completamente libre de dependencias—sin polyfills, sin paquetes externos, ni siquiera interfaces PSR. Esto significa menos vectores de ataque, un menor consumo de recursos y sin cambios inesperados que rompan la compatibilidad provenientes de dependencias externas. Los plugins opcionales pueden incluir dependencias, pero el núcleo siempre permanecerá ligero y seguro.
- Amigable con IA: La pequeña superficie de API de Flight y el esqueleto oficial (un diseño,
AGENTS.md, inyección por constructor) facilitan que las herramientas de codificación IA se mantengan dentro del patrón. Mismo código base ya sea que escribas cada línea o trabajes con un agente. Aprende más sobre usar IA con Flight.
Resumen en Video
Inicio Rápido
Para hacer una instalación básica rápida, instálalo con Composer:
composer require flightphp/core
O puedes descargar un zip del repositorio aquí. Luego tendrás un archivo index.php básico como el siguiente:
<?php
// si se instaló con composer
require 'vendor/autoload.php';
// o si se instaló manualmente por archivo zip
// require 'flight/Flight.php';
Flight::route('/', function() {
echo 'hello world!';
});
Flight::route('/json', function() {
Flight::json([
'hello' => 'world'
]);
});
Flight::start();
¡Eso es todo! Tienes una aplicación Flight básica. Ahora puedes ejecutar este archivo con php -S localhost:8000 y visitar http://localhost:8000 en tu navegador para ver la salida.
Ejemplos cortos de Flight:: como este son geniales para aprender y aplicaciones micro. Para un diseño de proyecto completo que humanos y herramientas IA compartan, usa el esqueleto de abajo.
Aplicación Esqueleto/Plantilla
Hay un iniciador oficial para ayudarte a comenzar cualquier nuevo proyecto Flight. Configura la estructura, configuración, scripts de Composer e instrucciones amigables para IA desde el primer momento.
Consulta flightphp/skeleton para un proyecto listo para usar, o visita la página de ejemplos para inspiración. ¿Quieres los detalles del flujo de trabajo de IA? Explora IA y experiencia de desarrollador.
Lo que obtienes (nivel alto):
- Espacios de nombres
App\con carpetas en PascalCase (app/Controller/,app/Middleware/,app/Model/, …)—la capitalización de las carpetas debe coincidir con el espacio de nombres (ver Autocarga) - Inyección de Dice +
Enginepara que los controladores permanezcan testeables (prefiere$this->appsobreFlight::en el código de la aplicación) - Vistas Twig, muestra de SimplePdo + ActiveRecord, migrate de Runway
AGENTS.mden la raíz (más copias por alcance) ySECURITY.mdpara asistentes y política de seguridad
Instalando la Aplicación Esqueleto
¡Bastante simple!
# Crear el nuevo proyecto
composer create-project flightphp/skeleton my-project/
# Entrar al directorio del nuevo proyecto
cd my-project/
# ¡Levantar el servidor de desarrollo local para comenzar de inmediato!
composer start
Crea la estructura del proyecto, copia config_sample.php → config.php (y .env.example → .env cuando esté presente), y estás listo para comenzar. Datos de muestra opcionales:
php runway migrate
# luego visita /posts y /api/posts
Alto Rendimiento
Flight es uno de los frameworks PHP más rápidos que existen. Su núcleo ligero significa menos sobrecarga y más velocidad—perfecto tanto para aplicaciones tradicionales como para flujos de trabajo modernos asistidos por IA. Puedes ver todos los benchmarks en TechEmpower
Mira el benchmark a continuación con algunos otros frameworks PHP populares.
| Framework | Reqs/seg Texto plano | Reqs/seg 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 e IA
¿Curioso sobre cómo Flight se combina con LLMs de codificación? Descubre cómo AGENTS.md, los comandos ai:* de Runway, y el diseño del esqueleto mantienen a los asistentes en el camino correcto.
Estabilidad y Compatibilidad con Versiones Anteriores
Valoramos tu tiempo. Todos hemos visto frameworks que se reinventan completamente cada par de años, dejando a los desarrolladores con código roto y migraciones costosas. Flight es diferente. Flight v3 fue diseñado como una mejora de v2, lo que significa que la API que conoces y amas no ha sido eliminada. De hecho, la mayoría de los proyectos de v2 funcionarán sin ningún cambio en v3.
Estamos comprometidos a mantener Flight estable para que puedas enfocarte en construir tu aplicación, no en arreglar tu framework. El esqueleto puede ser opinado para proyectos nuevos; las APIs del núcleo permanecen familiares para todos los demás.
Comunidad
Estamos en Matrix Chat
Y Discord
Contribuir
Hay dos formas en las que puedes contribuir a Flight:
- Contribuir al framework principal visitando el repositorio principal.
- ¡Ayuda a mejorar la documentación! Este sitio web de documentación está alojado en Github. Si encuentras un error o quieres mejorar algo, no dudes en enviar una solicitud de extracción. ¡Nos encantan las actualizaciones y nuevas ideas—especialmente alrededor de IA y nuevas tecnologías!
Requisitos
Flight requiere PHP 7.4 o superior.
Nota: PHP 7.4 es compatible porque en el momento actual de escritura (2024) PHP 7.4 es la versión predeterminada para algunas distribuciones Linux LTS. Forzar un cambio a PHP >8 causaría muchos problemas para esos usuarios. El framework también soporta PHP >8.
Licencia
Flight se publica bajo la licencia MIT.
Awesome-plugins/php_cookie
Cookies
overclokk/cookie es una biblioteca sencilla para administrar cookies dentro de su aplicación.
Instalación
La instalación es sencilla con composer.
composer require overclokk/cookie
Uso
El uso es tan simple como registrar un nuevo método en la clase Flight.
use Overclokk\Cookie\Cookie;
/*
* Establezca en su archivo bootstrap o public/index.php
*/
Flight::register('cookie', Cookie::class);
/**
* ExampleController.php
*/
class ExampleController {
public function login() {
// Establecer una cookie
// querrás que esto sea falso para obtener una nueva instancia
// usa el comentario a continuación si deseas el autocompletado
/** @var \Overclokk\Cookie\Cookie $cookie */
$cookie = Flight::cookie(false);
$cookie->set(
'stay_logged_in', // nombre de la cookie
'1', // el valor que deseas establecer
86400, // número de segundos que la cookie debe durar
'/', // ruta en la que estará disponible la cookie
'example.com', // dominio en el que estará disponible la cookie
true, // la cookie solo se transmitirá a través de una conexión segura HTTPS
true // la cookie solo estará disponible a través del protocolo HTTP
);
// opcionalmente, si deseas mantener los valores predeterminados
// y tener una forma rápida de establecer una cookie por mucho tiempo
$cookie->forever('stay_logged_in', '1');
}
public function home() {
// Verifica si tienes la cookie
if (Flight::cookie()->has('stay_logged_in')) {
// ponlos en el área del panel, por ejemplo.
Flight::redirect('/dashboard');
}
}
}Awesome-plugins/php_encryption
Cifrado en PHP
defuse/php-encryption es una biblioteca que se puede utilizar para cifrar y descifrar datos. Ponerse en marcha es bastante simple para comenzar a cifrar y descifrar datos. Tienen un excelente tutorial que ayuda a explicar los conceptos básicos sobre cómo utilizar la biblioteca, así como las importantes implicaciones de seguridad relacionadas con el cifrado.
Instalación
La instalación es sencilla con composer.
composer require defuse/php-encryption
Configuración
Luego necesitarás generar una clave de cifrado.
vendor/bin/generate-defuse-key
Esto generará una clave que deberás mantener segura. Podrías guardar la clave en tu archivo app/config/config.php en el array al final del archivo. Aunque no es el lugar perfecto, al menos es algo.
Uso
Ahora que tienes la biblioteca y una clave de cifrado, puedes empezar a cifrar y descifrar datos.
use Defuse\Crypto\Crypto;
use Defuse\Crypto\Key;
/*
* Establecerlo en tu archivo de inicio (bootstrap) o public/index.php
*/
// Método de cifrado
Flight::map('encrypt', function($datos_crudos) {
$clave_cifrado = /* $config['clave_cifrado'] o un file_get_contents de dónde pusiste la clave */;
return Crypto::encrypt($datos_crudos, Key::loadFromAsciiSafeString($clave_cifrado));
});
// Método de descifrado
Flight::map('decrypt', function($datos_cifrados) {
$clave_cifrado = /* $config['clave_cifrado'] o un file_get_contents de dónde pusiste la clave */;
try {
$datos_crudos = Crypto::decrypt($datos_cifrados, Key::loadFromAsciiSafeString($clave_cifrado));
} catch (Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException $ex) {
// ¡Un ataque! Se cargó la clave incorrecta o el texto cifrado
// ha cambiado desde que fue creado, ya sea corrompido en la base de datos o modificado intencionalmente por Eve tratando de llevar a cabo un ataque.
// ... manejar este caso de una manera adecuada para tu aplicación ...
}
return $datos_crudos;
});
Flight::route('/cifrar', function() {
$datos_cifrados = Flight::encrypt('Esto es un secreto');
echo $datos_cifrados;
});
Flight::route('/descifrar', function() {
$datos_cifrados = '...'; // Obtener los datos cifrados de algún lugar
$datos_descifrados = Flight::decrypt($datos_cifrados);
echo $datos_descifrados;
});Awesome-plugins/php_file_cache
flightphp/cache
Clase ligera, simple e independiente de caché en archivo PHP bifurcada de Wruczek/PHP-File-Cache
Ventajas
- Ligera, independiente y simple
- Todo el código en un archivo - sin controladores innecesarios.
- Segura - cada archivo de caché generado tiene una cabecera php con die, haciendo imposible el acceso directo incluso si alguien conoce la ruta y tu servidor no está configurado correctamente
- Bien documentada y probada
- Maneja la concurrencia correctamente mediante flock
- Compatible con PHP 7.4+
- Gratuita bajo licencia MIT
¡Este sitio de documentación está usando esta librería para cachear cada una de las páginas!
Haz clic aquí para ver el código.
Instalación
Instalar mediante composer:
composer require flightphp/cache
Uso
El uso es bastante sencillo. Esto guarda un archivo de caché en el directorio de caché.
use flight\Cache;
$app = Flight::app();
// Pasas el directorio donde se almacenará la caché en el constructor
$app->register('cache', Cache::class, [ __DIR__ . '/../cache/' ], function(Cache $cache) {
// Esto asegura que la caché solo se use en modo producción
// ENVIRONMENT es una constante que se establece en tu archivo bootstrap o en otro lugar de tu aplicación
$cache->setDevMode(ENVIRONMENT === 'development');
});
Obtener un Valor de Caché
Usas el método get() para obtener un valor en caché. Si quieres un método de conveniencia que actualice la caché si está expirada, puedes usar refreshIfExpired().
// Obtener instancia de caché
$cache = Flight::cache();
$data = $cache->refreshIfExpired('simple-cache-test', function () {
return date("H:i:s"); // devolver datos para almacenar en caché
}, 10); // 10 segundos
// o
$data = $cache->get('simple-cache-test');
if(empty($data)) {
$data = date("H:i:s");
$cache->set('simple-cache-test', $data, 10); // 10 segundos
}
Almacenar un Valor de Caché
Usas el método set() para almacenar un valor en la caché.
Flight::cache()->set('simple-cache-test', 'mis datos en caché', 10); // 10 segundos
Borrar un Valor de Caché
Usas el método delete() para borrar un valor en la caché.
Flight::cache()->delete('simple-cache-test');
Comprobar si Existe un Valor de Caché
Usas el método exists() para comprobar si un valor existe en la caché.
if(Flight::cache()->exists('simple-cache-test')) {
// hacer algo
}
Limpiar la Caché
Usas el método flush() para limpiar toda la caché.
Flight::cache()->flush();
Extraer metadatos con caché
Si quieres extraer marcas de tiempo y otros metadatos sobre una entrada de caché, asegúrate de pasar true como parámetro correcto.
$data = $cache->refreshIfExpired("simple-cache-meta-test", function () {
echo "¡Actualizando datos!" . PHP_EOL;
return date("H:i:s"); // devolver datos para almacenar en caché
}, 10, true); // true = devolver con metadatos
// o
$data = $cache->get("simple-cache-meta-test", true); // true = devolver con metadatos
/*
Ejemplo de elemento en caché recuperado con metadatos:
{
"time":1511667506, <-- marca de tiempo unix de guardado
"expire":10, <-- tiempo de expiración en segundos
"data":"04:38:26", <-- datos deserializados
"permanent":false
}
Usando metadatos, podemos, por ejemplo, calcular cuándo se guardó el elemento o cuándo expira
También podemos acceder a los datos en sí con la clave "data"
*/
$expiresin = ($data["time"] + $data["expire"]) - time(); // obtener marca de tiempo unix cuando los datos expiren y restar la marca de tiempo actual
$cacheddate = $data["data"]; // accedemos a los datos en sí con la clave "data"
echo "Último guardado de caché: $cacheddate, expira en $expiresin segundos";
Código Fuente
Visita https://github.com/flightphp/cache para ver el código.
Awesome-plugins/permissions
FlightPHP/Permisos
Este es un módulo de permisos que se puede usar en sus proyectos si tiene múltiples roles en su aplicación y cada rol tiene una funcionalidad un poco diferente. Este módulo le permite definir permisos para cada rol y luego verificar si el usuario actual tiene permiso para acceder a una determinada página o realizar una determinada acción.
Haga clic here para ver el repositorio en GitHub.
Instalación
Ejecute composer require flightphp/permissions ¡y listo!
Uso
Primero necesita configurar sus permisos, luego le dice a su aplicación lo que significan los permisos. En última instancia, verificará sus permisos con $Permissions->has(), ->can(), o is(). has() y can() tienen la misma funcionalidad, pero se nombran de manera diferente para hacer su código más legible.
Ejemplo Básico
Supongamos que tiene una característica en su aplicación que verifica si un usuario ha iniciado sesión. Puede crear un objeto de permisos como este:
// index.php
require 'vendor/autoload.php';
// algún código
// luego probablemente tenga algo que le diga cuál es el rol actual de la persona
// probablemente tenga algo donde extraiga el rol actual
// de una variable de sesión que define esto
// después de que alguien inicie sesión, de lo contrario tendrán un rol 'guest' o 'public'.
$current_role = 'admin';
// configurar permisos
$permission = new \flight\Permission($current_role);
$permission->defineRule('loggedIn', function($current_role) {
return $current_role !== 'guest';
});
// Probablemente querrá persistir este objeto en Flight en algún lugar
Flight::set('permission', $permission);
Luego en algún controlador, podría tener algo como esto.
<?php
// algún controlador
class SomeController {
public function someAction() {
$permission = Flight::get('permission');
if ($permission->has('loggedIn')) {
// hacer algo
} else {
// hacer algo más
}
}
}
También puede usar esto para rastrear si tienen permiso para hacer algo en su aplicación. Por ejemplo, si tiene una forma en que los usuarios pueden interactuar con publicaciones en su software, puede verificar si tienen permiso para realizar ciertas acciones.
$current_role = 'admin';
// configurar permisos
$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);
Luego en algún controlador...
class PostController {
public function create() {
$permission = Flight::get('permission');
if ($permission->can('post.create')) {
// hacer algo
} else {
// hacer algo más
}
}
}
Inyectando dependencias
Puede inyectar dependencias en el cierre que define los permisos. Esto es útil si tiene algún tipo de interruptor, id, o cualquier otro punto de datos que desee verificar. Lo mismo funciona para llamadas de tipo Clase->Método, excepto que define los argumentos en el método.
Cierres
$Permission->defineRule('order', function(string $current_role, MyDependency $MyDependency = null) {
// ... código
});
// en su archivo de controlador
public function createOrder() {
$MyDependency = Flight::myDependency();
$permission = Flight::get('permission');
if ($permission->can('order.create', $MyDependency)) {
// hacer algo
} else {
// hacer algo más
}
}
Clases
namespace MyApp;
class Permissions {
public function order(string $current_role, MyDependency $MyDependency = null) {
// ... código
}
}
Atajo para establecer permisos con clases
También puede usar clases para definir sus permisos. Esto es útil si tiene muchos permisos y desea mantener su código limpio. Puede hacer algo como esto:
<?php
// código de bootstrap
$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) {
// Asumiendo que configuró esto de antemano
/** @var \flight\database\SimplePdo $db */
$db = Flight::db();
$allowed_permissions = [ 'read' ]; // todos pueden ver un pedido
if($current_role === 'manager') {
$allowed_permissions[] = 'create'; // los gerentes pueden crear pedidos
}
$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'; // si el usuario tiene un interruptor especial, puede actualizar pedidos
}
if($current_role === 'admin') {
$allowed_permissions[] = 'delete'; // los administradores pueden eliminar pedidos
}
return $allowed_permissions;
}
}
La parte genial es que también hay un atajo que puede usar (¡que también se puede cachear!) donde simplemente le dice a la clase de permisos que mapee todos los métodos en una clase a permisos. Entonces, si tiene un método llamado order() y un método llamado company(), estos se mapearán automáticamente para que pueda simplemente ejecutar $Permissions->has('order.read') o $Permissions->has('company.read') y funcionará. Definir esto es muy difícil, así que quédese conmigo. Solo necesita hacer esto:
Cree la clase de permisos que desea agrupar.
class MyPermissions {
public function order(string $current_role, int $order_id = 0): array {
// código para determinar permisos
return $permissions_array;
}
public function company(string $current_role, int $company_id): array {
// código para determinar permisos
return $permissions_array;
}
}
Luego haga que los permisos sean descubribles usando esta biblioteca.
$Permissions = new \flight\Permission($current_role);
$Permissions->defineRulesFromClassMethods(MyApp\Permissions::class);
Flight::set('permissions', $Permissions);
Finalmente, llame al permiso en su base de código para verificar si el usuario tiene permitido realizar un permiso dado.
class SomeController {
public function createOrder() {
if(Flight::get('permissions')->can('order.create') === false) {
die('You can\'t create an order. Sorry!');
}
}
}
Caché
Para habilitar el caché, vea la simple biblioteca wruczak/phpfilecache. Un ejemplo de cómo habilitar esto está a continuación.
// este $app puede ser parte de su código, o
// puede simplemente pasar null y
// extraerá de Flight::app() en el constructor
$app = Flight::app();
// Por ahora acepta esto como un caché de archivos. Otros pueden
// agregarse fácilmente en el futuro.
$Cache = new Wruczek\PhpFileCache\PhpFileCache;
$Permissions = new \flight\Permission($current_role, $app, $Cache);
$Permissions->defineRulesFromClassMethods(MyApp\Permissions::class, 3600); // 3600 es cuántos segundos cachear esto. Déjelo en blanco para no usar caché
¡Y listo!
Awesome-plugins/simple_job_queue
Cola de Trabajos Simple
La Cola de Trabajos Simple es una biblioteca que se puede utilizar para procesar trabajos de forma asíncrona. Se puede usar con beanstalkd, MySQL/MariaDB, SQLite y PostgreSQL.
Instalar
composer require n0nag0n/simple-job-queue
Uso
Para que esto funcione, necesitas una manera de agregar trabajos a la cola y una manera de procesar los trabajos (un trabajador). A continuación se presentan ejemplos de cómo agregar un trabajo a la cola y cómo procesar el trabajo.
Agregando a Flight
Agregar esto a Flight es simple y se hace utilizando el método register(). A continuación se muestra un ejemplo de cómo agregar esto a Flight.
<?php
require 'vendor/autoload.php';
// Cambia ['mysql'] a ['beanstalkd'] si deseas usar beanstalkd
Flight::register('queue', n0nag0n\Job_Queue::class, ['mysql'], function($Job_Queue) {
// si ya tienes una conexión PDO en Flight::db();
$Job_Queue->addQueueConnection(Flight::db());
// o si estás usando beanstalkd/Pheanstalk
$pheanstalk = Pheanstalk\Pheanstalk::create('127.0.0.1');
$Job_Queue->addQueueConnection($pheanstalk);
});
Agregando un nuevo trabajo
Cuando agregas un trabajo, necesitas especificar un pipeline (cola). Esto es comparable a un canal en RabbitMQ o un tubo en beanstalkd.
<?php
Flight::queue()->selectPipeline('send_important_emails');
Flight::queue()->addJob(json_encode([ 'something' => 'that', 'ends' => 'up', 'a' => 'string' ]));
Ejecutando un trabajador
Aquí hay un archivo de ejemplo de cómo ejecutar un trabajador.
<?php
require 'vendor/autoload.php';
$Job_Queue = new n0nag0n\Job_Queue('mysql');
// Conexión PDO
$PDO = new PDO('mysql:dbname=testdb;host=127.0.0.1', 'user', 'pass');
$Job_Queue->addQueueConnection($PDO);
// o si estás usando 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();
// ajusta lo que te haga dormir mejor por la noche (solo para colas de base de datos, beanstalkd no necesita esta declaración if)
if(empty($job)) {
usleep(500000);
continue;
}
echo "Procesando {$job['id']}\n";
$payload = json_decode($job['payload'], true);
try {
$result = doSomethingThatDoesSomething($payload);
if($result === true) {
$Job_Queue->deleteJob($job);
} else {
// esto lo saca de la cola lista y lo coloca en otra cola que puede ser recogida y "patada" más tarde.
$Job_Queue->buryJob($job);
}
} catch(Exception $e) {
$Job_Queue->buryJob($job);
}
}
Manejo de Procesos Largos con Supervisord
Supervisord es un sistema de control de procesos que garantiza que tus procesos de trabajo sigan funcionando continuamente. Aquí hay una guía más completa sobre cómo configurarlo con tu trabajador de Cola de Trabajos Simple:
Instalando Supervisord
# En Ubuntu/Debian
sudo apt-get install supervisor
# En CentOS/RHEL
sudo yum install supervisor
# En macOS con Homebrew
brew install supervisor
Creando un Script de Trabajador
Primero, guarda tu código de trabajador en un archivo PHP dedicado:
<?php
require 'vendor/autoload.php';
$Job_Queue = new n0nag0n\Job_Queue('mysql');
// Conexión PDO
$PDO = new PDO('mysql:dbname=your_database;host=127.0.0.1', 'username', 'password');
$Job_Queue->addQueueConnection($PDO);
// Establecer el pipeline a observar
$Job_Queue->watchPipeline('send_important_emails');
// Registrar inicio del trabajador
echo date('Y-m-d H:i:s') . " - Trabajador iniciado\n";
while(true) {
$job = $Job_Queue->getNextJobAndReserve();
if(empty($job)) {
usleep(500000); // Dormir durante 0.5 segundos
continue;
}
echo date('Y-m-d H:i:s') . " - Procesando trabajo {$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') . " - Trabajo {$job['id']} completado con éxito\n";
} else {
$Job_Queue->buryJob($job);
echo date('Y-m-d H:i:s') . " - Trabajo {$job['id']} falló, enterrado\n";
}
} catch(Exception $e) {
$Job_Queue->buryJob($job);
echo date('Y-m-d H:i:s') . " - Excepción al procesar el trabajo {$job['id']}: {$e->getMessage()}\n";
}
}
Configurando Supervisord
Crea un archivo de configuración para tu trabajador:
[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
Opciones Clave de Configuración:
command: El comando para ejecutar tu trabajadordirectory: Directorio de trabajo para el trabajadorautostart: Iniciar automáticamente cuando supervisord iniciaautorestart: Reiniciar automáticamente si el proceso salestartretries: Número de veces para intentar iniciar si fallastderr_logfile/stdout_logfile: Ubicaciones de los archivos de registrouser: Usuario del sistema para ejecutar el procesonumprocs: Número de instancias de trabajador a ejecutarprocess_name: Formato de nombre para múltiples procesos de trabajador
Gestionando Trabajadores con Supervisorctl
Después de crear o modificar la configuración:
# Recargar configuración de supervisor
sudo supervisorctl reread
sudo supervisorctl update
# Controlar procesos de trabajadores específicos
sudo supervisorctl start email_worker:*
sudo supervisorctl stop email_worker:*
sudo supervisorctl restart email_worker:*
sudo supervisorctl status email_worker:*
Ejecutando Múltiples Pipelines
Para múltiples pipelines, crea archivos y configuraciones de trabajador separados:
[program:email_worker]
command=php /path/to/email_worker.php
# ... otras configuraciones ...
[program:notification_worker]
command=php /path/to/notification_worker.php
# ... otras configuraciones ...
Monitoreo y Registros
Verifica los registros para monitorear la actividad del trabajador:
# Ver registros
sudo tail -f /var/log/simple_job_queue.log
# Verificar estado
sudo supervisorctl status
Esta configuración asegura que tus trabajadores de trabajos continúen funcionando incluso después de fallos, reinicios del servidor u otros problemas, haciendo que tu sistema de colas sea confiable para entornos de producción.
Awesome-plugins/jwt
Firebase JWT - Autenticación con JSON Web Token
JWT (JSON Web Tokens) son una forma compacta y segura para URLs de representar afirmaciones entre tu aplicación y un cliente. ¡Son perfectos para la autenticación de API sin estado, sin necesidad de almacenamiento de sesiones en el servidor! Esta guía te muestra cómo integrar Firebase JWT con Flight para una autenticación segura basada en tokens.
Visita el repositorio de Github para obtener la documentación completa y detalles.
¿Qué es JWT?
Un JSON Web Token es una cadena que contiene tres partes:
- Header: Metadatos sobre el token (algoritmo, tipo)
- Payload: Tus datos (ID de usuario, roles, expiración, etc.)
- Signature: Firma criptográfica para verificar la autenticidad
Ejemplo de JWT: eyJ0eXAiOiJKV1QiLCJhbGc... (parece gibberish, ¡pero es data estructurada!)
¿Por qué usar JWT?
- Sin estado: No se necesita almacenamiento de sesiones en el servidor, perfecto para microservicios y APIs
- Escalable: Funciona genial con balanceadores de carga ya que no hay requisito de afinidad de sesión
- Multi-dominio: Puede usarse a través de diferentes dominios y servicios
- Amigable con móviles: Genial para apps móviles donde las cookies pueden no funcionar bien
- Estandarizado: Enfoque estándar de la industria (RFC 7519)
Instalación
Instala vía Composer:
composer require firebase/php-jwt
Uso básico
Aquí hay un ejemplo rápido de creación y verificación de un JWT:
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
// Tu clave secreta (¡MANTÉNLA SEGURA!)
$secretKey = 'your-256-bit-secret-key-here-keep-it-safe';
// Crear un token
$payload = [
'user_id' => 123,
'username' => 'johndoe',
'role' => 'admin',
'iat' => time(), // Emitido en
'exp' => time() + 3600 // Expira en 1 hora
];
$jwt = JWT::encode($payload, $secretKey, 'HS256');
echo "Token: " . $jwt;
// Verificar y decodificar un token
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 para Flight (Enfoque recomendado)
La forma más común y útil de usar JWT con Flight es como middleware para proteger tus rutas de API. Aquí hay un ejemplo completo y listo para producción:
Paso 1: Crear una clase de middleware JWT
// 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;
// Almacena tu clave secreta en app/config/config.php, ¡NO la codifiques de forma fija!
$this->secretKey = $app->get('config')['jwt_secret'];
}
public function before(array $params) {
$authHeader = $this->app->request()->getHeader('Authorization');
// Verificar si existe el header de autorización
if (empty($authHeader)) {
$this->app->jsonHalt(['error' => 'No se proporcionó token de autorización'], 401);
}
// Extraer el token del formato "Bearer <token>"
if (!preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches)) {
$this->app->jsonHalt(['error' => 'Formato de autorización inválido. Usa: Bearer <token>'], 401);
}
$jwt = $matches[1];
try {
// Decodificar y verificar el token
$decoded = JWT::decode($jwt, new Key($this->secretKey, 'HS256'));
// Almacenar datos de usuario en la solicitud para usar en manejadores de rutas
$this->app->request()->data->user = $decoded;
} catch (ExpiredException $e) {
$this->app->jsonHalt(['error' => 'El token ha expirado'], 401);
} catch (SignatureInvalidException $e) {
$this->app->jsonHalt(['error' => 'Firma de token inválida'], 401);
} catch (Exception $e) {
$this->app->jsonHalt(['error' => 'Token inválido: ' . $e->getMessage()], 401);
}
}
}
Paso 2: Registrar la clave secreta JWT en tu configuración
// app/config/config.php
return [
'jwt_secret' => getenv('JWT_SECRET') ?: 'your-fallback-secret-for-development'
];
// app/config/bootstrap.php o index.php
// asegúrate de agregar esta línea si quieres exponer la configuración a la app
$app->set('config', $config);
Nota de seguridad: ¡Nunca codifiques de forma fija tu clave secreta! Usa variables de entorno en producción.
Paso 3: Proteger tus rutas con middleware
// Proteger una ruta individual
Flight::route('GET /api/user/profile', function() {
$user = Flight::request()->data->user; // Establecido por el middleware
Flight::json([
'user_id' => $user->user_id,
'username' => $user->username,
'role' => $user->role
]);
})->addMiddleware(JwtMiddleware::class);
// Proteger un grupo completo de rutas (¡más común!)
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 ]); // ¡Todas las rutas en este grupo están protegidas!
Para más detalles sobre middleware, consulta la documentación de middleware.
Casos de uso comunes
1. Endpoint de login (Generación de token)
Crea una ruta que genera un JWT después de una autenticación exitosa:
Flight::route('POST /api/login', function() {
$data = Flight::request()->data;
$username = $data->username ?? '';
$password = $data->password ?? '';
// Validar credenciales (ejemplo - usa tu propia lógica!)
$user = validateUserCredentials($username, $password);
if (!$user) {
Flight::jsonHalt(['error' => 'Credenciales inválidas'], 401);
}
// Generar JWT
$secretKey = Flight::get('config')['jwt_secret'];
$payload = [
'user_id' => $user->id,
'username' => $user->username,
'role' => $user->role,
'iat' => time(),
'exp' => time() + (60 * 60) // Expiración en 1 hora
];
$jwt = JWT::encode($payload, $secretKey, 'HS256');
Flight::json([
'success' => true,
'token' => $jwt,
'expires_in' => 3600
]);
});
function validateUserCredentials($username, $password) {
// Tu búsqueda en la base de datos y verificación de contraseña aquí
// Ejemplo:
$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. Flujo de renovación de token
Implementa un sistema de token de renovación para sesiones de larga duración:
Flight::route('POST /api/login', function() {
// ... validar credenciales ...
$secretKey = Flight::get('config')['jwt_secret'];
$refreshSecret = Flight::get('config')['jwt_refresh_secret'];
// Token de acceso de corta duración (15 minutos)
$accessToken = JWT::encode([
'user_id' => $user->id,
'type' => 'access',
'iat' => time(),
'exp' => time() + (15 * 60)
], $secretKey, 'HS256');
// Token de renovación de larga duración (7 días)
$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'));
// Verificar que sea un token de renovación
if ($decoded->type !== 'refresh') {
Flight::jsonHalt(['error' => 'Tipo de token inválido'], 401);
}
// Generar nuevo token de acceso
$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' => 'Token de renovación inválido'], 401);
}
});
3. Control de acceso basado en roles
Extiende tu middleware para verificar roles de usuario:
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) {
// Asume que JwtMiddleware ya se ejecutó y estableció los datos de usuario
$user = $this->app->request()->data->user ?? null;
if (!$user) {
$this->app->jsonHalt(['error' => 'Autenticación requerida'], 401);
}
// Verificar si el usuario tiene el rol requerido
if (!empty($this->allowedRoles) && !in_array($user->role, $this->allowedRoles)) {
$this->app->jsonHalt(['error' => 'Permisos insuficientes'], 403);
}
}
}
// Uso: Ruta solo para admin
Flight::route('DELETE /api/users/@id', function($id) {
// Lógica de eliminación de usuario
})->addMiddleware([
JwtMiddleware::class,
new JwtRoleMiddleware(Flight::app(), ['admin'])
]);
4. API pública con limitación de tasa por usuario
Usa JWT para rastrear y limitar la tasa de usuarios sin sesiones:
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";
// Asegúrate de configurar un servicio de caché en app/config/services.php
$requests = Flight::cache()->get($cacheKey, 0);
if ($requests >= 100) { // 100 solicitudes por hora
Flight::jsonHalt(['error' => 'Límite de tasa excedido'], 429);
}
Flight::cache()->set($cacheKey, $requests + 1, 3600);
}
}
Mejores prácticas de seguridad
1. Usa claves secretas fuertes
// Genera una clave secreta segura (ejecuta una vez, guárdala en archivo .env)
$secretKey = base64_encode(random_bytes(32));
echo $secretKey; // ¡Almacena esto en tu archivo .env!
2. Almacena secretos en variables de entorno
// ¡Nunca comprometas secretos en control de versiones!
// Usa un archivo .env y una librería como vlucas/phpdotenv
// Archivo .env:
// JWT_SECRET=your-base64-encoded-secret-here
// JWT_REFRESH_SECRET=another-base64-encoded-secret-here
// También puedes usar el archivo app/config/config.php para almacenar tus secretos
// solo asegúrate de que el archivo de configuración no se comprometa en control de versiones
// return [
// 'jwt_secret' => 'your-base64-encoded-secret-here',
// 'jwt_refresh_secret' => 'another-base64-encoded-secret-here',
// ];
// En tu app:
$secretKey = getenv('JWT_SECRET');
3. Establece tiempos de expiración apropiados
// Buena práctica: tokens de acceso de corta duración
'exp' => time() + (15 * 60) // 15 minutos
// Para tokens de renovación: expiración más larga
'exp' => time() + (7 * 24 * 60 * 60) // 7 días
4. Usa HTTPS en producción
Los JWT deben siempre transmitirse sobre HTTPS. ¡Nunca envíes tokens sobre HTTP plano en producción!
5. Valida las afirmaciones del token
Siempre valida las afirmaciones que te importan:
$decoded = JWT::decode($jwt, new Key($secretKey, 'HS256'));
// La verificación de expiración se maneja automáticamente por la librería
// Pero puedes agregar validaciones personalizadas:
if ($decoded->iat > time()) {
throw new Exception('Token usado antes de que se emitiera');
}
if (isset($decoded->nbf) && $decoded->nbf > time()) {
throw new Exception('Token aún no válido');
}
6. Considera la lista negra de tokens para logout
Para seguridad extra, mantén una lista negra de tokens invalidados:
Flight::route('POST /api/logout', function() {
$authHeader = Flight::request()->getHeader('Authorization');
preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches);
$jwt = $matches[1];
// Extraer la expiración del token
$decoded = Flight::request()->data->user;
$ttl = $decoded->exp - time();
// Almacenar en caché/redis hasta la expiración
Flight::cache()->set("blacklist:$jwt", true, $ttl);
Flight::json(['message' => 'Cierre de sesión exitoso']);
});
// Agregar a tu JwtMiddleware:
public function before(array $params) {
// ... extraer JWT ...
// Verificar lista negra
if (Flight::cache()->get("blacklist:$jwt")) {
$this->app->jsonHalt(['error' => 'El token ha sido revocado'], 401);
}
// ... verificar token ...
}
Algoritmos y tipos de claves
Firebase JWT soporta múltiples algoritmos:
Algoritmos simétricos (HMAC)
- HS256 (Recomendado para la mayoría de apps): Usa una sola clave secreta
- HS384, HS512: Variantes más fuertes
$jwt = JWT::encode($payload, $secretKey, 'HS256');
$decoded = JWT::decode($jwt, new Key($secretKey, 'HS256'));
Algoritmos asimétricos (RSA/ECDSA)
- RS256, RS384, RS512: Usa pares de claves pública/privada
- ES256, ES384, ES512: Variantes de curva elíptica
// Generar claves: 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');
// Codificar con clave privada
$jwt = JWT::encode($payload, $privateKey, 'RS256');
// Decodificar con clave pública
$decoded = JWT::decode($jwt, new Key($publicKey, 'RS256'));
Cuándo usar RSA: Usa RSA cuando necesites distribuir la clave pública para verificación (p.ej., microservicios, integraciones de terceros). Para una sola aplicación, HS256 es más simple y suficiente.
Solución de problemas
Error "Token expirado"
La afirmación exp de tu token está en el pasado. Emite un nuevo token o implementa renovación de token.
"Fallo en la verificación de firma"
- Estás usando una clave secreta diferente para decodificar que la que usaste para codificar
- El token ha sido manipulado
- Desfase de reloj entre servidores (agrega un buffer de tolerancia)
use Firebase\JWT\JWT;
JWT::$leeway = 60; // Permitir 60 segundos de desfase de reloj
$decoded = JWT::decode($jwt, new Key($secretKey, 'HS256'));
Token no se envía en solicitudes
Asegúrate de que tu cliente envíe el header Authorization:
// Ejemplo en JavaScript
fetch('/api/users', {
headers: {
'Authorization': 'Bearer ' + token
}
});
Métodos
La librería Firebase JWT proporciona estos métodos principales:
JWT::encode(array $payload, string $key, string $alg): Crea un JWT a partir de un payloadJWT::decode(string $jwt, Key $key): Decodifica y verifica un JWTJWT::urlsafeB64Encode(string $input): Codificación Base64 segura para URLJWT::urlsafeB64Decode(string $input): Decodificación Base64 segura para URLJWT::$leeway: Propiedad estática para establecer tolerancia de tiempo para validación (en segundos)
¿Por qué usar esta librería?
- Estándar de la industria: Firebase JWT es la librería JWT más popular y confiable para PHP
- Mantenimiento activo: Mantenida por el equipo de Google/Firebase
- Enfocada en seguridad: Actualizaciones regulares y parches de seguridad
- API simple: Fácil de entender e implementar
- Bien documentada: Documentación extensa y soporte comunitario
- Flexible: Soporta múltiples algoritmos y opciones configurables
Ver también
- Repositorio de Github de Firebase JWT
- JWT.io - Depura y decodifica JWTs
- RFC 7519 - Especificación oficial de JWT
- Documentación de middleware de Flight
- Plugin de sesión de Flight - Para autenticación basada en sesiones tradicionales
Licencia
La librería Firebase JWT está licenciada bajo la Licencia BSD 3-Clause. Consulta el repositorio de Github para detalles.
Awesome-plugins/n0nag0n_wordpress
Integración de WordPress: n0nag0n/wordpress-integration-for-flight-framework
¿Quieres usar Flight PHP dentro de tu sitio de WordPress? ¡Este plugin lo hace muy fácil! Con n0nag0n/wordpress-integration-for-flight-framework, puedes ejecutar una aplicación completa de Flight junto con tu instalación de WordPress, perfecto para construir APIs personalizadas, microservicios o incluso aplicaciones completas sin salir de la comodidad de WordPress.
¿Qué hace?
- Integra de manera perfecta Flight PHP con WordPress
- Enruta solicitudes a Flight o WordPress según patrones de URL
- Organiza tu código con controladores, modelos y vistas (MVC)
- Configura fácilmente la estructura de carpetas recomendada de Flight
- Usa la conexión de base de datos de WordPress o la tuya propia
- Ajusta finamente cómo interactúan Flight y WordPress
- Interfaz de administración simple para la configuración
Instalación
- Sube la carpeta
flight-integrationa tu directorio/wp-content/plugins/. - Activa el plugin en la administración de WordPress (menú de Plugins).
- Ve a Settings > Flight Framework para configurar el plugin.
- Establece la ruta del proveedor a tu instalación de Flight (o usa Composer para instalar Flight).
- Configura la ruta de tu carpeta de aplicación y crea la estructura de carpetas (¡el plugin puede ayudarte con esto!).
- ¡Comienza a construir tu aplicación de Flight!
Ejemplos de Uso
Ejemplo Básico de Ruta
En tu archivo app/config/routes.php:
Flight::route('GET /api/hello', function() {
Flight::json(['message' => 'Hello World!']);
});
Ejemplo de Controlador
Crea un controlador en app/controllers/ApiController.php:
namespace app\controllers;
use Flight;
class ApiController {
public function getUsers() {
// ¡Puedes usar funciones de WordPress dentro de Flight!
$users = get_users();
$result = [];
foreach($users as $user) {
$result[] = [
'id' => $user->ID,
'name' => $user->display_name,
'email' => $user->user_email
];
}
Flight::json($result);
}
}
Luego, en tu routes.php:
Flight::route('GET /api/users', [app\controllers\ApiController::class, 'getUsers']);
Preguntas Frecuentes
P: ¿Necesito conocer Flight para usar este plugin?
R: Sí, esto es para desarrolladores que quieran usar Flight dentro de WordPress. Se recomienda un conocimiento básico de enrutamiento y manejo de solicitudes de Flight.
P: ¿Esto ralentizará mi sitio de WordPress?
R: ¡No! El plugin solo procesa solicitudes que coincidan con tus rutas de Flight. Todas las demás solicitudes van a WordPress como de costumbre.
P: ¿Puedo usar funciones de WordPress en mi aplicación de Flight?
R: ¡Absolutamente! Tienes acceso completo a todas las funciones, hooks y globales de WordPress desde dentro de tus rutas y controladores de Flight.
P: ¿Cómo creo rutas personalizadas?
R: Define tus rutas en el archivo config/routes.php en tu carpeta de aplicación. Consulta el archivo de muestra creado por el generador de estructura de carpetas para ejemplos.
Registro de Cambios
1.0.0
Lanzamiento inicial.
Para más información, consulta el GitHub repo.
Awesome-plugins/ghost_session
Ghostff/Session
Administrador de sesiones PHP (no bloqueante, flash, segment, encriptación de sesión). Usa PHP open_ssl para encriptación/desencriptación opcional de datos de sesión. Admite File, MySQL, Redis y Memcached.
Haz clic aquí para ver el código.
Instalación
Instala con composer.
composer require ghostff/session
Configuración Básica
No es necesario pasar nada para usar la configuración predeterminada con tu sesión. Puedes leer sobre más configuraciones en el Github Readme.
use Ghostff\Session\Session;
require 'vendor/autoload.php';
$app = Flight::app();
$app->register('session', Session::class);
// una cosa a recordar es que debes confirmar tu sesión en cada carga de página
// o tendrás que ejecutar auto_commit en tu configuración.
Ejemplo Simple
Aquí hay un ejemplo simple de cómo podrías usar esto.
Flight::route('POST /login', function() {
$session = Flight::session();
// haz tu lógica de inicio de sesión aquí
// valida la contraseña, etc.
// si el inicio de sesión es exitoso
$session->set('is_logged_in', true);
$session->set('user', $user);
// cualquier vez que escribas en la sesión, debes confirmarla deliberadamente.
$session->commit();
});
// Esta verificación podría estar en la lógica de la página restringida, o envuelta con middleware.
Flight::route('/some-restricted-page', function() {
$session = Flight::session();
if(!$session->get('is_logged_in')) {
Flight::redirect('/login');
}
// haz tu lógica de página restringida aquí
});
// la versión con middleware
Flight::route('/some-restricted-page', function() {
// lógica de página regular
})->addMiddleware(function() {
$session = Flight::session();
if(!$session->get('is_logged_in')) {
Flight::redirect('/login');
}
});
Ejemplo Más Complejo
Aquí hay un ejemplo más complejo de cómo podrías usar esto.
use Ghostff\Session\Session;
require 'vendor/autoload.php';
$app = Flight::app();
// establece una ruta personalizada a tu archivo de configuración de sesión como el primer argumento
// o dale el arreglo personalizado
$app->register('session', Session::class, [
[
// si quieres almacenar tus datos de sesión en una base de datos (bueno si quieres algo como, "cerrar sesión en todos los dispositivos" funcionalidad)
Session::CONFIG_DRIVER => Ghostff\Session\Drivers\MySql::class,
Session::CONFIG_ENCRYPT_DATA => true,
Session::CONFIG_SALT_KEY => hash('sha256', 'my-super-S3CR3T-salt'), // por favor cambia esto a algo más
Session::CONFIG_AUTO_COMMIT => true, // solo haz esto si es necesario y/o es difícil de confirmar() tu sesión.
// adicionalmente podrías hacer Flight::after('start', function() { Flight::session()->commit(); });
Session::CONFIG_MYSQL_DS => [
'driver' => 'mysql', # Controlador de base de datos para PDO dns ej(mysql:host=...;dbname=...)
'host' => '127.0.0.1', # Host de la base de datos
'db_name' => 'my_app_database', # Nombre de la base de datos
'db_table' => 'sessions', # Tabla de la base de datos
'db_user' => 'root', # Nombre de usuario de la base de datos
'db_pass' => '', # Contraseña de la base de datos
'persistent_conn'=> false, # Evita el costo de establecer una nueva conexión cada vez que un script necesita hablar con una base de datos, lo que resulta en una aplicación web más rápida. ENCUENTRA EL LADO NEGATIVO TÚ MISMO
]
]
]);
¡Ayuda! ¡Mis Datos de Sesión No Se Están Manteniendo!
¿Estás estableciendo tus datos de sesión y no se mantienen entre solicitudes? Quizás olvidaste confirmar tus datos de sesión. Puedes hacer esto llamando a $session->commit() después de haber establecido tus datos de sesión.
Flight::route('POST /login', function() {
$session = Flight::session();
// haz tu lógica de inicio de sesión aquí
// valida la contraseña, etc.
// si el inicio de sesión es exitoso
$session->set('is_logged_in', true);
$session->set('user', $user);
// cualquier vez que escribas en la sesión, debes confirmarla deliberadamente.
$session->commit();
});
La otra forma de manejar esto es cuando configuras tu servicio de sesión, debes establecer auto_commit en true en tu configuración. Esto confirmará automáticamente tus datos de sesión después de cada solicitud.
$app->register('session', Session::class, [ 'path/to/session_config.php', bin2hex(random_bytes(32)) ], function(Session $session) {
$session->updateConfiguration([
Session::CONFIG_AUTO_COMMIT => true,
]);
}
);
Adicionalmente, podrías hacer Flight::after('start', function() { Flight::session()->commit(); }); para confirmar tus datos de sesión después de cada solicitud.
Documentación
Visita el Github Readme para la documentación completa. Las opciones de configuración están bien documentadas en el archivo default_config.php mismo. El código es simple de entender si quieres explorar este paquete tú mismo.
Awesome-plugins/mcp
Servidor MCP de FlightPHP
El Servidor MCP de FlightPHP proporciona a cualquier asistente de codificación de IA compatible con MCP acceso instantáneo y estructurado a toda la documentación de FlightPHP: enrutamiento, middleware, plugins, guías y más. En lugar de que tu IA alucine detalles de la API o adivine firmas de métodos, obtiene los documentos reales a demanda. Sin claves de API, sin instalación requerida para la versión alojada.
Visita el repositorio de GitHub para el código fuente completo y detalles.
Inicio Rápido
El servidor está alojado públicamente y listo para usar:
https://mcp.flightphp.com/mcp
Solo agrega esa URL a tu extensión de codificación de IA. Sin registro, sin credenciales. Consulta la sección Configuración de IDE a continuación para configuraciones de copiar y pegar para las herramientas más populares.
Qué Hace
Una vez conectado, tu asistente de IA puede:
- Explorar todos los documentos disponibles — listar cada tema principal, guía y página de plugin
- Obtener cualquier página de documentación — recuperar el contenido completo para enrutamiento, middleware, solicitudes, seguridad y más
- Buscar documentación de plugins — obtener la documentación completa para ActiveRecord, Session, Tracy, Runway y todos los otros plugins oficiales
- Seguir guías paso a paso — acceder a walkthroughs completos para construir blogs, APIs REST y aplicaciones probadas
- Buscar en todo — encontrar páginas relevantes en documentos principales, guías y plugins al mismo tiempo
Puntos Clave
- Cero configuración — el servidor alojado en
https://mcp.flightphp.com/mcpno requiere instalación ni claves de API. - Siempre actualizado — el servidor obtiene documentos en vivo de docs.flightphp.com, por lo que siempre está actualizado.
- Funciona en todas partes — cualquier herramienta que soporte el transporte HTTP Streamable de MCP puede conectarse.
- Autoalojable — ejecuta tu propia instancia con PHP >= 8.1 y Composer si lo prefieres.
Configuración de IDE / Extensión de IA
El servidor usa transporte HTTP Streamable. Elige tu extensión a continuación y pega la configuración.
Claude Code (CLI)
Ejecuta el siguiente comando para agregarlo a tu proyecto:
claude mcp add --transport http flightphp-docs https://mcp.flightphp.com/mcp
O agrégalo manualmente al archivo .mcp.json de tu proyecto:
{
"mcpServers": {
"flightphp-docs": {
"type": "http",
"url": "https://mcp.flightphp.com/mcp"
}
}
}
GitHub Copilot (VS Code)
Agrega a .vscode/mcp.json en tu espacio de trabajo:
{
"servers": {
"flightphp-docs": {
"type": "http",
"url": "https://mcp.flightphp.com/mcp"
}
}
}
Kilo Code (VS Code)
Agrega a tu settings.json de VS Code:
{
"kilocode.mcpServers": {
"flightphp-docs": {
"url": "https://mcp.flightphp.com/mcp",
"transport": "streamable-http"
}
}
}
Continue.dev (VS Code / JetBrains)
Agrega a ~/.continue/config.json:
{
"mcpServers": [
{
"name": "flightphp-docs",
"transport": {
"type": "http",
"url": "https://mcp.flightphp.com/mcp"
}
}
]
}
Herramientas Disponibles
El servidor MCP expone las siguientes herramientas a tu asistente de IA:
| Herramienta | Descripción |
|---|---|
list_docs_pages |
Lista todos los temas de documentación principal disponibles con slugs y descripciones |
get_docs_page |
Obtiene una página de documentos principal por slug de tema (p. ej. routing, middleware, security) |
list_guide_pages |
Lista todas las guías paso a paso disponibles |
get_guide_page |
Obtiene una guía completa por slug (p. ej. blog, unit-testing) |
list_plugin_pages |
Lista todas las páginas de plugins y extensiones disponibles |
get_plugin_docs |
Obtiene la documentación completa del plugin por slug (p. ej. active-record, session, jwt) |
search_docs |
Busca en todos los documentos, guías y plugins por una palabra clave o tema |
fetch_url |
Obtiene cualquier página directamente por su URL completa de docs.flightphp.com |
Autoalojamiento
¿Prefieres ejecutar tu propia instancia? Necesitarás PHP >= 8.1 y Composer.
git clone https://github.com/flightphp/mcp.git
cd mcp
composer install
php server.php
El servidor se inicia en http://0.0.0.0:8890/mcp por defecto. Actualiza la configuración de tu IDE para apuntar a tu dirección local:
{
"mcpServers": {
"flightphp-docs": {
"type": "http",
"url": "http://localhost:8890/mcp"
}
}
}Awesome-plugins/async
Async
Async es un paquete pequeño para el framework Flight que te permite ejecutar tus aplicaciones Flight dentro de servidores y entornos asíncronos como Swoole, AdapterMan, ReactPHP, Amp, RoadRunner, Workerman, etc. De fábrica incluye adaptadores para Swoole y AdapterMan.
El objetivo: desarrollar y depurar con PHP-FPM (o el servidor integrado) y cambiar a Swoole (u otro controlador asíncrono) para producción con cambios mínimos.
Requisitos
- PHP 7.4 o superior
- Framework Flight 3.16.1 o superior
- Extensión Swoole
Instalación
Instala vía composer:
composer require flightphp/async
Si planeas ejecutar con Swoole, instala la extensión:
# usando pecl
pecl install swoole
# o openswoole
pecl install openswoole
# o con un administrador de paquetes (ejemplo Debian/Ubuntu)
sudo apt-get install php-swoole
Ejemplo rápido de Swoole
A continuación se muestra una configuración mínima que ilustra cómo soportar tanto PHP-FPM (o servidor integrado) como Swoole utilizando el mismo código base.
Archivos que necesitarás en tu proyecto:
- index.php
- swoole_server.php
- SwooleServerDriver.php
index.php
Este archivo es un simple interruptor que fuerza a la aplicación a ejecutarse en modo PHP para desarrollo.
// index.php
<?php
define('NOT_SWOOLE', true);
include 'swoole_server.php';
swoole_server.php
Este archivo inicializa tu aplicación Flight y comenzará el controlador Swoole cuando NOT_SWOOLE no esté definido.
// 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 la clase SwooleServerDriver cuando se ejecute en modo Swoole.
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
Un controlador conciso que muestra cómo conectar solicitudes Swoole a Flight utilizando el AsyncBridge y los adaptadores de 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();
}
}
Ejecutando el servidor
- Desarrollo (servidor integrado de PHP / PHP-FPM):
- php -S localhost:8000 (o agrega -t public/ si tu index está en public/)
- Producción (Swoole):
- php swoole_server.php
Consejo: Para producción, usa un proxy inverso (Nginx) delante de Swoole para manejar TLS, archivos estáticos y balanceo de carga.
Notas de configuración
El controlador Swoole expone varias opciones de configuración:
- worker_num: número de procesos de worker
- max_request: solicitudes por worker antes del reinicio
- enable_coroutine: usar corutinas para concurrencia
- buffer_output_size: tamaño del búfer de salida
Ajusta estos para adaptarlos a los recursos de tu host y patrones de tráfico.
Manejo de errores
AsyncBridge traduce los errores de Flight en respuestas HTTP adecuadas. También puedes agregar manejo de errores a nivel de ruta:
$app->route('/*', function() use ($app) {
try {
// lógica de la ruta
} catch (Exception $e) {
$app->response()->status(500);
$app->json(['error' => $e->getMessage()]);
}
});
AdapterMan y otros entornos
AdapterMan está soportado como un adaptador de entorno alternativo. El paquete está diseñado para ser adaptable — agregar o usar otros adaptadores generalmente sigue el mismo patrón: convertir la solicitud/respuesta del servidor en la solicitud/respuesta de Flight a través del AsyncBridge y los adaptadores específicos del entorno.
Awesome-plugins/migrations
Migraciones
Una migración para tu proyecto es el seguimiento de todos los cambios de base de datos involucrados en tu proyecto. byjg/php-migration es una biblioteca central muy útil para comenzar.
Instalación
Biblioteca PHP
Si deseas usar solo la Biblioteca PHP en tu proyecto:
composer require "byjg/migration"
Interfaz de Línea de Comando
La interfaz de línea de comando es independiente y no requiere que la instales con tu proyecto.
Puedes instalarlo globalmente y crear un enlace simbólico.
composer require "byjg/migration-cli"
Por favor visita byjg/migration-cli para obtener más información sobre Migration CLI.
Bases de datos soportadas
| Base de datos | Controlador | Cadena de conexión |
|---|---|---|
| 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 |
¿Cómo funciona?
La Migración de Base de Datos utiliza SQL PURO para gestionar la versión de la base de datos. Para que funcione, necesitas:
- Crear los Scripts SQL
- Gestionar usando la Línea de Comando o la API.
Los Scripts SQL
Los scripts se dividen en tres conjuntos de scripts:
- El script BASE contiene TODOS los comandos SQL para crear una nueva base de datos;
- Los scripts UP contienen todos los comandos de migración SQL para "subir" la versión de la base de datos;
- Los scripts DOWN contienen todos los comandos de migración SQL para "bajar" o revertir la versión de la base de datos;
El directorio de scripts es:
<root dir>
|
+-- base.sql
|
+-- /migrations
|
+-- /up
|
+-- 00001.sql
+-- 00002.sql
+-- /down
|
+-- 00000.sql
+-- 00001.sql
- "base.sql" es el script base
- La carpeta "up" contiene los scripts para migrar la versión hacia arriba. Por ejemplo: 00002.sql es el script para mover la base de datos de la versión '1' a '2'.
- La carpeta "down" contiene los scripts para migrar la versión hacia abajo. Por ejemplo: 00001.sql es el script para mover la base de datos de la versión '2' a '1'. La carpeta "down" es opcional.
Entorno de Desarrollo Múltiple
Si trabajas con múltiples desarrolladores y múltiples ramas, es difícil determinar cuál es el siguiente número.
En ese caso, tienes el sufijo "-dev" después del número de versión.
Veamos el escenario:
- El desarrollador 1 crea una rama y la versión más reciente es, por ejemplo, 42.
- El desarrollador 2 crea una rama al mismo tiempo y tiene el mismo número de versión de base de datos.
En ambos casos, los desarrolladores crearán un archivo llamado 43-dev.sql. Ambos desarrolladores migrarán HACIA ARRIBA y HACIA ABAJO sin problemas y tu versión local será 43.
Pero el desarrollador 1 fusionó tus cambios y creó una versión final 43.sql (git mv 43-dev.sql 43.sql). Si el desarrollador 2 actualiza su rama local, tendrá un archivo 43.sql (del dev 1) y su archivo 43-dev.sql. Si intenta migrar HACIA ARRIBA o HACIA ABAJO, el script de migración fallará y le alertará que hay DOS versiones 43. En ese caso, el desarrollador 2 tendrá que actualizar su archivo a 44-dev.sql y continuar trabajando hasta fusionar tus cambios y generar una versión final.
Usando la API PHP e integrándola en tus proyectos
El uso básico es
- Crear una conexión con un objeto ConnectionManagement. Para más información, consulta el componente "byjg/anydataset".
- Crear un objeto de Migración con esta conexión y la carpeta donde se encuentran los scripts SQL.
- Usar el comando apropiado para "resetear", "subir" o "bajar" los scripts de migración.
Veamos un ejemplo:
<?php
// Crear la URI de conexión
// Ver más: https://github.com/byjg/anydataset#connection-based-on-uri
$connectionUri = new \ByJG\Util\Uri('mysql://migrateuser:migratepwd@localhost/migratedatabase');
// Registrar la base de datos o bases de datos que pueden manejar esa URI:
\ByJG\DbMigration\Migration::registerDatabase(\ByJG\DbMigration\Database\MySqlDatabase::class);
// Crear la instancia de Migración
$migration = new \ByJG\DbMigration\Migration($connectionUri, '.');
// Agregar una función de progreso de devolución de llamada para recibir información de la ejecución
$migration->addCallbackProgress(function ($action, $currentVersion, $fileInfo) {
echo "$action, $currentVersion, ${fileInfo['description']}\n";
});
// Restaurar la base de datos usando el script "base.sql"
// y ejecutar TODOS los scripts existentes para subir la versión de la base de datos a la última versión
$migration->reset();
// Ejecutar TODOS los scripts existentes para subir o bajar la versión de la base de datos
// desde la versión actual hasta el número $version;
// Si el número de versión no está especificado, migrar hasta la última versión de la base de datos
$migration->update($version = null);
El objeto de Migración controla la versión de la base de datos.
Creando un control de versión en tu proyecto
<?php
// Registrar la base de datos o bases de datos que pueden manejar esa URI:
\ByJG\DbMigration\Migration::registerDatabase(\ByJG\DbMigration\Database\MySqlDatabase::class);
// Crear la instancia de Migración
$migration = new \ByJG\DbMigration\Migration($connectionUri, '.');
// Este comando creará la tabla de versiones en tu base de datos
$migration->createVersion();
Obteniendo la versión actual
<?php
$migration->getCurrentVersion();
Agregar Callback para controlar el progreso
<?php
$migration->addCallbackProgress(function ($command, $version, $fileInfo) {
echo "Ejecutando Comando: $command en la versión $version - ${fileInfo['description']}, ${fileInfo['exists']}, ${fileInfo['file']}, ${fileInfo['checksum']}\n";
});
Obteniendo la instancia del controlador de base de datos
<?php
$migration->getDbDriver();
Para usarlo, por favor visita: https://github.com/byjg/anydataset-db
Evitando Migraciones Parciales (no disponible para MySQL)
Una migración parcial es cuando el script de migración se interrumpe en medio del proceso debido a un error o una interrupción manual.
La tabla de migración tendrá el estado partial up o partial down y debe ser corregido manualmente antes de poder migrar nuevamente.
Para evitar esta situación, puedes especificar que la migración se ejecute en un contexto transaccional.
Si el script de migración falla, la transacción se revertirá y la tabla de migración se marcará como complete y
la versión será la versión anterior inmediata antes del script que causó el error.
Para habilitar esta función, debes llamar al método withTransactionEnabled pasando true como parámetro:
<?php
$migration->withTransactionEnabled(true);
NOTA: Esta característica no está disponible para MySQL, ya que no admite comandos DDL dentro de una transacción. Si utilizas este método con MySQL, la Migración lo ignorará en silencio. Más info: https://dev.mysql.com/doc/refman/8.0/en/cannot-roll-back.html
Consejos sobre cómo escribir migraciones SQL para Postgres
Al crear triggers y funciones SQL
-- HACER
CREATE FUNCTION emp_stamp() RETURNS trigger AS $emp_stamp$
BEGIN
-- Comprobar que empname y salary están dados
IF NEW.empname IS NULL THEN
RAISE EXCEPTION 'empname no puede ser nulo'; -- no importa si estos comentarios están vacíos o no
END IF; --
IF NEW.salary IS NULL THEN
RAISE EXCEPTION '% no puede tener salary nulo', NEW.empname; --
END IF; --
-- ¿Quién trabaja para nosotros cuando tienen que pagarlo?
IF NEW.salary < 0 THEN
RAISE EXCEPTION '% no puede tener un salary negativo', NEW.empname; --
END IF; --
-- Recuerda quién cambió la nómina y cuándo
NEW.last_date := current_timestamp; --
NEW.last_user := current_user; --
RETURN NEW; --
END; --
$emp_stamp$ LANGUAGE plpgsql;
-- NO HACER
CREATE FUNCTION emp_stamp() RETURNS trigger AS $emp_stamp$
BEGIN
-- Comprobar que empname y salary están dados
IF NEW.empname IS NULL THEN
RAISE EXCEPTION 'empname no puede ser nulo';
END IF;
IF NEW.salary IS NULL THEN
RAISE EXCEPTION '% no puede tener salary nulo', NEW.empname;
END IF;
-- ¿Quién trabaja para nosotros cuando tienen que pagarlo?
IF NEW.salary < 0 THEN
RAISE EXCEPTION '% no puede tener un salary negativo', NEW.empname;
END IF;
-- Recuerda quién cambió la nómina y cuándo
NEW.last_date := current_timestamp;
NEW.last_user := current_user;
RETURN NEW;
END;
$emp_stamp$ LANGUAGE plpgsql;
Dado que la capa de abstracción de base de datos PDO no puede ejecutar lotes de declaraciones SQL, al leer un archivo de migración, byjg/migration tiene que dividir todo el contenido del archivo SQL en los puntos y comas, y ejecutar las declaraciones una por una. Sin embargo, hay un tipo de declaración que puede tener múltiples puntos y comas en su interior: funciones.
Con el fin de poder analizar correctamente las funciones, byjg/migration 2.1.0 comenzó a dividir los archivos de migración en la secuencia de punto y coma + EOL en lugar de solo el punto y coma. De esta manera, si agregas un comentario vacío después de cada punto y coma interno de una definición de función, byjg/migration podrá analizarlo.
Desafortunadamente, si olvidas agregar alguno de estos comentarios, la biblioteca dividirá la declaración CREATE FUNCTION en múltiples partes y la migración fallará.
Evitar el carácter de dos puntos (:)
-- HACER
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
);
-- NO HACER
CREATE TABLE bookings (
booking_id UUID PRIMARY KEY,
booked_at TIMESTAMPTZ NOT NULL CHECK (booked_at::DATE <= check_in),
check_in DATE NOT NULL
);
Dado que PDO utiliza el carácter de dos puntos para prefijar parámetros nombrados en declaraciones preparadas, su uso puede causar problemas en otros contextos.
Por ejemplo, las declaraciones de PostgreSQL pueden usar :: para convertir valores entre tipos. Por otro lado, PDO leerá esto como un parámetro nombrado inválido en un contexto inválido y fallará cuando intente ejecutarlo.
La única forma de solucionar esta inconsistencia es evitar los dos puntos por completo (en este caso, PostgreSQL también tiene una sintaxis alternativa: CAST(value AS type)).
Usar un editor SQL
Finalmente, escribir migraciones SQL manuales puede ser agotador, pero es significativamente más fácil si usas un editor capaz de entender la sintaxis SQL, proporcionando autocompletado, introspección de tu esquema de base de datos actual y/o autoformateo de tu código.
Manejo de diferentes migraciones dentro de un esquema
Si necesitas crear diferentes scripts de migración y versiones dentro del mismo esquema, es posible, pero es demasiado arriesgado y no lo recomiendo en absoluto.
Para hacerlo, necesitas crear diferentes "tablas de migración" pasando el parámetro al constructor.
<?php
$migration = new \ByJG\DbMigration\Migration("db:/uri", "/path", true, "NEW_MIGRATION_TABLE_NAME");
Por razones de seguridad, esta función no está disponible en la línea de comandos, pero puedes usar la variable de entorno MIGRATION_VERSION para almacenar el nombre.
Recomendamos encarecidamente no utilizar esta función. La recomendación es una migración para un esquema.
Ejecución de pruebas unitarias
Las pruebas unitarias básicas se pueden ejecutar con:
vendor/bin/phpunit
Ejecución de pruebas de base de datos
Ejecutar pruebas de integración requiere que las bases de datos estén activas y en funcionamiento. Proporcionamos un docker-compose.yml básico que puedes usar para iniciar las bases de datos para pruebas.
Ejecutando las bases de datos
docker-compose up -d postgres mysql mssql
Ejecutar las pruebas
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*
Opcionalmente puedes establecer el host y la contraseña utilizados por las pruebas unitarias
export MYSQL_TEST_HOST=localhost # predeterminado a localhost
export MYSQL_PASSWORD=newpassword # usa '.' si quieres tener una contraseña nula
export PSQL_TEST_HOST=localhost # predeterminado a localhost
export PSQL_PASSWORD=newpassword # usa '.' si quieres tener una contraseña nula
export MSSQL_TEST_HOST=localhost # predeterminado a localhost
export MSSQL_PASSWORD=Pa55word
export SQLITE_TEST_HOST=/tmp/test.db # predeterminado a /tmp/test.dbAwesome-plugins/comment_template
CommentTemplate
CommentTemplate es un potente motor de plantillas PHP con compilación de activos, herencia de plantillas y procesamiento de variables. Proporciona una manera simple pero flexible de gestionar plantillas con minificación y caché integrados de CSS/JS.
Características
- Herencia de Plantillas: Usa diseños y incluye otras plantillas
- Compilación de Activos: Minificación y caché automáticos de CSS/JS
- Procesamiento de Variables: Variables de plantilla con filtros y comandos
- Codificación Base64: Activos en línea como URIs de datos
- Integración con el Framework Flight: Integración opcional con el framework PHP Flight
Instalación
Instala con composer.
composer require knifelemon/comment-template
Configuración Básica
Hay algunas opciones de configuración básicas para comenzar. Puedes leer más sobre ellas en el Repositorio de CommentTemplate.
Método 1: Usando Función de Retorno de Llamada
<?php
require_once 'vendor/autoload.php';
use KnifeLemon\CommentTemplate\Engine;
$app = Flight::app();
$app->register('view', Engine::class, [], function (Engine $engine) use ($app) {
// Directorio raíz (donde está index.php) - el directorio raíz de tu aplicación web
$engine->setPublicPath(__DIR__);
// Directorio de archivos de plantillas - soporta rutas relativas y absolutas
$engine->setSkinPath('views'); // Relativo a la ruta pública
// Dónde se almacenarán los activos compilados - soporta rutas relativas y absolutas
$engine->setAssetPath('assets'); // Relativo a la ruta pública
// Extensión de archivo de plantilla
$engine->setFileExtension('.php');
});
$app->map('render', function(string $template, array $data) use ($app): void {
echo $app->view()->render($template, $data);
});
Método 2: Usando Parámetros del Constructor
<?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 - directorio raíz (donde está index.php)
'views', // skinPath - ruta de plantillas (soporta relativa/absoluta)
'assets', // assetPath - ruta de activos compilados (soporta relativa/absoluta)
'.php' // fileExtension - extensión de archivo de plantilla
]);
$app->map('render', function(string $template, array $data) use ($app): void {
echo $app->view()->render($template, $data);
});
Configuración de Rutas
CommentTemplate proporciona un manejo inteligente de rutas tanto relativas como absolutas:
Ruta Pública
La Ruta Pública es el directorio raíz de tu aplicación web, típicamente donde reside index.php. Este es el directorio raíz desde el que los servidores web sirven archivos.
// Ejemplo: si tu index.php está en /var/www/html/myapp/index.php
$template->setPublicPath('/var/www/html/myapp'); // Directorio raíz
// Ejemplo en Windows: si tu index.php está en C:\xampp\htdocs\myapp\index.php
$template->setPublicPath('C:\\xampp\\htdocs\\myapp');
Configuración de Ruta de Plantillas
La ruta de plantillas soporta tanto rutas relativas como absolutas:
$template = new Engine();
$template->setPublicPath('/var/www/html/myapp'); // Directorio raíz (donde está index.php)
// Rutas relativas - se combinan automáticamente con la ruta pública
$template->setSkinPath('views'); // → /var/www/html/myapp/views/
$template->setSkinPath('templates/pages'); // → /var/www/html/myapp/templates/pages/
// Rutas absolutas - se usan tal cual (Unix/Linux)
$template->setSkinPath('/var/www/templates'); // → /var/www/templates/
$template->setSkinPath('/full/path/to/templates'); // → /full/path/to/templates/
// Rutas absolutas en Windows
$template->setSkinPath('C:\\www\\templates'); // → C:\www\templates\
$template->setSkinPath('D:/projects/templates'); // → D:/projects/templates/
// Rutas UNC (comparticiones de red en Windows)
$template->setSkinPath('\\\\server\\share\\templates'); // → \\server\share\templates\
Configuración de Ruta de Activos
La ruta de activos también soporta tanto rutas relativas como absolutas:
// Rutas relativas - se combinan automáticamente con la ruta pública
$template->setAssetPath('assets'); // → /var/www/html/myapp/assets/
$template->setAssetPath('static/files'); // → /var/www/html/myapp/static/files/
// Rutas absolutas - se usan tal cual (Unix/Linux)
$template->setAssetPath('/var/www/cdn'); // → /var/www/cdn/
$template->setAssetPath('/full/path/to/assets'); // → /full/path/to/assets/
// Rutas absolutas en Windows
$template->setAssetPath('C:\\www\\static'); // → C:\www\static\
$template->setAssetPath('D:/projects/assets'); // → D:/projects/assets/
// Rutas UNC (comparticiones de red en Windows)
$template->setAssetPath('\\\\server\\share\\assets'); // → \\server\share\assets\
Detección Inteligente de Rutas:
- Rutas Relativas: Sin separadores iniciales (
/,\) ni letras de unidad - Absolutas Unix: Comienzan con
/(p. ej.,/var/www/assets) - Absolutas Windows: Comienzan con letra de unidad (p. ej.,
C:\www,D:/assets) - Rutas UNC: Comienzan con
\\(p. ej.,\\server\share)
Cómo funciona:
- Todas las rutas se resuelven automáticamente según el tipo (relativa vs absoluta)
- Las rutas relativas se combinan con la ruta pública
@cssy@jscrean archivos minificados en:{resolvedAssetPath}/css/o{resolvedAssetPath}/js/@assetcopia archivos individuales a:{resolvedAssetPath}/{relativePath}@assetDircopia directorios a:{resolvedAssetPath}/{relativePath}- Caché inteligente: los archivos solo se copian cuando la fuente es más nueva que el destino
Integración con Tracy Debugger
CommentTemplate incluye integración con Tracy Debugger para registro y depuración en desarrollo.

Instalación
composer require tracy/tracy
Uso
<?php
use KnifeLemon\CommentTemplate\Engine;
use Tracy\Debugger;
// Habilitar Tracy (debe llamarse antes de cualquier salida)
Debugger::enable(Debugger::DEVELOPMENT);
Flight::set('flight.content_length', false);
// Sobrescritura de plantilla
$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();
Características del Panel de Depuración
CommentTemplate agrega un panel personalizado a la barra de depuración de Tracy con cuatro pestañas:
- Overview: Configuración, métricas de rendimiento y contadores
- Assets: Detalles de compilación CSS/JS con ratios de compresión
- Variables: Valores originales y transformados con filtros aplicados
- Timeline: Vista cronológica de todas las operaciones de plantilla
Qué se Registra
- Renderizado de plantillas (inicio/fin, duración, diseños, importaciones)
- Compilación de activos (archivos CSS/JS, tamaños, ratios de compresión)
- Procesamiento de variables (valores originales/transformados, filtros)
- Operaciones de activos (codificación base64, copia de archivos)
- Métricas de rendimiento (duración, uso de memoria)
Nota: Cero impacto en el rendimiento cuando Tracy no está instalado o deshabilitado.
Consulte el ejemplo completo funcional con Flight PHP.
Directivas de Plantilla
Herencia de Diseños
Usa diseños para crear una estructura común:
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>
Gestión de Activos
Archivos CSS
<!--@css(/css/styles.css)--> <!-- Minificado y en caché -->
<!--@cssSingle(/css/critical.css)--> <!-- Archivo único, no minificado -->
Archivos JavaScript
CommentTemplate soporta diferentes estrategias de carga de JavaScript:
<!--@js(/js/script.js)--> <!-- Minificado, cargado al final -->
<!--@jsAsync(/js/analytics.js)--> <!-- Minificado, cargado al final con async -->
<!--@jsDefer(/js/utils.js)--> <!-- Minificado, cargado al final con defer -->
<!--@jsTop(/js/critical.js)--> <!-- Minificado, cargado en head -->
<!--@jsTopAsync(/js/tracking.js)--> <!-- Minificado, cargado en head con async -->
<!--@jsTopDefer(/js/polyfill.js)--> <!-- Minificado, cargado en head con defer -->
<!--@jsSingle(/js/widget.js)--> <!-- Archivo único, no minificado -->
<!--@jsSingleAsync(/js/ads.js)--> <!-- Archivo único, no minificado, async -->
<!--@jsSingleDefer(/js/social.js)--> <!-- Archivo único, no minificado, defer -->
Directivas de Activos en Archivos CSS/JS
CommentTemplate también procesa directivas de activos dentro de archivos CSS y JavaScript durante la compilación:
Ejemplo CSS:
/* En tus archivos 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)-->');
}
Ejemplo JavaScript:
/* En tus archivos JS */
const fontUrl = '<!--@asset(fonts/custom.woff2)-->';
const imageData = '<!--@base64(images/icon.png)-->';
Codificación Base64
<!--@base64(images/logo.png)--> <!-- En línea como URI de datos -->
Ejemplo:
<!-- Incorpora imágenes pequeñas como URIs de datos para una carga más rápida -->
<img src="<!--@base64(images/logo.png)-->" alt="Logo">
<div style="background-image: url('<!--@base64(icons/star.svg)-->');">
Icono pequeño como fondo
</div>
Copia de Activos
<!--@asset(images/photo.jpg)--> <!-- Copia un activo único al directorio público -->
<!--@assetDir(assets)--> <!-- Copia todo el directorio al directorio público -->
Ejemplo:
<!-- Copia y referencia activos estáticos -->
<img src="<!--@asset(images/hero-banner.jpg)-->" alt="Hero Banner">
<a href="<!--@asset(documents/brochure.pdf)-->" download>Descargar Folleto</a>
<!-- Copia todo el directorio (fuentes, iconos, etc.) -->
<!--@assetDir(assets/fonts)-->
<!--@assetDir(assets/icons)-->
Inclusiones de Plantilla
<!--@import(components/header)--> <!-- Incluye otras plantillas -->
Ejemplo:
<!-- Incluye componentes reutilizables -->
<!--@import(components/header)-->
<main>
<h1>Bienvenido a nuestro sitio web</h1>
<!--@import(components/sidebar)-->
<div class="content">
<p>Contenido principal aquí...</p>
</div>
</main>
<!--@import(components/footer)-->
Procesamiento de Variables
Variables Básicas
<h1>{$title}</h1>
<p>{$description}</p>
Filtros de Variables
{$title|upper} <!-- Convertir a mayúsculas -->
{$content|lower} <!-- Convertir a minúsculas -->
{$html|striptag} <!-- Eliminar etiquetas HTML -->
{$text|escape} <!-- Escapar HTML -->
{$multiline|nl2br} <!-- Convertir saltos de línea a <br> -->
{$html|br2nl} <!-- Convertir etiquetas <br> a saltos de línea -->
{$description|trim} <!-- Recortar espacios en blanco -->
{$subject|title} <!-- Convertir a título -->
Comandos de Variables
{$title|default=Default Title} <!-- Establecer valor predeterminado -->
{$name|concat= (Admin)} <!-- Concatenar texto -->
Comandos de Variables
{$content|striptag|trim|escape} <!-- Encadenar múltiples filtros -->
Comentarios
Los comentarios de plantilla se eliminan completamente de la salida y no aparecerán en el HTML final:
{* Este es un comentario de plantilla de una línea *}
{*
Este es un comentario de plantilla
de varias líneas
que abarca varias líneas
*}
<h1>{$title}</h1>
{* Comentario de depuración: verificando si la variable title funciona *}
<p>{$content}</p>
Nota: Los comentarios de plantilla {* ... *} son diferentes de los comentarios HTML <!-- ... -->. Los comentarios de plantilla se eliminan durante el procesamiento y nunca llegan al navegador.
Estructura de Proyecto de Ejemplo
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/ # Activos generados
│ ├── css/
│ └── js/
└── vendor/Awesome-plugins/easy_query
EasyQuery
knifelemon/easy-query es un constructor de consultas SQL ligero y fluido que genera SQL y parámetros para declaraciones preparadas. Funciona con SimplePdo.
Características
- 🔗 API Fluida - Enlaza métodos para una construcción de consultas legible
- 🛡️ Protección contra Inyección SQL - Enlace automático de parámetros con declaraciones preparadas
- 🔧 Soporte para SQL Crudo - Inserta expresiones SQL crudas con
raw() - 📝 Múltiples Tipos de Consultas - SELECT, INSERT, UPDATE, DELETE, COUNT
- 🔀 Soporte para JOIN - JOIN INNER, LEFT, RIGHT con alias
- 🎯 Condiciones Avanzadas - LIKE, IN, NOT IN, BETWEEN, operadores de comparación
- 🌐 Independiente de la Base de Datos - Devuelve SQL + params, úsalo con cualquier conexión de BD
- 🪶 Ligero - Huella mínima con cero dependencias
Instalación
composer require knifelemon/easy-query
Inicio Rápido
use KnifeLemon\EasyQuery\Builder;
$q = Builder::table('users')
->select(['id', 'name', 'email'])
->where(['status' => 'active'])
->orderBy('created_at DESC')
->limit(10)
->build();
// Usa con SimplePdo de Flight
$users = Flight::db()->fetchAll($q['sql'], $q['params']);
Entendiendo build()
El método build() devuelve un array con sql y params. Esta separación mantiene tu base de datos segura usando declaraciones preparadas.
$q = Builder::table('users')
->where(['email' => 'user@example.com'])
->build();
// Devuelve:
// [
// 'sql' => 'SELECT * FROM users WHERE email = ?',
// 'params' => ['user@example.com']
// ]
Tipos de Consultas
SELECT
// Selecciona todas las columnas
$q = Builder::table('users')->build();
// SELECT * FROM users
// Selecciona columnas específicas
$q = Builder::table('users')
->select(['id', 'name', 'email'])
->build();
// SELECT id, name, email FROM users
// Con alias de tabla
$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']);
Condiciones WHERE
Igualdad Simple
$q = Builder::table('users')
->where(['id' => 123, 'status' => 'active'])
->build();
// WHERE id = ? AND status = ?
Operadores de Comparación
$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 ?
Condiciones OR
Usa orWhere() para agregar condiciones OR agrupadas:
$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
Múltiples JOINs
$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();
Ordenamiento, Agrupación y Límites
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 y OFFSET
$q = Builder::table('users')
->limit(10)
->build();
// LIMIT 10
$q = Builder::table('users')
->limit(10, 20) // limit, offset
->build();
// LIMIT 10 OFFSET 20
Expresiones SQL Crudas
Usa raw() cuando necesites funciones SQL o expresiones que no deben tratarse como parámetros enlazados.
Raw Básico
$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 con Parámetros Enlazados
$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 en WHERE (Subconsulta)
$q = Builder::table('products')
->where([
'price' => ['>', Builder::raw('(SELECT AVG(price) FROM products)')]
])
->build();
// WHERE price > (SELECT AVG(price) FROM products)
Identificadores Seguros para Entrada de Usuario
Cuando los nombres de columnas provienen de entrada de usuario, usa safeIdentifier() para prevenir inyección SQL:
$sortColumn = $_GET['sort']; // e.g., 'created_at'
$safeColumn = Builder::safeIdentifier($sortColumn);
$q = Builder::table('users')
->orderBy($safeColumn . ' DESC')
->build();
// Si el usuario intenta: "name; DROP TABLE users--"
// Lanza InvalidArgumentException
rawSafe para Nombres de Columnas Proporcionados por el Usuario
$userColumn = $_GET['aggregate_column'];
$q = Builder::table('orders')
->select([
Builder::rawSafe('SUM({col})', ['col' => $userColumn])->value . ' AS total'
])
->build();
// Valida el nombre de la columna, lanza excepción si es inválido
Advertencia: Nunca concatene entrada de usuario directamente en
raw(). Siempre use parámetros enlazados osafeIdentifier().
Reutilización del Constructor de Consultas
Métodos de Limpieza
Limpia partes específicas para reutilizar el constructor:
$query = Builder::table('users')
->select(['id', 'name'])
->where(['status' => 'active'])
->orderBy('created_at DESC');
// Primera consulta
$q1 = $query->limit(10)->build();
// Limpia y reutiliza
$query->clearWhere()->clearLimit();
// Segunda consulta con condiciones diferentes
$q2 = $query
->where(['status' => 'pending'])
->limit(5)
->build();
Métodos de Limpieza Disponibles
| Método | Descripción |
|---|---|
clearWhere() |
Limpia condiciones WHERE y parámetros |
clearSelect() |
Reinicia columnas SELECT al valor predeterminado '*' |
clearJoin() |
Limpia todas las cláusulas JOIN |
clearGroupBy() |
Limpia cláusula GROUP BY |
clearOrderBy() |
Limpia cláusula ORDER BY |
clearLimit() |
Limpia LIMIT y OFFSET |
clearAll() |
Reinicia el constructor al estado inicial |
Ejemplo de Paginación
$baseQuery = Builder::table('users')
->select(['id', 'name', 'email'])
->where(['status' => 'active'])
->orderBy('created_at DESC');
// Obtiene el conteo total
$countQuery = clone $baseQuery;
$countResult = $countQuery->clearSelect()->count()->build();
$total = Flight::db()->fetchField($countResult['sql'], $countResult['params']);
// Obtiene resultados paginados
$page = 1;
$perPage = 20;
$listResult = $baseQuery->limit($perPage, ($page - 1) * $perPage)->build();
$users = Flight::db()->fetchAll($listResult['sql'], $listResult['params']);
Construcción Dinámica de Consultas
$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']);
Ejemplo Completo de FlightPHP
use KnifeLemon\EasyQuery\Builder;
// Lista usuarios con paginación
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]);
});
// Crea usuario
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()]);
});
// Actualiza usuario
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]);
});
// Elimina usuario
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]);
});
Referencia de API
Métodos Estáticos
| Método | Descripción |
|---|---|
Builder::table(string $table) |
Crea una nueva instancia del constructor para la tabla |
Builder::raw(string $sql, array $bindings = []) |
Crea una expresión SQL cruda |
Builder::rawSafe(string $expr, array $identifiers, array $bindings = []) |
Expresión cruda con sustitución segura de identificadores |
Builder::safeIdentifier(string $identifier) |
Valida y devuelve un nombre de columna/tabla seguro |
Métodos de Instancia
| Método | Descripción |
|---|---|
alias(string $alias) |
Establece alias de tabla |
select(string\|array $columns) |
Establece columnas a seleccionar (predeterminado: '*') |
where(array $conditions) |
Agrega condiciones WHERE (AND) |
orWhere(array $conditions) |
Agrega condiciones OR WHERE |
join(string $table, string $condition, string $alias, string $type) |
Agrega cláusula JOIN |
innerJoin(string $table, string $condition, string $alias) |
Agrega INNER JOIN |
leftJoin(string $table, string $condition, string $alias) |
Agrega LEFT JOIN |
groupBy(string $groupBy) |
Agrega cláusula GROUP BY |
orderBy(string $orderBy) |
Agrega cláusula ORDER BY |
limit(int $limit, int $offset = 0) |
Agrega LIMIT y OFFSET |
count(string $column = '*') |
Establece consulta a COUNT |
insert(array $data) |
Establece consulta a INSERT |
update(array $data) |
Establece consulta a UPDATE |
delete() |
Establece consulta a DELETE |
build() |
Construye y devuelve ['sql' => ..., 'params' => ...] |
get() |
Alias para build() |
Integración con Tracy Debugger
EasyQuery se integra automáticamente con Tracy Debugger si está instalado. ¡No se requiere configuración!
composer require tracy/tracy
use Tracy\Debugger;
Debugger::enable();
// Todas las consultas se registran automáticamente en el panel de Tracy
$q = Builder::table('users')->where(['status' => 'active'])->build();
El panel de Tracy muestra:
- Total de consultas y desglose por tipo
- SQL generado (con resaltado de sintaxis)
- Array de parámetros
- Detalles de la consulta (tabla, where, joins, etc.)
Para documentación completa, visita el repositorio de GitHub.
Awesome-plugins/twig
Twig
Twig es un motor de plantillas flexible, rápido y seguro para PHP. Es el lenguaje de plantillas utilizado por Symfony y muchos otros proyectos, lo que significa que las herramientas de codificación de IA y la mayoría de los desarrolladores de PHP ya conocen bien su sintaxis. Twig compila las plantillas a PHP optimizado, escapa automáticamente la salida por defecto (excelente para la protección contra XSS), y es fácil de extender con filtros, funciones y extensiones.
Instalación
Instalar con composer.
composer require twig/twig
Configuración Básica
Hay algunas opciones de configuración básicas para comenzar. Puedes leer más sobre ellas en la Documentación de 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, [
// Dónde Twig almacena sus plantillas compiladas
'cache' => __DIR__ . '/../cache/twig',
// Recompilar plantillas cuando la fuente cambia (útil en desarrollo)
'auto_reload' => true,
]);
echo $twig->render($template, $data);
});
Registrar Twig como la Clase de Vista
Si prefieres reutilizar un solo entorno de Twig (recomendado para producción), regístralo y apunta render a él:
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);
});
Ejemplo Simple de Diseño
Aquí tienes un ejemplo simple de un archivo de diseño. Este es el archivo que se utilizará para envolver todas tus otras vistas.
{# 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>
{# tus elementos de navegación aquí #}
</nav>
</header>
<div id="content">
{# Este es el truco aquí #}
{% block content %}{% endblock %}
</div>
<div id="footer">
© Copyright
</div>
</body>
</html>
Y ahora tenemos tu archivo que se va a renderizar dentro de ese bloque de contenido:
{# app/views/home.twig #}
{# Esto le dice a Twig que este archivo está "dentro" del archivo layout.twig #}
{% extends 'layout.twig' %}
{# Este es el contenido que se renderizará dentro del diseño dentro del bloque de contenido #}
{% block content %}
<h1>Home Page</h1>
<p>Welcome to my app!</p>
{% endblock %}
Luego cuando vayas a renderizar esto dentro de tu función o controlador, harías algo como esto:
// ruta simple
Flight::route('/', function () {
Flight::render('home.twig', [
'title' => 'Home Page'
]);
});
// o si estás usando un controlador
Flight::route('/', [HomeController::class, 'index']);
// HomeController.php
class HomeController
{
public function index()
{
Flight::render('home.twig', [
'title' => 'Home Page'
]);
}
}
¡Consulta la Documentación de Twig para más información sobre cómo usar Twig a su máximo potencial!
Depuración
Twig viene con una Extensión de Depuración que añade una función dump() que puedes usar dentro de las plantillas. Habilítala solo en desarrollo:
$app->register('view', \Twig\Environment::class, [
new \Twig\Loader\FilesystemLoader($app->get('flight.views.path')),
[
'cache' => __DIR__ . '/../cache/twig',
'debug' => true, // requerido para la función dump()
'auto_reload' => true,
],
], function (\Twig\Environment $twig): void {
$twig->addExtension(new \Twig\Extension\DebugExtension());
});
Luego en una plantilla:
{{ dump(user) }}
También puedes combinar Twig con Tracy para depuración a nivel de PHP. Para métricas a nivel de plantilla (tiempo de renderizado, memoria, qué plantillas/bloques se ejecutaron), usa el panel Twig opcional en flightphp/tracy-extensions: pasa un Twig\Profiler\Profile como twig_profile a TracyExtensionLoader. La TwigTracyExtension opcional expone {{ dump() }} / {{ bdump() }} / {{ dumpe() }} en las plantillas cuando Tracy está activado.
Nota de Seguridad
Twig escapa automáticamente la salida por defecto, lo que ayuda a proteger contra ataques XSS. Prefiere {{ variable }} para texto. Usa el filtro |raw solo cuando confíes intencionalmente en el contenido HTML (por ejemplo, markdown sanitizado que ya procesaste en el servidor).
Awesome-plugins/session
FlightPHP Sesión - Manejador de Sesiones Ligero Basado en Archivos
Esto es un plugin ligero, basado en archivos, para manejar sesiones en el Flight PHP Framework. Proporciona una solución simple pero potente para gestionar sesiones, con características como lecturas de sesiones no bloqueantes, cifrado opcional, funcionalidad de auto-commit y un modo de prueba para desarrollo. Los datos de sesión se almacenan en archivos, lo que lo hace ideal para aplicaciones que no requieren una base de datos.
Si deseas usar una base de datos, consulta el plugin ghostff/session que incluye muchas de estas mismas características pero con un backend de base de datos.
Visita el repositorio de Github para el código fuente completo y detalles.
Instalación
Instala el plugin a través de Composer:
composer require flightphp/session
Uso Básico
Aquí hay un ejemplo simple de cómo usar el plugin flightphp/session en tu aplicación Flight:
require 'vendor/autoload.php';
use flight\Session;
$app = Flight::app();
// Registra el servicio de sesión
$app->register('session', Session::class);
// Ejemplo de ruta con uso de sesión
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'); // Esto imprime: johndoe
echo $session->get('preferences', 'default_theme'); // Esto imprime: default_theme
if ($session->get('user_id')) {
Flight::json(['message' => '¡El usuario está logueado!', 'user_id' => $session->get('user_id')]);
}
});
Flight::route('/logout', function() {
$session = Flight::session();
$session->clear(); // Limpia todos los datos de sesión
Flight::json(['message' => 'Cerrado de sesión exitoso']);
});
Flight::start();
Puntos Clave
- No Bloqueante: Usa
read_and_closepara iniciar la sesión por defecto, previniendo problemas de bloqueo de sesiones. - Auto-Commit: Habilitado por defecto, por lo que los cambios se guardan automáticamente al cerrar a menos que se desactive.
- Almacenamiento en Archivos: Las sesiones se almacenan en el directorio temporal del sistema bajo
/flight_sessionspor defecto.
Configuración
Puedes personalizar el manejador de sesiones pasando un arreglo de opciones al registrar:
// Sí, es un arreglo doble :)
$app->register('session', Session::class, [ [
'save_path' => '/custom/path/to/sessions', // Directorio para los archivos de sesión
'prefix' => 'myapp_', // Prefijo para los archivos de sesión
'encryption_key' => 'a-secure-32-byte-key-here', // Habilita el cifrado (se recomienda 32 bytes para AES-256-CBC)
'auto_commit' => false, // Desactiva el auto-commit para control manual
'start_session' => true, // Inicia la sesión automáticamente (por defecto: true)
'test_mode' => false, // Habilita el modo de prueba para desarrollo
'serialization' => 'json', // Método de serialización: 'json' (por defecto) o 'php' (heredado)
] ]);
Opciones de Configuración
| Opción | Descripción | Valor por Defecto |
|---|---|---|
save_path |
Directorio donde se almacenan los archivos de sesión | sys_get_temp_dir() . '/flight_sessions' |
prefix |
Prefijo para el archivo de sesión guardado | sess_ |
encryption_key |
Clave para el cifrado AES-256-CBC (opcional) | null (sin cifrado) |
auto_commit |
Guarda automáticamente los datos de sesión al cerrar | true |
start_session |
Inicia la sesión automáticamente | true |
test_mode |
Ejecuta en modo de prueba sin afectar las sesiones de PHP | false |
test_session_id |
ID de sesión personalizado para el modo de prueba (opcional) | Generado aleatoriamente si no se establece |
serialization |
Método de serialización: 'json' (por defecto, seguro) o 'php' (heredado, permite objetos) | 'json' |
Modos de Serialización
Por defecto, esta biblioteca usa serialización JSON para los datos de sesión, lo que es seguro y evita vulnerabilidades de inyección de objetos PHP. Si necesitas almacenar objetos PHP en la sesión (no recomendado para la mayoría de las aplicaciones), puedes optar por la serialización PHP heredada:
'serialization' => 'json'(por defecto):- Solo se permiten arreglos y primitivos en los datos de sesión.
- Más seguro: inmune a la inyección de objetos PHP.
- Los archivos se prefijan con
J(JSON plano) oF(JSON cifrado).
'serialization' => 'php':- Permite almacenar objetos PHP (usa con precaución).
- Los archivos se prefijan con
P(serialización PHP plana) oE(serialización PHP cifrada).
Nota: Si usas serialización JSON, intentar almacenar un objeto lanzará una excepción.
Uso Avanzado
Commit Manual
Si desactivas el auto-commit, debes confirmar los cambios manualmente:
$app->register('session', Session::class, ['auto_commit' => false]);
Flight::route('/update', function() {
$session = Flight::session();
$session->set('key', 'value');
$session->commit(); // Guarda explícitamente los cambios
});
Seguridad de Sesión con Cifrado
Habilita el cifrado para datos sensibles:
$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'); // Se cifra automáticamente
echo $session->get('credit_card'); // Se descifra al recuperar
});
Regeneración de Sesión
Regenera el ID de sesión por seguridad (por ejemplo, después de iniciar sesión):
Flight::route('/post-login', function() {
$session = Flight::session();
$session->regenerate(); // Nuevo ID, mantiene los datos
// O
$session->regenerate(true); // Nuevo ID, elimina los datos antiguos
});
Ejemplo de Middleware
Protege rutas con autenticación basada en sesiones:
Flight::route('/admin', function() {
Flight::json(['message' => 'Bienvenido al panel de administración']);
})->addMiddleware(function() {
$session = Flight::session();
if (!$session->get('is_admin')) {
Flight::halt(403, 'Acceso denegado');
}
// Esto es solo un ejemplo simple de cómo usarlo en middleware. Para un ejemplo más detallado, consulta la documentación de [middleware](/learn/middleware).
});
Métodos
La clase Session proporciona estos métodos:
set(string $key, $value): Almacena un valor en la sesión.get(string $key, $default = null): Recupera un valor, con un valor predeterminado opcional si la clave no existe.delete(string $key): Elimina una clave específica de la sesión.clear(): Elimina todos los datos de sesión, pero mantiene el mismo nombre de archivo para la sesión.commit(): Guarda los datos de sesión actuales en el sistema de archivos.id(): Devuelve el ID de sesión actual.regenerate(bool $deleteOldFile = false): Regenera el ID de sesión, incluyendo la creación de un nuevo archivo de sesión, manteniendo todos los datos antiguos y el archivo antiguo permanece en el sistema. Si$deleteOldFileestrue, se elimina el archivo de sesión antiguo.destroy(string $id): Destruye una sesión por ID y elimina el archivo de sesión del sistema. Esto forma parte de laSessionHandlerInterfacey$ides requerido. El uso típico sería$session->destroy($session->id()).getAll(): Devuelve todos los datos de la sesión actual.
Todos los métodos excepto get() y id() devuelven la instancia de Session para encadenamiento.
¿Por Qué Usar Este Plugin?
- Ligero: Sin dependencias externas, solo archivos.
- No Bloqueante: Evita el bloqueo de sesiones con
read_and_closepor defecto. - Seguro: Soporta cifrado AES-256-CBC para datos sensibles.
- Flexible: Opciones de auto-commit, modo de prueba y control manual.
- Nativo de Flight: Construido específicamente para el framework Flight.
Detalles Técnicos
- Formato de Almacenamiento: Los archivos de sesión se prefijan con
sess_y se almacenan en elsave_pathconfigurado. Prefijos de contenido de archivo:J: JSON plano (por defecto, sin cifrado)F: JSON cifrado (por defecto con cifrado)P: Serialización PHP plana (heredada, sin cifrado)E: Serialización PHP cifrada (heredada con cifrado)
- Cifrado: Usa AES-256-CBC con un IV aleatorio por escritura de sesión cuando se proporciona una
encryption_key. El cifrado funciona para ambos modos de serialización JSON y PHP. - Serialización: JSON es el método por defecto y más seguro. La serialización PHP está disponible para usos heredados/avanzados, pero es menos segura.
- Recolección de Basura: Implementa
SessionHandlerInterface::gc()de PHP para limpiar sesiones expiradas.
Contribuciones
¡Las contribuciones son bienvenidas! Bifurca el repositorio, realiza tus cambios y envía una solicitud de extracción. Reporta errores o sugiere características a través del rastreador de problemas de Github.
Licencia
Este plugin está licenciado bajo la Licencia MIT. Consulta el repositorio de Github para detalles.
Awesome-plugins/runway
Runway
Runway es una aplicación CLI que te ayuda a gestionar tus aplicaciones Flight. Puede generar controladores, mostrar todas las rutas, ejecutar ayudantes de configuración de IA, migraciones (en el esqueleto) y más. Está basado en la excelente biblioteca adhocore/php-cli.
Haz clic aquí para ver el código.
Los comandos de scaffolding están intencionalmente alineados con el esqueleto oficial para que las herramientas de codificación de IA y los humanos obtengan las mismas rutas, espacios de nombres y estilo de inyección de constructor cada vez.
Instalación
Instalar con composer.
composer require flightphp/runway
El esqueleto ya depende de Runway; usa php runway desde la raíz del proyecto.
Configuración Básica
La primera vez que ejecutes Runway, intentará encontrar una configuración runway en app/config/config.php mediante la clave 'runway'.
<?php
// app/config/config.php
return [
'runway' => [
'app_root' => 'app/',
'public_root' => 'public/',
// opcional; el esqueleto también usa index_root para la entrada pública
'index_root' => 'public/index.php',
],
];
NOTA - A partir de v1.2.0,
.runway-config.jsonestá obsoleto en favor deapp/config/config.php. Migra conphp runway config:migrateal actualizar proyectos antiguos. El esqueleto puede seguir escribiendo un pequeño.runway-config.jsonal crear el proyecto para compatibilidad; prefiere la claverunwayenconfig.phpde ahora en adelante.
Detección de Raíz del Proyecto
Runway es lo suficientemente inteligente para detectar la raíz de tu proyecto, incluso si lo ejecutas desde un subdirectorio. Busca indicadores como composer.json, .git, o app/config/config.php para determinar dónde está la raíz del proyecto. ¡Esto significa que puedes ejecutar comandos de Runway desde cualquier lugar de tu proyecto!
Uso
Runway tiene una serie de comandos que puedes usar para gestionar tu aplicación Flight. Hay dos formas fáciles de usar Runway.
- Si estás usando el proyecto esqueleto, puedes ejecutar
php runway [comando]desde la raíz de tu proyecto. - Si estás usando Runway como un paquete instalado vía composer, puedes ejecutar
vendor/bin/runway [comando]desde la raíz de tu proyecto.
Lista de Comandos
Puedes ver una lista de todos los comandos disponibles ejecutando el comando php runway.
php runway
Solo confía en los comandos que realmente aparezcan en esa lista para tu instalación (comandos principales de Runway vs comandos específicos del proyecto como migrate del esqueleto).
Ayuda de Comandos
Para cualquier comando, puedes pasar el flag --help para obtener más información sobre cómo usar el comando.
php runway routes --help
php runway make:controller --help
Aquí hay algunos ejemplos:
Generar un Controlador
make:controller genera un controlador que coincide con el diseño del esqueleto oficial:
| Ruta | app/Controller/{Nombre}.php |
| Espacio de nombres | App\Controller |
| Estilo | Inyección de constructor de flight\Engine (sin Flight:: en el cuerpo de la clase) |
php runway make:controller MiControlador
# → app/Controller/MiControlador.php
# namespace App\Controller;
Ejemplo de la estructura que debes esperar (simplificada):
<?php
declare(strict_types=1);
namespace App\Controller;
use flight\Engine;
class MiControlador
{
protected Engine $app;
public function __construct(Engine $app)
{
$this->app = $app;
}
public function index(): void
{
// ej. $this->app->render('…', […]);
}
}
Regístralo con un callable de clase para que Dice pueda construir el controlador:
// app/config/routes.php
use App\Controller\MiControlador;
$router->get('/mio', [MiControlador::class, 'index']);
¿Por qué este diseño? La mayúscula de la carpeta debe coincidir con el espacio de nombres (Controller no controllers) para Composer PSR-4 en Linux—ver Autocarga. La misma ruta es la que los archivos AGENTS.md raíz y con alcance indican a las herramientas de IA que usen, así que los controladores generados y escritos a mano permanecen idénticos.
Documentación antigua y proyectos comunitarios a veces usaban
app/controllers/yapp\controllers. Eso sigue siendo válido si tu árbol aún usa carpetas en minúsculas. Los nuevos proyectos esqueleto y la salida actual demake:controllerusanapp/Controller/+App\Controller.
Generar un Modelo Active Record
Primero asegúrate de haber instalado el plugin Active Record.
php runway make:record usuarios
En el esqueleto oficial, los modelos viven bajo app/Model/ con espacio de nombres App\Model, y la conexión a la base de datos es SimplePdo (inyéctalo o pásalo al constructor de ActiveRecord). Los nombres de archivo/espacios de nombres generados siguen los valores predeterminados actuales de Runway y tu configuración runway—prefiere alinear los nuevos modelos con App\Model para que coincidan con autocarga y AGENTS.md.
Ejemplo de un modelo consistente con la demostración de posts del esqueleto:
<?php
declare(strict_types=1);
namespace App\Model;
use flight\ActiveRecord;
/**
* @property int $id
* @property string $título
* // …
*/
class Post extends ActiveRecord
{
protected array $relations = [];
public function __construct($databaseConnection)
{
parent::__construct($databaseConnection, 'posts');
}
}
Si un generador más antiguo aún emite app/records / app\records, puedes mantener esa convención en aplicaciones heredadas o mover archivos a app/Model/ y actualizar el espacio de nombres para que coincida con la mayúscula de la carpeta.
Migraciones (esqueleto)
El esqueleto oficial incluye un comando de proyecto (descubierto desde app/commands/) como:
php runway migrate
Las migraciones son archivos SQL bajo migrations/ (por ejemplo YYYYMMDDHHMMSS_descripcion.sql para SQLite y …_descripcion.mysql.sql para MySQL), seleccionados desde la configuración del controlador de base de datos / env. Los flags y comportamiento exactos están definidos por ese comando de proyecto—ejecuta php runway migrate --help en tu app.
Ayudantes de IA
Runway expone comandos orientados a IA usados con IA y experiencia de desarrollador:
php runway ai:init
php runway ai:generate-instructions
Estos almacenan credenciales LLM y generan instrucciones de proyecto (principalmente AGENTS.md). En el esqueleto, trata AGENTS.md (y copias con alcance bajo app/) más SECURITY.md como la fuente de verdad para agentes.
Mostrar Todas las Rutas
Esto mostrará todas las rutas que están actualmente registradas con Flight.
php runway routes
Si deseas ver solo rutas específicas, puedes pasar un flag para filtrar las rutas.
# Mostrar solo rutas GET
php runway routes --get
# Mostrar solo rutas POST
php runway routes --post
# etc.
Agregar Comandos Personalizados a Runway
Si estás creando un paquete para Flight, o quieres agregar tus propios comandos personalizados a tu proyecto, puedes hacerlo creando un directorio src/commands/, flight/commands/, app/commands/, o commands/ para tu proyecto/paquete. Si necesitas más personalización, consulta la sección de Configuración a continuación.
En el esqueleto, los comandos de proyecto viven en app/commands/ con espacio de nombres App\Command. Runway los descubre por ruta; mantén esa carpeta sincronizada con el classmap/PSR-4 de Composer como ya hace tu proyecto.
Para crear un comando, simplemente extiende la clase AbstractBaseCommand, e implementa como mínimo un método __construct y un método execute.
<?php
declare(strict_types=1);
namespace App\Command;
use flight\commands\AbstractBaseCommand;
class ComandoEjemplo extends AbstractBaseCommand
{
/**
* Construct
*
* @param array<string,mixed> $config Configuración desde app/config/config.php
*/
public function __construct(array $config)
{
parent::__construct('make:example', 'Crear un ejemplo para la documentación', $config);
$this->argument('<gif-divertido>', 'El nombre del gif divertido');
}
/**
* Ejecuta la función
*
* @return void
*/
public function execute()
{
$io = $this->app()->io();
$io->info('Creando ejemplo...');
// Hacer algo aquí
$io->ok('¡Ejemplo creado!');
}
}
¡Consulta la Documentación de adhocore/php-cli para más información sobre cómo construir tus propios comandos personalizados en tu aplicación Flight!
Gestión de Configuración
Dado que la configuración se ha movido a app/config/config.php a partir de v1.2.0, hay algunos comandos de ayuda para gestionar la configuración.
Consejo del esqueleto: Mantén
config.phpcomo valores PHP literales. Los secretos pertenecen a.env. Evita expresiones$_ENV[...]dentro deconfig.php—config:setreescribe ese archivo como datos estáticos y podría incrustar secretos en el archivo. Ver Configuración.
Migrar Configuración Antigua
Si tienes un archivo .runway-config.json antiguo, puedes migrarlo fácilmente a app/config/config.php con el siguiente comando:
php runway config:migrate
Establecer Valor de Configuración
Puedes establecer un valor de configuración usando el comando config:set. Esto es útil si quieres actualizar un valor de configuración sin abrir el archivo.
php runway config:set app_root "app/"
Obtener Valor de Configuración
Puedes obtener un valor de configuración usando el comando config:get.
php runway config:get app_root
Todas las Configuraciones de Runway
Si necesitas personalizar la configuración para Runway, puedes establecer estos valores en app/config/config.php. A continuación hay algunas configuraciones adicionales que puedes establecer:
<?php
// app/config/config.php
return [
// ... otros valores de configuración ...
'runway' => [
// Aquí es donde está ubicado el directorio de tu aplicación
'app_root' => 'app/',
// Este es el directorio donde está ubicado tu archivo index raíz
'index_root' => 'public/',
// Estas son las rutas a las raíces de otros proyectos
'root_paths' => [
'/home/user/different-project',
'/var/www/another-project'
],
// Las rutas base probablemente no necesiten configurarse, pero está aquí si lo quieres
'base_paths' => [
'/includes/libs/vendor', // si tienes una ruta realmente única para tu directorio vendor o algo similar
],
// Las rutas finales son ubicaciones dentro de un proyecto para buscar los archivos de comando
'final_paths' => [
'src/diff-path/commands',
'app/module/admin/commands',
],
// Si quieres agregar la ruta completa, adelante (absoluta o relativa a la raíz del proyecto)
'paths' => [
'/home/user/different-project/src/diff-path/commands',
'/var/www/another-project/app/module/admin/commands',
'app/my-unique-commands'
]
]
];
Accediendo a la Configuración
Si necesitas acceder a los valores de configuración efectivamente, puedes acceder a ellos a través del método __construct o el método app(). También es importante notar que si tienes un archivo app/config/services.php, esos servicios también estarán disponibles para tu comando.
public function execute()
{
$io = $this->app()->io();
// Acceder a la configuración
$app_root = $this->config['runway']['app_root'];
// Acceder a servicios como posiblemente una conexión a base de datos
$database = $this->config['database']
// ...
}
Envoltorios de Ayudantes de IA
Runway tiene algunos envoltorios de ayudantes que facilitan que la IA genere comandos. Puedes usar addOption y addArgument de una manera que se sienta similar a Symfony Console. Esto es útil si estás usando herramientas de IA para generar tus comandos.
public function __construct(array $config)
{
parent::__construct('make:example', 'Crear un ejemplo para la documentación', $config);
// El argumento mode es nullable y por defecto es completamente opcional
$this->addOption('name', 'El nombre del ejemplo', null);
}
Ver También
- Instalación - Árbol del esqueleto y valores predeterminados de create-project
- Autocarga -
App\y mayúscula de carpeta - Inyección de Dependencias - Inyección Dice + Engine para controladores generados
- IA y Experiencia de Desarrollador -
ai:init,ai:generate-instructions,AGENTS.md - Active Record - Modelos usados con
make:record/App\Modeldel esqueleto - SimplePdo - Conexión a BD usada por migraciones y modelos del esqueleto
Awesome-plugins/tracy_extensions
Extensiones del Panel Tracy de Flight
Este es un conjunto de extensiones para hacer el trabajo con Flight un poco más enriquecido.
- Flight - Analiza todas las variables de Flight.
- Database - Analiza todas las consultas que se han ejecutado en la página (si inicia correctamente la conexión a la base de datos)
- Request - Analiza todas las variables
$_SERVERy examina todas las cargas globales ($_GET,$_POST,$_FILES) - Session - Analiza todas las variables
$_SESSIONsi las sesiones están activas. - Twig (opcional) - Analiza el tiempo de renderizado de plantillas Twig, memoria y qué plantillas/bloques/macros se ejecutaron (requiere
twig/twigy una configuracióntwig_profile)
Esto es especialmente útil con el esqueleto oficial, que por defecto usa Twig: el mismo diseño que siguen las herramientas de IA también aparece claramente en la barra de Tracy.
Este es el Panel

¡Y cada panel muestra información muy útil sobre su aplicación!

Haga clic aquí para ver el código.
Instalación
¡Ejecute composer require flightphp/tracy-extensions --dev y ya está en camino!
Twig no es una dependencia estricta del paquete. Instale twig/twig solo si desea el panel de Twig (el esqueleto ya lo hace para las vistas).
Configuración
Hay muy poca configuración que necesite hacer para comenzar. Necesitará iniciar el depurador Tracy antes de usar esto https://tracy.nette.org/en/guide:
<?php
use Tracy\Debugger;
use flight\debug\tracy\TracyExtensionLoader;
// código de arranque
require __DIR__ . '/vendor/autoload.php';
Debugger::enable();
// Es posible que necesite especificar su entorno con Debugger::enable(Debugger::DEVELOPMENT)
// si usa conexiones de base de datos en su aplicación, hay un
// envoltorio PDO requerido para usar SOLO EN DESARROLLO (¡no en producción!)
// Tiene los mismos parámetros que una conexión PDO normal
$pdo = new PdoQueryCapture('sqlite:test.db', 'user', 'pass');
// o si adjunta esto al framework Flight
Flight::register('db', PdoQueryCapture::class, ['sqlite:test.db', 'user', 'pass']);
// ahora cada vez que haga una consulta capturará el tiempo, la consulta y los parámetros
// Esto conecta los puntos
if(Debugger::$showBar === true) {
// Esto necesita ser false o Tracy no puede renderizar realmente :(
Flight::set('flight.content_length', false);
new TracyExtensionLoader(Flight::app());
}
// más código
Flight::start();
Configuración Adicional
Datos de Sesión
Si tiene un manejador de sesión personalizado (como ghostff/session), puede pasar cualquier array de datos de sesión a Tracy y automáticamente lo mostrará. Lo pasa con la clave session_data en el segundo parámetro del constructor de TracyExtensionLoader.
use Ghostff\Session\Session;
// o use flight\Session;
require 'vendor/autoload.php';
$app = Flight::app();
$app->register('session', Session::class);
if(Debugger::$showBar === true) {
// Esto necesita ser false o Tracy no puede renderizar realmente :(
Flight::set('flight.content_length', false);
new TracyExtensionLoader(Flight::app(), [ 'session_data' => Flight::session()->getAll() ]);
}
// rutas y otras cosas...
Flight::start();
Panel de Twig (opcional)
Si su aplicación usa Twig (incluido el esqueleto oficial), puede mostrar métricas de plantillas en la barra de Tracy. Cree un Profile de Twig, adjunte ProfilerExtension a su entorno, luego pase ese perfil al cargador bajo la clave twig_profile. Adjunte el perfilado solo en desarrollo.
<?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,
]);
// Opcional: expone helpers de dump de Tracy en las plantillas
// {{ 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);
}
// Mapea Flight::render() a Twig (ejemplo)
Flight::map('render', function (string $template, array $data = []) use ($twig) {
if (substr($template, -5) !== '.twig') {
$template .= '.twig';
}
echo $twig->render($template, $data);
});
Lo que muestra el panel
- Tiempo total de renderizado de Twig y memoria
- Conteo de llamadas a plantillas / bloques / macros
- Cada plantilla que se renderizó, con su propio tiempo y memoria
La pestaña de Twig está oculta cuando no se renderizaron plantillas para la solicitud, o cuando omite twig_profile (o no tiene Twig instalado): los otros paneles de Flight siguen funcionando.
En un services.php de estilo esqueleto, construya el mismo $profile / ProfilerExtension cuando la depuración esté activada, pase twig_profile a TracyExtensionLoader, y siga usando su entorno Twig compartido para $app->render().
Latte
Se requiere PHP 8.1+ para esta sección.
Si tiene Latte instalado en su proyecto, Tracy tiene una integración nativa con Latte para analizar sus plantillas. Simplemente registre la extensión con su instancia de Latte (este es el propio puente de Tracy de Latte, no el panel de Twig anterior).
require 'vendor/autoload.php';
$app = Flight::app();
$app->map('render', function($template, $data, $block = null) {
$latte = new Latte\Engine;
// otras configuraciones...
// solo agrega la extensión si la Barra de Depuración de Tracy está habilitada
if(Debugger::$showBar === true) {
// aquí es donde agrega el Panel de Latte a Tracy
$latte->addExtension(new Latte\Bridges\Tracy\TracyExtension);
}
$latte->render($template, $data, $block);
});
Ver También
- Tracy - Configuración base de Tracy para Flight
- Twig - Plantillas usadas por el esqueleto y el panel de Twig
- Plantillas - Cómo Flight mapea
rendera Twig/Latte - Instalación - El esqueleto incluye tracy-extensions en dev
Awesome-plugins/apm
Documentación de FlightPHP APM
Bienvenido a FlightPHP APM—tu coach personal de rendimiento de aplicaciones. Esta guía es tu hoja de ruta para configurar, usar y dominar el Monitoreo de Rendimiento de Aplicaciones (APM) con FlightPHP. Ya sea que estés buscando solicitudes lentas o quieras entusiasmarte con los gráficos de latencia, te tenemos cubierto. ¡Hagamos tu aplicación más rápida, a tus usuarios más felices y tus sesiones de depuración más fáciles!
Ver una demo del panel para el sitio de documentación de Flight.

Por qué importa el APM
Imagina esto: tu aplicación es un restaurante ocupado. Sin una forma de rastrear cuánto tiempo toman los pedidos o dónde se atasca la cocina, estás adivinando por qué los clientes se van enfadados. El APM es tu sous-chef—vigila cada paso, desde las solicitudes entrantes hasta las consultas de base de datos, y marca cualquier cosa que te esté ralentizando. Las páginas lentas pierden usuarios (¡los estudios dicen que el 53% rebota si un sitio tarda más de 3 segundos en cargar!), y el APM te ayuda a detectar esos problemas antes de que duela. Es tranquilidad proactiva—menos momentos de "¿por qué está roto esto?" y más victorias de "¡mira qué fluido funciona esto!"
Instalación
Comienza con Composer:
composer require flightphp/apm
Necesitarás:
- PHP 7.4+: Nos mantiene compatibles con las distribuciones LTS de Linux mientras soportamos PHP moderno.
- FlightPHP Core v3.15+: El framework ligero que estamos potenciando.
Bases de datos soportadas
FlightPHP APM actualmente soporta las siguientes bases de datos para almacenar métricas:
- SQLite3: Simple, basada en archivos, y excelente para desarrollo local o aplicaciones pequeñas. Opción predeterminada en la mayoría de configuraciones.
- MySQL/MariaDB: Ideal para proyectos más grandes o entornos de producción donde necesitas almacenamiento robusto y escalable.
Puedes elegir tu tipo de base de datos durante el paso de configuración (ver abajo). Asegúrate de que tu entorno PHP tenga las extensiones necesarias instaladas (ej. pdo_sqlite o pdo_mysql).
Comenzando
Aquí está tu guía paso a paso para la grandeza del APM:
1. Registrar el APM
Coloca esto en tu archivo index.php o services.php para comenzar el seguimiento:
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);
// Si estás agregando una conexión de base de datos
// Prefiere SimplePdo (o PdoQueryCapture de Tracy Extensions en desarrollo).
// Habilita el seguimiento de consultas APM a través del array de opciones (5to argumento).
$pdo = new SimplePdo('mysql:host=localhost;dbname=example', 'user', 'pass', null, [
'trackApmQueries' => true, // requerido para capturar consultas para el APM
]);
$Apm->addPdoConnection($pdo);
¿Qué está pasando aquí?
LoggerFactory::create()toma tu configuración (más sobre eso pronto) y configura un registrador—SQLite por defecto.Apmes la estrella—escucha los eventos de Flight (solicitudes, rutas, errores, etc.) y recolecta métricas.bindEventsToFlightInstance($app)lo vincula todo a tu aplicación Flight.
Consejo Pro: Muestreo Si tu aplicación está ocupada, registrar cada solicitud podría sobrecargar las cosas. Usa una tasa de muestreo (0.0 a 1.0):
$Apm = new Apm($ApmLogger, 0.1); // Registra el 10% de las solicitudes
Esto mantiene el rendimiento ágil mientras te da datos sólidos.
2. Configurarlo
Ejecuta esto para crear tu .runway-config.json:
php vendor/bin/runway apm:init
¿Qué hace esto?
- Lanza un asistente preguntando de dónde vienen las métricas crudas (fuente) y dónde va la data procesada (destino).
- El predeterminado es SQLite—ej.
sqlite:/tmp/apm_metrics.sqlitepara fuente, otro para destino. - Terminarás con una configuración como:
{ "apm": { "source_type": "sqlite", "source_db_dsn": "sqlite:/tmp/apm_metrics.sqlite", "storage_type": "sqlite", "dest_db_dsn": "sqlite:/tmp/apm_metrics_processed.sqlite" } }
Este proceso también preguntará si quieres ejecutar las migraciones para esta configuración. Si lo estás configurando por primera vez, la respuesta es sí.
¿Por qué dos ubicaciones? Las métricas crudas se acumulan rápidamente (piensa en registros sin filtrar). El worker las procesa en un destino estructurado para el panel. ¡Mantiene las cosas ordenadas!
3. Procesar métricas con el Worker
El worker convierte las métricas crudas en datos listos para el panel. Ejecútalo una vez:
php vendor/bin/runway apm:worker
¿Qué está haciendo?
- Lee de tu fuente (ej.
apm_metrics.sqlite). - Procesa hasta 100 métricas (tamaño de lote predeterminado) a tu destino.
- Se detiene cuando termina o si no quedan métricas.
Mantenerlo Ejecutándose Para aplicaciones en vivo, querrás procesamiento continuo. Aquí están tus opciones:
-
Modo Daemon:
php vendor/bin/runway apm:worker --daemonSe ejecuta para siempre, procesando métricas a medida que llegan. Excelente para desarrollo o configuraciones pequeñas.
-
Crontab: Agrega esto a tu crontab (
crontab -e):* * * * * php /path/to/project/vendor/bin/runway apm:workerSe ejecuta cada minuto—perfecto para producción.
-
Tmux/Screen: Inicia una sesión desmontable:
tmux new -s apm-worker php vendor/bin/runway apm:worker --daemon # Ctrl+B, luego D para desmontar; `tmux attach -t apm-worker` para reconectarLo mantiene ejecutándose incluso si cierras sesión.
-
Personalizaciones:
php vendor/bin/runway apm:worker --batch_size 50 --max_messages 1000 --timeout 300--batch_size 50: Procesa 50 métricas a la vez.--max_messages 1000: Se detiene después de 1000 métricas.--timeout 300: Sale después de 5 minutos.
¿Por qué molestarse? Sin el worker, tu panel está vacío. Es el puente entre los registros crudos y los insights accionables.
4. Lanzar el Panel
Ve los signos vitales de tu aplicación:
php vendor/bin/runway apm:dashboard
¿Qué es esto?
- Inicia un servidor PHP en
http://localhost:8001/apm/dashboard. - Muestra registros de solicitudes, rutas lentas, tasas de error, y más.
Personalizarlo:
php vendor/bin/runway apm:dashboard --host 0.0.0.0 --port 8080 --php-path=/usr/local/bin/php
--host 0.0.0.0: Accesible desde cualquier IP (útil para visualización remota).--port 8080: Usa un puerto diferente si 8001 está ocupado.--php-path: Apunta a PHP si no está en tu PATH.
¡Visita la URL en tu navegador y explora!
Modo Producción
Para producción, puede que necesites probar algunas técnicas para hacer que el panel funcione ya que probablemente hay firewalls y otras medidas de seguridad en su lugar. Aquí algunas opciones:
- Usar un Proxy Inverso: Configura Nginx o Apache para reenviar solicitudes al panel.
- Túnel SSH: Si puedes hacer SSH al servidor, usa
ssh -L 8080:localhost:8001 tuusuario@tuservidorpara tunelizar el panel a tu máquina local. - VPN: Si tu servidor está detrás de una VPN, conéctate a ella y accede al panel directamente.
- Configurar Firewall: Abre el puerto 8001 para tu IP o la red del servidor. (o cualquier puerto que configures).
- Configurar Apache/Nginx: Si tienes un servidor web frente a tu aplicación, puedes configurarlo a un dominio o subdominio. Si haces esto, establecerás el document root a
/ruta/a/tu/proyecto/vendor/flightphp/apm/dashboard
¿Quieres un panel diferente?
¡Puedes construir tu propio panel si quieres! ¡Mira el directorio vendor/flightphp/apm/src/apm/presenter para ideas sobre cómo presentar los datos para tu propio panel!
Características del Panel
El panel es tu HQ del APM—aquí está lo que verás:
- Registro de Solicitudes: Cada solicitud con marca de tiempo, URL, código de respuesta, y tiempo total. Haz clic en "Detalles" para middleware, consultas y errores.
- Solicitudes Más Lentas: Las 5 solicitudes principales que consumen tiempo (ej. "/api/heavy" en 2.5s).
- Rutas Más Lentas: Las 5 rutas principales por tiempo promedio—excelente para detectar patrones.
- Tasa de Error: Porcentaje de solicitudes fallidas (ej. 2.3% 500s).
- Percentiles de Latencia: Tiempos de respuesta del percentil 95 (p95) y 99 (p99)—conoce tus peores escenarios.
- Gráfico de Códigos de Respuesta: Visualiza 200s, 404s, 500s a lo largo del tiempo.
- Consultas/Middleware Largos: Las 5 llamadas de base de datos y capas de middleware más lentas.
- Aciertos/Fallos de Caché: Con qué frecuencia tu caché salva el día.
Extras:
- Filtrar por "Última Hora," "Último Día," o "Última Semana."
- Alternar modo oscuro para esas sesiones nocturnas.
Ejemplo:
Una solicitud a /users podría mostrar:
- Tiempo Total: 150ms
- Middleware:
AuthMiddleware->handle(50ms) - Consulta:
SELECT * FROM users(80ms) - Caché: Acierto en
user_list(5ms)
Agregando Eventos Personalizados
Rastrea cualquier cosa—como una llamada API o proceso de pago:
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
]));
¿Dónde aparece? En los detalles de solicitud del panel bajo "Eventos Personalizados"—expandible con formato JSON bonito.
Caso de Uso:
$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
]));
¡Ahora verás si esa API está arrastrando tu aplicación!
Monitoreo de Base de Datos
Rastrea consultas PDO así:
use flight\database\SimplePdo;
$pdo = new SimplePdo('sqlite:/path/to/db.sqlite', null, null, null, [
'trackApmQueries' => true, // requerido para capturar consultas para el APM
]);
$Apm->addPdoConnection($pdo);
Lo que obtienes:
- Texto de consulta (ej.
SELECT * FROM users WHERE id = ?) - Tiempo de ejecución (ej. 0.015s)
- Conteo de filas (ej. 42)
Atención:
- Opcional: Omite esto si no necesitas seguimiento de BD.
- SimplePdo (preferido): Usa
SimplePdocontrackApmQueries => true. El obsoletoPdoWrapperaún funciona (5to argumento del constructortrue). PDO core crudo aún no está conectado—¡mantente atento! - Advertencia de Rendimiento: Registrar cada consulta en un sitio pesado en BD puede ralentizar las cosas. Usa muestreo (
$Apm = new Apm($ApmLogger, 0.1)) para aligerar la carga.
Salida de Ejemplo:
- Consulta:
SELECT name FROM products WHERE price > 100 - Tiempo: 0.023s
- Filas: 15
Opciones del Worker
Ajusta el worker a tu gusto:
--timeout 300: Se detiene después de 5 minutos—bueno para pruebas.--max_messages 500: Limita a 500 métricas—lo mantiene finito.--batch_size 200: Procesa 200 a la vez—equilibra velocidad y memoria.--daemon: Se ejecuta sin parar—ideal para monitoreo en vivo.
Ejemplo:
php vendor/bin/runway apm:worker --daemon --batch_size 100 --timeout 3600
Se ejecuta por una hora, procesando 100 métricas a la vez.
ID de Solicitud en la App
Cada solicitud tiene un ID de solicitud único para seguimiento. Puedes usar este ID en tu aplicación para correlacionar registros y métricas. Por ejemplo, puedes agregar el ID de solicitud a una página de error:
Flight::map('error', function($message) {
// Obtén el ID de solicitud del encabezado de respuesta X-Flight-Request-Id
$requestId = Flight::response()->getHeader('X-Flight-Request-Id');
// Adicionalmente podrías obtenerlo de la variable de Flight
// Este método no funcionará bien en swoole u otras plataformas async.
// $requestId = Flight::get('apm.request_id');
echo "Error: $message (Request ID: $requestId)";
});
Actualización
Si estás actualizando a una versión más nueva del APM, hay una posibilidad de que haya migraciones de base de datos que necesiten ejecutarse. Puedes hacer esto ejecutando el siguiente comando:
php vendor/bin/runway apm:migrate
Esto ejecutará cualquier migración que sea necesaria para actualizar el esquema de la base de datos a la última versión.
Nota: Si tu base de datos APM es grande en tamaño, estas migraciones pueden tomar algo de tiempo para ejecutarse. Puede que quieras ejecutar este comando durante horas de bajo tráfico.
Actualizando de 0.4.3 -> 0.5.0
Si estás actualizando de 0.4.3 a 0.5.0, necesitarás ejecutar el siguiente comando:
php vendor/bin/runway apm:config-migrate
Esto migrará tu configuración del formato antiguo usando el archivo .runway-config.json al nuevo formato que almacena los pares clave/valor en el archivo config.php.
Purgando Datos Antiguos
Para mantener tu base de datos ordenada, puedes purgar datos antiguos. Esto es especialmente útil si estás ejecutando una aplicación ocupada y quieres mantener el tamaño de la base de datos manejable. Puedes hacer esto ejecutando el siguiente comando:
php vendor/bin/runway apm:purge
Esto eliminará todos los datos más antiguos de 30 días de la base de datos. Puedes ajustar el número de días pasando un valor diferente a la opción --days:
php vendor/bin/runway apm:purge --days 7
Esto eliminará todos los datos más antiguos de 7 días de la base de datos.
Solución de Problemas
¿Atascado? Prueba esto:
-
¿Sin datos en el panel?
- ¿Está ejecutándose el worker? Verifica
ps aux | grep apm:worker. - ¿Coinciden las rutas de configuración? Verifica que los DSN de
.runway-config.jsonapunten a archivos reales. - Ejecuta
php vendor/bin/runway apm:workermanualmente para procesar métricas pendientes.
- ¿Está ejecutándose el worker? Verifica
-
¿Errores del Worker?
- Echa un vistazo a tus archivos SQLite (ej.
sqlite3 /tmp/apm_metrics.sqlite "SELECT * FROM apm_metrics_log LIMIT 5"). - Revisa los registros de PHP para trazas de pila.
- Echa un vistazo a tus archivos SQLite (ej.
-
¿El panel no inicia?
- ¿Puerto 8001 en uso? Usa
--port 8080. - ¿PHP no encontrado? Usa
--php-path /usr/bin/php. - ¿Firewall bloqueando? Abre el puerto o usa
--host localhost.
- ¿Puerto 8001 en uso? Usa
-
¿Demasiado lento?
- Baja la tasa de muestreo:
$Apm = new Apm($ApmLogger, 0.05)(5%). - Reduce el tamaño de lote:
--batch_size 20.
- Baja la tasa de muestreo:
-
¿No rastreando excepciones/errores?
- Si tienes Tracy habilitado para tu proyecto, sobrescribirá el manejo de errores de Flight. Necesitarás deshabilitar Tracy y luego asegurarte de que
Flight::set('flight.handle_errors', true);esté configurado.
- Si tienes Tracy habilitado para tu proyecto, sobrescribirá el manejo de errores de Flight. Necesitarás deshabilitar Tracy y luego asegurarte de que
-
¿No rastreando consultas de base de datos?
- Prefiere
SimplePdocon['trackApmQueries' => true]como el 5to argumento del constructor (array de opciones). - Si aún usas el obsoleto
PdoWrapper, pasatruecomo el 5to argumento. - Llama a
$Apm->addPdoConnection($pdo)después de crear la conexión.
- Prefiere
Awesome-plugins/tracy
Tracy
Tracy es un manejador de errores increíble que se puede usar con Flight. Tiene una serie de paneles que pueden ayudarte a depurar tu aplicación. También es muy fácil de extender y agregar tus propios paneles. El equipo de Flight ha creado algunos paneles específicamente para proyectos Flight con el complemento flightphp/tracy-extensions (Flight vars, consultas DB, solicitud, sesión, y un panel opcional de Twig cuando pasas un perfil de profiler—ver Tracy Extensions).
Instalación
Instalar con composer. Y en realidad querrás instalar esto sin la versión de desarrollo ya que Tracy viene con un componente de manejo de errores de producción.
composer require tracy/tracy
Configuración Básica
Hay algunas opciones de configuración básicas para empezar. Puedes leer más sobre ellas en la Documentación de Tracy.
require 'vendor/autoload.php';
use Tracy\Debugger;
// Habilitar Tracy
Debugger::enable();
// Debugger::enable(Debugger::DEVELOPMENT) // a veces tienes que ser explícito (también Debugger::PRODUCTION)
// Debugger::enable('23.75.345.200'); // también puedes proporcionar un array de direcciones IP
// Aquí es donde se registrarán errores y excepciones. Asegúrate de que este directorio exista y sea escribible.
Debugger::$logDirectory = __DIR__ . '/../log/';
Debugger::$strictMode = true; // mostrar todos los errores
// Debugger::$strictMode = E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED; // todos los errores excepto avisos obsoletos
if (Debugger::$showBar) {
$app->set('flight.content_length', false); // si la barra de Debugger está visible, entonces content-length no puede ser establecido por Flight
// Esto es específico de la Extensión Tracy para Flight si la has incluido
// de lo contrario comenta esto.
new TracyExtensionLoader($app);
}
Consejos Útiles
Cuando estés depurando tu código, hay algunas funciones muy útiles para mostrar datos para ti.
bdump($var)- Esto volcará la variable a la Barra Tracy en un panel separado.dumpe($var)- Esto volcará la variable y luego morirá inmediatamente.
Awesome-plugins/active_record
Flight Active Record
Un active record es un mapeo de una entidad de base de datos a un objeto PHP. En términos simples, si tienes una tabla de usuarios en tu base de datos, puedes "traducir" una fila en esa tabla a una clase User y un objeto $user en tu código. Ver ejemplo básico.
Haz clic aquí para el repositorio en GitHub.
Ejemplo Básico
Supongamos que tienes la siguiente tabla:
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT,
password TEXT
);
Ahora puedes configurar una nueva clase para representar esta tabla:
/**
* Una clase ActiveRecord suele ser singular
*
* Se recomienda encarecidamente agregar las propiedades de la tabla como comentarios aquí
*
* @property int $id
* @property string $name
* @property string $password
*/
class User extends flight\ActiveRecord {
public function __construct($database_connection)
{
// puedes configurarlo de esta manera
parent::__construct($database_connection, 'users');
// o de esta manera
parent::__construct($database_connection, null, [ 'table' => 'users']);
}
}
¡Ahora observa la magia suceder!
// para sqlite
$database_connection = new PDO('sqlite:test.db'); // esto es solo un ejemplo, probablemente usarías una conexión de base de datos real
// para mysql
$database_connection = new PDO('mysql:host=localhost;dbname=test_db&charset=utf8bm4', 'username', 'password');
// o mysqli
$database_connection = new mysqli('localhost', 'username', 'password', 'test_db');
// o mysqli con creación no basada en objetos
$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();
// o $user->save();
echo $user->id; // 1
$user->name = 'Joseph Mamma';
$user->password = password_hash('some cool password again!!!');
$user->insert();
// ¡no puedes usar $user->save() aquí o pensará que es una actualización!
echo $user->id; // 2
¡Y fue tan fácil agregar un nuevo usuario! Ahora que hay una fila de usuario en la base de datos, ¿cómo la extraes?
$user->find(1); // busca id = 1 en la base de datos y la devuelve.
echo $user->name; // 'Bobby Tables'
¿Y si quieres encontrar todos los usuarios?
$users = $user->findAll();
¿Qué pasa con una condición específica?
$users = $user->like('name', '%mamma%')->findAll();
¿Ves lo divertido que es? ¡Instálalo y comencemos!
Instalación
Simplemente instala con Composer
composer require flightphp/active-record
Uso
Esto se puede usar como una biblioteca independiente o con el Flight PHP Framework. Completamente a tu elección.
Independiente
Solo asegúrate de pasar una conexión PDO al constructor.
$pdo_connection = new PDO('sqlite:test.db'); // esto es solo un ejemplo, probablemente usarías una conexión de base de datos real
$User = new User($pdo_connection);
¿No quieres configurar siempre tu conexión de base de datos en el constructor? Ver Gestión de Conexión de Base de Datos para otras ideas!
Registrar como un método en Flight
Si estás usando el Flight PHP Framework, puedes registrar la clase ActiveRecord como un servicio, pero honestamente no tienes que hacerlo.
Flight::register('user', 'User', [ $pdo_connection ]);
// entonces puedes usarlo así en un controlador, una función, etc.
Flight::user()->find(1);
Métodos runway
runway es una herramienta CLI para Flight que tiene un comando personalizado para esta biblioteca.
# Uso
php runway make:record database_table_name [class_name]
# Ejemplo
php runway make:record users
Esto creará una nueva clase en el directorio app/records/ como UserRecord.php con el siguiente contenido:
<?php
declare(strict_types=1);
namespace app\records;
/**
* Clase ActiveRecord para la tabla 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 Configura las relaciones para el modelo
* https://docs.flightphp.com/awesome-plugins/active-record#relationships
*/
protected array $relations = [
// 'relation_name' => [ self::HAS_MANY, 'RelatedClass', 'foreign_key' ],
];
/**
* Constructor
* @param mixed $databaseConnection La conexión a la base de datos
*/
public function __construct($databaseConnection)
{
parent::__construct($databaseConnection, 'users');
}
}
Funciones CRUD
find($id = null) : boolean|ActiveRecord
Encuentra un registro y lo asigna al objeto actual. Si pasas un $id de algún tipo, realizará una búsqueda en la clave primaria con ese valor. Si no se pasa nada, simplemente encontrará el primer registro en la tabla.
Además, puedes pasarle otros métodos auxiliares para consultar tu tabla.
// encontrar un registro con algunas condiciones previas
$user->notNull('password')->orderBy('id DESC')->find();
// encontrar un registro por un id específico
$id = 123;
$user->find($id);
findAll(): array<int,ActiveRecord>
Encuentra todos los registros en la tabla que especifiques.
$user->findAll();
isHydrated(): boolean (v0.4.0)
Devuelve true si el registro actual ha sido hidratado (recuperado de la base de datos).
$user->find(1);
// si se encuentra un registro con datos...
$user->isHydrated(); // true
insert(): boolean|ActiveRecord
Inserta el registro actual en la base de datos.
$user = new User($pdo_connection);
$user->name = 'demo';
$user->password = md5('demo');
$user->insert();
Claves Primarias Basadas en Texto
Si tienes una clave primaria basada en texto (como un UUID), puedes establecer el valor de la clave primaria antes de insertar de una de dos maneras.
$user = new User($pdo_connection, [ 'primaryKey' => 'uuid' ]);
$user->uuid = 'some-uuid';
$user->name = 'demo';
$user->password = md5('demo');
$user->insert(); // o $user->save();
o puedes tener la clave primaria generada automáticamente para ti a través de eventos.
class User extends flight\ActiveRecord {
public function __construct($database_connection)
{
parent::__construct($database_connection, 'users', [ 'primaryKey' => 'uuid' ]);
// también puedes establecer la primaryKey de esta manera en lugar del array anterior.
$this->primaryKey = 'uuid';
}
protected function beforeInsert(self $self) {
$self->uuid = uniqid(); // o como necesites generar tus ids únicos
}
}
Si no estableces la clave primaria antes de insertar, se establecerá en rowid y la base de datos la generará para ti, pero no persistirá porque ese campo puede no existir en tu tabla. Por eso se recomienda usar el evento para manejar esto automáticamente.
update(): boolean|ActiveRecord
Actualiza el registro actual en la base de datos.
$user->greaterThan('id', 0)->orderBy('id desc')->find();
$user->email = 'test@example.com';
$user->update();
save(): boolean|ActiveRecord
Inserta o actualiza el registro actual en la base de datos. Si el registro tiene un id, actualizará, de lo contrario insertará.
$user = new User($pdo_connection);
$user->name = 'demo';
$user->password = md5('demo');
$user->save();
Nota: Si tienes relaciones definidas en la clase, también guardará recursivamente esas relaciones si han sido definidas, instanciadas y tienen datos sucios para actualizar. (v0.4.0 y superior)
delete(): boolean
Elimina el registro actual de la base de datos.
$user->gt('id', 0)->orderBy('id desc')->find();
$user->delete();
También puedes eliminar múltiples registros ejecutando una búsqueda previamente.
$user->like('name', 'Bob%')->delete();
dirty(array $dirty = []): ActiveRecord
Los datos sucios se refieren a los datos que han cambiado en un registro.
$user->greaterThan('id', 0)->orderBy('id desc')->find();
// nada es "sucio" hasta este punto.
$user->email = 'test@example.com'; // ahora email se considera "sucio" ya que ha cambiado.
$user->update();
// ahora no hay datos sucios porque se han actualizado y persistido en la base de datos
$user->password = password_hash()'newpassword'); // ahora esto es sucio
$user->dirty(); // pasar nada limpiará todas las entradas sucias.
$user->update(); // nada se actualizará porque nada fue capturado como sucio.
$user->dirty([ 'name' => 'something', 'password' => password_hash('a different password') ]);
$user->update(); // tanto name como password se actualizan.
copyFrom(array $data): ActiveRecord (v0.4.0)
Este es un alias para el método dirty(). Es un poco más claro lo que estás haciendo.
$user->copyFrom([ 'name' => 'something', 'password' => password_hash('a different password') ]);
$user->update(); // tanto name como password se actualizan.
isDirty(): boolean (v0.4.0)
Devuelve true si el registro actual ha sido cambiado.
$user->greaterThan('id', 0)->orderBy('id desc')->find();
$user->email = 'test@email.com';
$user->isDirty(); // true
reset(bool $include_query_data = true): ActiveRecord
Reinicia el registro actual a su estado inicial. Esto es realmente bueno para usar en comportamientos de tipo bucle. Si pasas true, también reiniciará los datos de consulta que se usaron para encontrar el objeto actual (comportamiento predeterminado).
$users = $user->greaterThan('id', 0)->orderBy('id desc')->find();
$user_company = new UserCompany($pdo_connection);
foreach($users as $user) {
$user_company->reset(); // comenzar con una pizarra limpia
$user_company->user_id = $user->id;
$user_company->company_id = $some_company_id;
$user_company->insert();
}
getBuiltSql(): string (v0.4.1)
Después de ejecutar un método find(), findAll(), insert(), update(), o save() puedes obtener el SQL que se construyó y usarlo para fines de depuración.
Métodos de Consulta SQL
select(string $field1 [, string $field2 ... ])
Puedes seleccionar solo algunas de las columnas en una tabla si lo deseas (es más performant en tablas realmente anchas con muchas columnas)
$user->select('id', 'name')->find();
from(string $table)
¡Técnicamente puedes elegir otra tabla también! ¿Por qué no?!
$user->select('id', 'name')->from('user')->find();
join(string $table_name, string $join_condition)
Incluso puedes unirte a otra tabla en la base de datos.
$user->join('contacts', 'contacts.user_id = users.id')->find();
where(string $where_conditions)
Puedes establecer algunos argumentos where personalizados (no puedes establecer params en esta declaración where)
$user->where('id=1 AND name="demo"')->find();
Nota de Seguridad - Podrías sentirte tentado a hacer algo como $user->where("id = '{$id}' AND name = '{$name}'")->find();. ¡POR FAVOR NO HAGAS ESTO!!! Esto es susceptible a lo que se conoce como ataques de inyección SQL. Hay muchos artículos en línea, por favor busca "sql injection attacks php" y encontrarás muchos artículos sobre este tema. La forma adecuada de manejar esto con esta biblioteca es, en lugar de este método where(), harías algo más como $user->eq('id', $id)->eq('name', $name)->find(); Si absolutamente tienes que hacer esto, la biblioteca PDO tiene $pdo->quote($var) para escaparlo por ti. Solo después de usar quote() puedes usarlo en una declaración where().
group(string $group_by_statement)/groupBy(string $group_by_statement)
Agrupa tus resultados por una condición particular.
$user->select('COUNT(*) as count')->groupBy('name')->findAll();
order(string $order_by_statement)/orderBy(string $order_by_statement)
Ordena la consulta devuelta de cierta manera.
$user->orderBy('name DESC')->find();
limit(string $limit)/limit(int $offset, int $limit)
Limita la cantidad de registros devueltos. Si se da un segundo int, será offset, limit justo como en SQL.
$user->orderby('name DESC')->limit(0, 10)->findAll();
Condiciones WHERE
equal(string $field, mixed $value) / eq(string $field, mixed $value)
Donde field = $value
$user->eq('id', 1)->find();
notEqual(string $field, mixed $value) / ne(string $field, mixed $value)
Donde field <> $value
$user->ne('id', 1)->find();
isNull(string $field)
Donde field IS NULL
$user->isNull('id')->find();
isNotNull(string $field) / notNull(string $field)
Donde field IS NOT NULL
$user->isNotNull('id')->find();
greaterThan(string $field, mixed $value) / gt(string $field, mixed $value)
Donde field > $value
$user->gt('id', 1)->find();
lessThan(string $field, mixed $value) / lt(string $field, mixed $value)
Donde field < $value
$user->lt('id', 1)->find();
greaterThanOrEqual(string $field, mixed $value) / ge(string $field, mixed $value) / gte(string $field, mixed $value)
Donde field >= $value
$user->ge('id', 1)->find();
lessThanOrEqual(string $field, mixed $value) / le(string $field, mixed $value) / lte(string $field, mixed $value)
Donde field <= $value
$user->le('id', 1)->find();
like(string $field, mixed $value) / notLike(string $field, mixed $value)
Donde field LIKE $value o field NOT LIKE $value
$user->like('name', 'de')->find();
in(string $field, array $values) / notIn(string $field, array $values)
Donde field IN($value) o field NOT IN($value)
$user->in('id', [1, 2])->find();
between(string $field, array $values)
Donde field BETWEEN $value AND $value1
$user->between('id', [1, 2])->find();
Condiciones OR
Es posible envolver tus condiciones en una declaración OR. Esto se hace con cualquiera de los métodos startWrap() y endWrap() o rellenando el tercer parámetro de la condición después del campo y valor.
// Método 1
$user->eq('id', 1)->startWrap()->eq('name', 'demo')->or()->eq('name', 'test')->endWrap('OR')->find();
// Esto evaluará a `id = 1 AND (name = 'demo' OR name = 'test')`
// Método 2
$user->eq('id', 1)->eq('name', 'demo', 'OR')->find();
// Esto evaluará a `id = 1 OR name = 'demo'`
Relaciones
Puedes establecer varios tipos de relaciones usando esta biblioteca. Puedes establecer relaciones uno-a-muchos y uno-a-uno entre tablas. Esto requiere un poco de configuración extra en la clase previamente.
Establecer el array $relations no es difícil, pero adivinar la sintaxis correcta puede ser confuso.
protected array $relations = [
// puedes nombrar la clave como quieras. El nombre del ActiveRecord probablemente sea bueno. Ej: user, contact, client
'user' => [
// requerido
// self::HAS_MANY, self::HAS_ONE, self::BELONGS_TO
self::HAS_ONE, // este es el tipo de relación
// requerido
'Some_Class', // esta es la clase ActiveRecord "otra" a la que se referenciará
// requerido
// dependiendo del tipo de relación
// self::HAS_ONE = la clave foránea que referencia la unión
// self::HAS_MANY = la clave foránea que referencia la unión
// self::BELONGS_TO = la clave local que referencia la unión
'local_or_foreign_key',
// solo FYI, esto también solo se une a la clave primaria del modelo "otro"
// opcional
[ 'eq' => [ 'client_id', 5 ], 'select' => 'COUNT(*) as count', 'limit' 5 ], // condiciones adicionales que quieres al unir la relación
// $record->eq('client_id', 5)->select('COUNT(*) as count')->limit(5))
// opcional
'back_reference_name' // esto es si quieres referenciar de vuelta esta relación a sí misma Ej: $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');
}
}
Ahora tenemos las referencias configuradas para poder usarlas muy fácilmente!
$user = new User($pdo_connection);
// encuentra el usuario más reciente.
$user->notNull('id')->orderBy('id desc')->find();
// obtén contactos usando la relación:
foreach($user->contacts as $contact) {
echo $contact->id;
}
// o podemos ir en la otra dirección.
$contact = new Contact();
// encuentra un contacto
$contact->find();
// obtén usuario usando la relación:
echo $contact->user->name; // este es el nombre del usuario
¿Bastante genial, eh?
Carga Ansiosa
Resumen
La carga ansiosa resuelve el problema de la consulta N+1 cargando relaciones de antemano. En lugar de ejecutar una consulta separada para las relaciones de cada registro, la carga ansiosa obtiene todos los datos relacionados en solo una consulta adicional por relación.
Nota: La carga ansiosa solo está disponible para v0.7.0 y superior.
Uso Básico
Usa el método with() para especificar qué relaciones cargar ansiosamente:
// Carga usuarios con sus contactos en 2 consultas en lugar de N+1
$users = $user->with('contacts')->findAll();
foreach ($users as $u) {
foreach ($u->contacts as $contact) {
echo $contact->email; // ¡Sin consulta adicional!
}
}
Múltiples Relaciones
Carga múltiples relaciones a la vez:
$users = $user->with(['contacts', 'profile', 'settings'])->findAll();
Tipos de Relación
HAS_MANY
// Carga ansiosamente todos los contactos para cada usuario
$users = $user->with('contacts')->findAll();
foreach ($users as $u) {
// $u->contacts ya está cargado como un array
foreach ($u->contacts as $contact) {
echo $contact->email;
}
}
HAS_ONE
// Carga ansiosamente un contacto para cada usuario
$users = $user->with('contact')->findAll();
foreach ($users as $u) {
// $u->contact ya está cargado como un objeto
echo $u->contact->email;
}
BELONGS_TO
// Carga ansiosamente usuarios padre para todos los contactos
$contacts = $contact->with('user')->findAll();
foreach ($contacts as $c) {
// $c->user ya está cargado
echo $c->user->name;
}
Con find()
La carga ansiosa funciona con tanto findAll() como find() :
$user = $user->with('contacts')->find(1);
// Usuario y todos sus contactos cargados en 2 consultas
Beneficios de Rendimiento
Sin carga ansiosa (problema N+1):
$users = $user->findAll(); // 1 consulta
foreach ($users as $u) {
$contacts = $u->contacts; // N consultas (una por usuario!)
}
// Total: 1 + N consultas
Con carga ansiosa:
$users = $user->with('contacts')->findAll(); // 2 consultas totales
foreach ($users as $u) {
$contacts = $u->contacts; // 0 consultas adicionales!
}
// Total: 2 consultas (1 para usuarios + 1 para todos los contactos)
Para 10 usuarios, esto reduce las consultas de 11 a 2 - una reducción del 82%!
Notas Importantes
- La carga ansiosa es completamente opcional - la carga perezosa aún funciona como antes
- Las relaciones ya cargadas se omiten automáticamente
- Las referencias de vuelta funcionan con carga ansiosa
- Los callbacks de relación se respetan durante la carga ansiosa
Limitaciones
- La carga ansiosa anidada (ej., with(['contacts.addresses']) ) no está soportada actualmente
- Las restricciones de carga ansiosa vía closures no están soportadas en esta versión
Estableciendo Datos Personalizados
A veces puedes necesitar adjuntar algo único a tu ActiveRecord como un cálculo personalizado que podría ser más fácil de adjuntar al objeto que luego se pasaría a, digamos, una plantilla.
setCustomData(string $field, mixed $value)
Adjuntas los datos personalizados con el método setCustomData().
$user->setCustomData('page_view_count', $page_view_count);
Y luego simplemente lo referencias como una propiedad de objeto normal.
echo $user->page_view_count;
Eventos
Una característica súper genial más sobre esta biblioteca es sobre eventos. Los eventos se activan en ciertos momentos basados en ciertos métodos que llamas. Son muy muy útiles para configurar datos automáticamente para ti.
onConstruct(ActiveRecord $ActiveRecord, array &config)
Esto es realmente útil si necesitas establecer una conexión predeterminada o algo así.
// index.php o bootstrap.php
Flight::register('db', 'PDO', [ 'sqlite:test.db' ]);
//
//
//
// User.php
class User extends flight\ActiveRecord {
protected function onConstruct(self $self, array &$config) { // no olvides la referencia &
// podrías hacer esto para establecer automáticamente la conexión
$config['connection'] = Flight::db();
// o esto
$self->transformAndPersistConnection(Flight::db());
// También puedes establecer el nombre de la tabla de esta manera.
$config['table'] = 'users';
}
}
beforeFind(ActiveRecord $ActiveRecord)
Esto probablemente solo es útil si necesitas una manipulación de consulta cada vez.
class User extends flight\ActiveRecord {
public function __construct($database_connection)
{
parent::__construct($database_connection, 'users');
}
protected function beforeFind(self $self) {
// siempre ejecuta id >= 0 si eso es lo tuyo
$self->gte('id', 0);
}
}
afterFind(ActiveRecord $ActiveRecord)
Este probablemente es más útil si siempre necesitas ejecutar alguna lógica cada vez que se recupera este registro. ¿Necesitas descifrar algo? ¿Necesitas ejecutar una consulta de conteo personalizada cada vez (no performant pero whatever)?
class User extends flight\ActiveRecord {
public function __construct($database_connection)
{
parent::__construct($database_connection, 'users');
}
protected function afterFind(self $self) {
// descifrando algo
$self->secret = yourDecryptFunction($self->secret, $some_key);
// tal vez almacenando algo personalizado como una consulta???
$self->setCustomData('view_count', $self->select('COUNT(*) count')->from('user_views')->eq('user_id', $self->id)['count'];
}
}
beforeFindAll(ActiveRecord $ActiveRecord)
Esto probablemente solo es útil si necesitas una manipulación de consulta cada vez.
class User extends flight\ActiveRecord {
public function __construct($database_connection)
{
parent::__construct($database_connection, 'users');
}
protected function beforeFindAll(self $self) {
// siempre ejecuta id >= 0 si eso es lo tuyo
$self->gte('id', 0);
}
}
afterFindAll(array<int,ActiveRecord> $results)
Similar a afterFind() pero puedes hacerlo a todos los registros en lugar de uno!
class User extends flight\ActiveRecord {
public function __construct($database_connection)
{
parent::__construct($database_connection, 'users');
}
protected function afterFindAll(array $results) {
foreach($results as $self) {
// haz algo genial como afterFind()
}
}
}
beforeInsert(ActiveRecord $ActiveRecord)
Realmente útil si necesitas algunos valores predeterminados establecidos cada vez.
class User extends flight\ActiveRecord {
public function __construct($database_connection)
{
parent::__construct($database_connection, 'users');
}
protected function beforeInsert(self $self) {
// establece algunos valores predeterminados sólidos
if(!$self->created_date) {
$self->created_date = gmdate('Y-m-d');
}
if(!$self->password) {
$self->password = password_hash((string) microtime(true));
}
}
}
afterInsert(ActiveRecord $ActiveRecord)
¿Tal vez tienes un caso de uso para cambiar datos después de insertarlos?
class User extends flight\ActiveRecord {
public function __construct($database_connection)
{
parent::__construct($database_connection, 'users');
}
protected function afterInsert(self $self) {
// haz lo que quieras
Flight::cache()->set('most_recent_insert_id', $self->id);
// o lo que sea....
}
}
beforeUpdate(ActiveRecord $ActiveRecord)
Realmente útil si necesitas algunos valores predeterminados establecidos cada vez en una actualización.
class User extends flight\ActiveRecord {
public function __construct($database_connection)
{
parent::__construct($database_connection, 'users');
}
protected function beforeInsert(self $self) {
// establece algunos valores predeterminados sólidos
if(!$self->updated_date) {
$self->updated_date = gmdate('Y-m-d');
}
}
}
afterUpdate(ActiveRecord $ActiveRecord)
¿Tal vez tienes un caso de uso para cambiar datos después de que se actualicen?
class User extends flight\ActiveRecord {
public function __construct($database_connection)
{
parent::__construct($database_connection, 'users');
}
protected function afterInsert(self $self) {
// haz lo que quieras
Flight::cache()->set('most_recently_updated_user_id', $self->id);
// o lo que sea....
}
}
beforeSave(ActiveRecord $ActiveRecord)/afterSave(ActiveRecord $ActiveRecord)
Esto es útil si quieres que los eventos ocurran tanto en inserts como en updates. Te ahorraré la larga explicación, pero estoy seguro de que puedes adivinar qué es.
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)
No estoy seguro de qué querrías hacer aquí, ¡pero sin juicios! ¡Adelante!
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:';
}
}
Gestión de Conexión de Base de Datos
Cuando usas esta biblioteca, puedes establecer la conexión de base de datos de varias maneras diferentes. Puedes establecer la conexión en el constructor, puedes establecerla a través de una variable de configuración $config['connection'] o puedes establecerla a través de setDatabaseConnection() (v0.4.1).
$pdo_connection = new PDO('sqlite:test.db'); // por ejemplo
$user = new User($pdo_connection);
// o
$user = new User(null, [ 'connection' => $pdo_connection ]);
// o
$user = new User();
$user->setDatabaseConnection($pdo_connection);
Si quieres evitar siempre establecer un $database_connection cada vez que llamas a un active record, ¡hay formas de evitarlo!
// index.php o bootstrap.php
// Establece esto como una clase registrada en 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);
}
}
// ¡Y ahora, sin argumentos requeridos!
$user = new User();
Nota: Si planeas hacer pruebas unitarias, hacerlo de esta manera puede agregar algunos desafíos a las pruebas unitarias, pero en general, porque puedes inyectar tu conexión con
setDatabaseConnection()o$config['connection']no es tan malo.
Si necesitas actualizar la conexión de base de datos, por ejemplo, si estás ejecutando un script CLI de larga duración y necesitas actualizar la conexión de vez en cuando, puedes reestablecer la conexión con $your_record->setDatabaseConnection($pdo_connection).
Contribuyendo
Por favor hazlo. :D
Configuración
Cuando contribuyas, asegúrate de ejecutar composer test-coverage para mantener una cobertura de pruebas del 100% (esto no es cobertura de pruebas unitarias real, más como pruebas de integración).
También asegúrate de ejecutar composer beautify y composer phpcs para corregir cualquier error de linting.
Licencia
MIT
Awesome-plugins/latte
Latte
Latte es un motor de plantillas completo que es muy fácil de usar y se siente más cercano a la sintaxis de PHP que Twig o Smarty. También es muy fácil de extender y agregar tus propios filtros y funciones.
Instalación
Instala con composer.
composer require latte/latte
Configuración Básica
Hay algunas opciones de configuración básicas para comenzar. Puedes leer más sobre ellas en la Documentación de Latte.
require 'vendor/autoload.php';
$app = Flight::app();
$app->map('render', function(string $template, array $data, ?string $block): void {
$latte = new Latte\Engine;
// Dónde Latte almacena específicamente su caché
$latte->setTempDirectory(__DIR__ . '/../cache/');
$finalPath = Flight::get('flight.views.path') . $template;
$latte->render($finalPath, $data, $block);
});
Ejemplo Simple de Diseño
Aquí hay un ejemplo simple de un archivo de diseño. Este es el archivo que se usará para envolver todas tus otras vistas.
<!-- app/views/layout.latte -->
<!doctype html>
<html lang="en">
<head>
<title>{$title ? $title . ' - '}Mi App</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<nav>
<!-- tus elementos de navegación aquí -->
</nav>
</header>
<div id="content">
<!-- Aquí está la magia -->
{block content}{/block}
</div>
<div id="footer">
© Copyright
</div>
</body>
</html>
Y ahora tenemos tu archivo que se va a renderizar dentro de ese bloque de contenido:
<!-- app/views/home.latte -->
<!-- Esto le dice a Latte que este archivo está "dentro" del archivo layout.latte -->
{extends layout.latte}
<!-- Este es el contenido que se renderizará dentro del diseño en el bloque de contenido -->
{block content}
<h1>Página de Inicio</h1>
<p>¡Bienvenido a mi app!</p>
{/block}
Luego, cuando vayas a renderizar esto dentro de tu función o controlador, harías algo como esto:
// ruta simple
Flight::route('/', function () {
Flight::render('home.latte', [
'title' => 'Página de Inicio'
]);
});
// o si estás usando un controlador
Flight::route('/', [HomeController::class, 'index']);
// HomeController.php
class HomeController
{
public function index()
{
Flight::render('home.latte', [
'title' => 'Página de Inicio'
]);
}
}
¡Consulta la Documentación de Latte para obtener más información sobre cómo usar Latte a su máximo potencial!
Depuración con Tracy
Se requiere PHP 8.1+ para esta sección.
¡También puedes usar Tracy para ayudar con la depuración de tus archivos de plantillas Latte directamente de la caja! Si ya tienes Tracy instalado, necesitas agregar la extensión de Latte a Tracy.
// services.php
use Tracy\Debugger;
$app->map('render', function(string $template, array $data, ?string $block): void {
$latte = new Latte\Engine;
// Dónde Latte almacena específicamente su caché
$latte->setTempDirectory(__DIR__ . '/../cache/');
$finalPath = Flight::get('flight.views.path') . $template;
// Esto solo agregará la extensión si la Barra de Depuración de Tracy está habilitada
if (Debugger::$showBar === true) {
// aquí es donde agregas el Panel de Latte a Tracy
$latte->addExtension(new Latte\Bridges\Tracy\TracyExtension);
}
$latte->render($finalPath, $data, $block);
});Awesome-plugins/awesome_plugins
Plugins Impresionantes
Flight es increíblemente extensible. Hay una serie de plugins que se pueden utilizar para agregar funcionalidad a tu aplicación Flight. Algunos son oficialmente soportados por el Equipo de Flight y otros son bibliotecas micro/lite para ayudarte a comenzar.
Herramientas de IA
Flight puede ser aún más genial con plugins impulsados por IA.
- Flight MCP - Un plugin para integrar MCP (Model Control Protocol) con Flight, permitiendo una funcionalidad sin problemas impulsada por IA. Principalmente enfocado en las páginas de documentación, ayuda a mantener bajos los costos de tokens proporcionando la información más actualizada sobre tus proyectos Flight.
Documentación de API
La documentación de API es crucial para cualquier API. Ayuda a los desarrolladores a entender cómo interactuar con tu API y qué esperar a cambio. Hay un par de herramientas disponibles para ayudarte a generar documentación de API para tus Proyectos Flight.
- FlightPHP OpenAPI Generator - Publicación de blog escrita por Daniel Schreiber sobre cómo usar la Especificación OpenAPI con FlightPHP para construir tu API utilizando un enfoque API primero.
- SwaggerUI - Swagger UI es una gran herramienta para ayudarte a generar documentación de API para tus proyectos Flight. Es muy fácil de usar y se puede personalizar para satisfacer tus necesidades. Esta es la biblioteca PHP para ayudarte a generar la documentación Swagger.
Monitoreo de Rendimiento de Aplicaciones (APM)
El Monitoreo de Rendimiento de Aplicaciones (APM) es crucial para cualquier aplicación. Te ayuda a entender cómo está funcionando tu aplicación y dónde están los cuellos de botella. Hay una serie de herramientas APM que se pueden usar con Flight.
- official flightphp/apm - Flight APM es una biblioteca APM simple que se puede usar para monitorear tus aplicaciones Flight. Se puede usar para monitorear el rendimiento de tu aplicación y ayudarte a identificar cuellos de botella.
Asíncrono
Flight ya es un framework rápido, ¡pero agregarle un motor turbo lo hace todo más divertido (y desafiante)!
- flightphp/async - Biblioteca Asíncrona oficial de Flight. Esta biblioteca es una forma simple de agregar procesamiento asíncrono a tu aplicación. Utiliza Swoole/Openswoole bajo el capó para proporcionar una forma simple y efectiva de ejecutar tareas de forma asíncrona.
Autorización/Permisos
La autorización y los permisos son cruciales para cualquier aplicación que requiera controles para establecer quién puede acceder a qué.
- official flightphp/permissions - Biblioteca de Permisos oficial de Flight. Esta biblioteca es una forma simple de agregar permisos a nivel de usuario y aplicación a tu aplicación.
Autenticación
La autenticación es esencial para las aplicaciones que necesitan verificar la identidad del usuario y asegurar los endpoints de API.
- firebase/php-jwt - Biblioteca de JSON Web Token (JWT) para PHP. Una forma simple y segura de implementar autenticación basada en tokens en tus aplicaciones Flight. Perfecta para autenticación de API sin estado, proteger rutas con middleware e implementar flujos de autorización de estilo OAuth.
Caché
El caché es una gran manera de acelerar tu aplicación. Hay una serie de bibliotecas de caché que se pueden usar con Flight.
- official flightphp/cache - Clase de caché en archivo PHP ligera, simple e independiente
CLI
Las aplicaciones CLI son una gran manera de interactuar con tu aplicación. Puedes usarlas para generar controladores, mostrar todas las rutas, y más.
- official flightphp/runway - Runway es una aplicación CLI que te ayuda a gestionar tus aplicaciones Flight.
Cookies
Las cookies son una gran manera de almacenar pequeños bits de datos en el lado del cliente. Se pueden usar para almacenar preferencias de usuario, configuraciones de aplicación, y más.
- overclokk/cookie - PHP Cookie es una biblioteca PHP que proporciona una forma simple y efectiva de gestionar cookies.
Depuración
La depuración es crucial cuando estás desarrollando en tu entorno local. Hay algunos plugins que pueden elevar tu experiencia de depuración.
- tracy/tracy - Este es un manejador de errores con todas las funciones que se puede usar con Flight. Tiene una serie de paneles que pueden ayudarte a depurar tu aplicación. También es muy fácil de extender y agregar tus propios paneles.
- official flightphp/tracy-extensions - Usado con el manejador de errores Tracy, este plugin agrega algunos paneles adicionales para ayudar con la depuración específicamente para proyectos Flight.
Bases de Datos
Las bases de datos son el núcleo de la mayoría de las aplicaciones. Así es como almacenas y recuperas datos. Algunas bibliotecas de bases de datos son simplemente envoltorios para escribir consultas y algunas son ORMs completos.
- official flightphp/core SimplePdo - Ayudante PDO oficial de Flight que forma parte del núcleo. Este es un envoltorio moderno con métodos de ayuda convenientes como
insert(),update(),delete(), ytransaction()para simplificar las operaciones de base de datos. Todos los resultados se devuelven como Colecciones para acceso flexible de array/objeto. No es un ORM, solo una mejor manera de trabajar con PDO. - deprecated flightphp/core PdoWrapper - Envoltorio PDO oficial de Flight que forma parte del núcleo (obsoleto desde v3.18.0). Usa SimplePdo en su lugar.
- official flightphp/active-record - ORM/Mapeador ActiveRecord oficial de Flight. Una gran biblioteca pequeña para recuperar y almacenar fácilmente datos en tu base de datos.
- byjg/php-migration - Plugin para hacer seguimiento de todos los cambios de base de datos para tu proyecto.
- knifelemon/easy-query - Constructor de consultas SQL fluido y ligero que genera SQL y parámetros para sentencias preparadas. Funciona muy bien con SimplePdo.
Encriptación
La encriptación es crucial para cualquier aplicación que almacene datos sensibles. Encriptar y desencriptar los datos no es terriblemente difícil, pero almacenar correctamente la clave de encriptación puede ser difícil. Lo más importante es nunca almacenar tu clave de encriptación en un directorio público o comprometerla en tu repositorio de código.
- defuse/php-encryption - Esta es una biblioteca que se puede usar para encriptar y desencriptar datos. Ponerse en marcha es bastante simple para comenzar a encriptar y desencriptar datos.
Cola de Trabajos
Las colas de trabajos son realmente útiles para procesar tareas de forma asíncrona. Esto puede ser enviar correos electrónicos, procesar imágenes, o cualquier cosa que no necesite hacerse en tiempo real.
- n0nag0n/simple-job-queue - Simple Job Queue es una biblioteca que se puede usar para procesar trabajos de forma asíncrona. Se puede usar con beanstalkd, MySQL/MariaDB, SQLite, y PostgreSQL.
Sesión
Las sesiones no son realmente útiles para APIs pero para construir una aplicación web, las sesiones pueden ser cruciales para mantener el estado y la información de inicio de sesión.
- official flightphp/session - Biblioteca de Sesiones oficial de Flight. Esta es una biblioteca de sesiones simple que se puede usar para almacenar y recuperar datos de sesión. Utiliza el manejo de sesiones incorporado de PHP.
- Ghostff/Session - Administrador de Sesiones PHP (sin bloqueo, flash, segmento, encriptación de sesión). Utiliza PHP open_ssl para encriptación/desencriptación opcional de datos de sesión.
Plantillas
Las plantillas son el núcleo de cualquier aplicación web con una interfaz de usuario. Hay una serie de motores de plantillas que se pueden usar con Flight.
- deprecated flightphp/core View - Este es un motor de plantillas muy básico que forma parte del núcleo. No se recomienda usarlo si tienes más de un par de páginas en tu proyecto.
- latte/latte - Latte es un motor de plantillas completo que es muy fácil de usar y se siente más cercano a una sintaxis PHP que Twig o Smarty. También es muy fácil de extender y agregar tus propios filtros y funciones.
- twig/twig - Twig es un motor de plantillas flexible, rápido y seguro (el mismo que usa Symfony). Las herramientas de IA y muchos desarrolladores PHP lo conocen bien, escapa automáticamente la salida por defecto, y tiene un enorme ecosistema de extensiones.
- knifelemon/comment-template - CommentTemplate es un potente motor de plantillas PHP con compilación de assets, herencia de plantillas y procesamiento de variables. Cuenta con minificación automática de CSS/JS, caché, codificación Base64 e integración opcional con el framework Flight PHP.
Integración con WordPress
¿Quieres usar Flight en tu proyecto WordPress? ¡Hay un plugin útil para eso!
- n0nag0n/wordpress-integration-for-flight-framework - Este plugin de WordPress te permite ejecutar Flight junto con WordPress. Es perfecto para agregar APIs personalizadas, microservicios, o incluso aplicaciones completas a tu sitio WordPress usando el framework Flight. ¡Súper útil si quieres lo mejor de ambos mundos!
Contribuyendo
¿Tienes un plugin que te gustaría compartir? ¡Envía una solicitud de extracción para agregarlo a la lista!
Media
Media
Hemos intentado rastrear lo que podemos de los diversos tipos de media en internet sobre Flight. A continuación, encontrarás diferentes recursos que puedes usar para aprender más sobre Flight.
Artículos y reseñas
- Unit Testing and SOLID Principles por Brian Fenton (2015?)
- PHP Web Framework Flight por ojambo (2025)
- Define, Generate, and Implement: An API-First Approach with OpenAPI Generator and FlightPHP por Daniel Schreiber (2025)
- Best PHP Micro Frameworks for 2024 por n0nag0n (2024)
- Creating a RESTful API with Flight Framework por n0nag0n (2024)
- Building a Simple Blog with Flight Part 2 por n0nag0n (2024)
- Building a Simple Blog with Flight Part 1 por n0nag0n (2024)
- 🚀 Build a Simple CRUD API in PHP with the Flight Framework por soheil-khaledabadi (2024)
- Building a PHP Web Application with the Flight Micro-framework por Arthur C. Codex (2023)
- Best PHP Frameworks for Web Development in 2024 por Ravikiran A S (2023)
- Top 12 PHP Frameworks: A Comprehensive Guide for 2023 por marketing kbk (2023)
- 5 PHP Frameworks You've (Probably) Never Heard of por n0nag0n (2022)
- 12 top PHP frameworks for web developers to consider in 2023 por Anna Monus (2022)
- The Best PHP Microframeworks on a Cloud Server por Shahzeb Ahmed (2021)
- PHP framework: Top 15 powerful ones for your web development por AHT Tech (2020)
- Easy PHP Routing with FlightPHP por Lucas Conceição (2019)
- Trying Out New PHP Framework (Flight) por Leon (2017)
- Setting up FlightPHP to work with Backbonejs por Timothy Tocci (2015)
Videos y tutoriales
- Build a Flight PHP App with MVC & MariaDB in 10 Minutes! (Beginner Friendly) por ojamboshop (2025)
- Create a REST API for IoT Devices Using PHP & FlightPHP - ESP32 API por IoT Craft Hub (2024)
- PHP Flight Framework Simple Introductory Video por n0nag0n (2024)
- Set header HTTP code in Flightphp (3 Solutions!!) por Roel Van de Paar (2024)
- PHP Flight Framework Tutorial. Super easy API Project! por n0nag0n (2022)
- Aplicación web CRUD con php y mysql y bootstrap usando flight por Devlopteca - Oscar Uh (2021)
- DevOps & SysAdmins: Lighttpd rewrite rule for Flight PHP microframework por Roel Van de Paar (2021)
- Tutorial REST API Flight PHP #PART2 INSERT TABLE Info #Code (Tagalog) por Info Singkat Official (2020)
- Tutorial REST API Flight PHP #PART1 Info #Code (Tagalog) por Info Singkat Official (2020)
- How To Create JSON REST API IN PHP - Part 2 por Codewife (2018)
- How To Create JSON REST API IN PHP - Part 1 por Codewife (2018)
- Teste Micro Frameworks PHP - Flight PHP, Lumen, Slim 3 e Laravel por Codemarket (2016)
- Tutorial 1 Flight PHP - Instalación por absagg (2014)
- Tutorial 2 Flight PHP - Route parte 1 por absagg (2014)
¿Falta algo?
¿Nos falta algo que escribiste o grabaste? ¡Avísanos con un issue o pull request!
Examples
¿Necesitas un inicio rápido?
Tienes dos opciones para comenzar con un nuevo proyecto de Flight:
- Full Skeleton Boilerplate: Un ejemplo más completo con controladores y vistas.
- Single File Skeleton Boilerplate: Un solo archivo que incluye todo lo que necesitas para ejecutar tu app en un archivo simple.
Ejemplos contribuidos por la comunidad:
- flightravel: FlightPHP con directorios de Laravel, con herramientas de PHP + GH Actions
- fleact - Un kit de inicio de FlightPHP con integración de ReactJS.
- flastro - Un kit de inicio de FlightPHP con integración de Astro.
- velt - Velt es una plantilla de inicio rápida y fácil de Svelte con un backend de FlightPHP.
- vite-flightphp - FlightPHP y frontend moderno (Vite + Tailwind CSS) con hot reload (recarga en caliente).
¿Necesitas algo de inspiración?
Aunque estos no están patrocinados oficialmente por el equipo de Flight, podrían darte ideas sobre cómo estructurar tus propios proyectos construidos con Flight!
- ASC REST API Spell Checker - Una API REST ligera para corrección ortográfica en árabe construida con FlightPHP y la biblioteca ArPHP. Esta API proporciona capacidades de corrección ortográfica de texto árabe, incluyendo detección de palabras mal escritas y sugerencias de corrección.
- Eventify - Eventify es una aplicación de una sola página que conecta a organizadores de eventos con asistentes. Construida con PHP (FlightPHP), JavaScript y MySQL, cuenta con autenticación JWT, gestión de eventos y documentación de API RESTful usando OpenAPI.
- Ivox Car Rental - Ivox Car Rental es una aplicación web de alquiler de autos de una sola página, amigable con dispositivos móviles, construida con PHP (FlightPHP), JavaScript y MySQL. Soporta registro de usuarios, navegación y reserva de autos, mientras que los administradores pueden gestionar autos, usuarios y reservas. La app cuenta con una API REST, autenticación JWT y un diseño responsivo para una experiencia de alquiler moderna.
- Decay - Flight v3 con HTMX y SleekDB todo sobre zombis! (Demo)
- Flight Example Blog - Flight v3 con Middleware, Controladores, Active Record y Latte.
- Flight CRUD RESTful API - Proyecto de API CRUD simple usando el framework Flight, que proporciona una estructura básica para que los nuevos usuarios configuren rápidamente una aplicación PHP con operaciones CRUD y conectividad a base de datos. El proyecto demuestra cómo usar Flight para el desarrollo de API RESTful, lo que lo convierte en una herramienta de aprendizaje ideal para principiantes y un kit de inicio útil para desarrolladores más experimentados.
- 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 - Framework de autenticación listo para producción que te ahorra semanas de desarrollo. Características de seguridad de nivel empresarial: 2FA/TOTP, integración LDAP, SSO de Azure, limitación de velocidad inteligente, huella dactilar de sesión, protección contra fuerza bruta, panel de análisis de seguridad, registro de auditoría integral y control de acceso basado en roles granular.
¿Quieres compartir tu propio ejemplo?
Si tienes un proyecto que quieres compartir, ¡por favor envía una solicitud de pull para agregarlo a esta lista!
Install/install
Instrucciones de Instalación
Hay algunos requisitos previos básicos antes de poder instalar Flight. Concretamente, necesitarás:
- Instalar PHP en tu sistema
- Instalar Composer para la mejor experiencia de desarrollo.
Instalación Básica
Si estás usando Composer, puedes ejecutar el siguiente comando:
composer require flightphp/core
Esto solo colocará los archivos principales de Flight en tu sistema. Necesitarás definir la estructura del proyecto, layout, dependencias, configuraciones, autoloading, etc. Este método asegura que no se instalen otras dependencias además de Flight.
También puedes descargar los archivos directamente y extraerlos a tu directorio web.
La instalación básica es perfecta para aprender, micro APIs y experimentos de copiar y pegar. Para una estructura de aplicación completa que los humanos y herramientas de codificación de IA puedan seguir de la misma manera, usa el esqueleto recomendado a continuación.
Instalación Recomendada
Es muy recomendable comenzar con la aplicación flightphp/skeleton para cualquier proyecto nuevo. La instalación es muy sencilla.
composer create-project flightphp/skeleton my-project/
cd my-project/
composer start
# base de datos de muestra opcional + demo de publicaciones
php runway migrate
Ese paso configura la estructura del proyecto, el autoloading PSR-4 de Composer, la configuración y herramientas como Tracy, Tracy Extensions y Runway. También incluye el AGENTS.md raíz (y copias específicas bajo app/) para que los asistentes de IA compartan una misma estructura contigo—ver IA y experiencia de desarrollador.
Lo que te ofrece el esqueleto
project-root/
├── AGENTS.md # fuente de verdad para IA / agentes
├── SECURITY.md # expectativas de seguridad
├── .env.example # secretos / overlays de despliegue (copiado a .env)
├── public/index.php # solo entrada web
├── app/
│ ├── config/ # bootstrap, rutas, servicios, config_sample.php
│ ├── Controller/ # App\Controller\* (¡carpeta PascalCase!)
│ ├── Middleware/ # App\Middleware\*
│ ├── Model/ # App\Model\* (ActiveRecord)
│ ├── Utils/ # Config, Env, DatabaseFactory
│ ├── commands/ # comandos CLI de Runway
│ ├── views/ # plantillas Twig (*.twig)
│ ├── cache/
│ └── log/
├── migrations/ # migraciones SQL (.sql / .mysql.sql)
└── tests/ # PHPUnit
Los namespaces siguen el caso de la carpeta. Composer mapea "App\\": "app/", por lo tanto:
| Ruta en disco | Namespace |
|---|---|
app/Controller/HomeController.php |
App\Controller\HomeController |
app/Middleware/… |
App\Middleware\… |
app/Model/… |
App\Model\… |
app/Utils/… |
App\Utils\… |
En Linux, app/controller/ no es lo mismo que app/Controller/. El autoloading distingue entre mayúsculas y minúsculas—coincide con las carpetas PascalCase del esqueleto. Detalles: Autoloading.
Configuración por defecto (nuevos proyectos): vistas Twig, SimplePdo + ActiveRecord, Dice con inyección de Engine (prefiere no usar Flight:: dentro de las clases de la aplicación), SQLite opcional después de php runway migrate.
create-project normalmente copia app/config/config_sample.php → config.php y .env.example → .env cuando están presentes. Las rutas viven en app/config/routes.php; los servicios y la DI viven en app/config/services.php.
Docs ↔ esqueleto: Estos documentos enseñan las APIs de Flight (a menudo con ejemplos cortos de
Flight::). El esqueleto fija la forma de la aplicación. Al agregar código bajoapp/, sigue el árbol del esqueleto; usa los docs para nombres de métodos, opciones y plugins.
Configura tu Servidor Web
Servidor de Desarrollo PHP Integrado
Esta es, con mucho, la forma más simple de poner todo en marcha. Puedes usar el servidor integrado para ejecutar tu aplicación e incluso usar SQLite como base de datos (siempre que sqlite3 esté instalado en tu sistema) y ¡no necesitar mucho más! Solo ejecuta el siguiente comando una vez que PHP esté instalado:
php -S localhost:8000
# o con la aplicación esqueleto
composer start
Luego abre tu navegador y ve a http://localhost:8000.
Si quieres que la raíz de documentos de tu proyecto sea un directorio diferente (Ej: tu proyecto está en ~/myproject, pero tu raíz de documentos es ~/myproject/public/), puedes ejecutar el siguiente comando una vez que estés en el directorio ~/myproject:
php -S localhost:8000 -t public/
# con la aplicación esqueleto, esto ya está configurado
composer start
Luego abre tu navegador y ve a http://localhost:8000.
Apache
Asegúrate de que Apache ya esté instalado en tu sistema. Si no, busca en Google cómo instalar Apache en tu sistema.
Para Apache, edita tu archivo .htaccess con lo siguiente:
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [QSA,L]
Nota: Si necesitas usar Flight en un subdirectorio, agrega la línea
RewriteBase /subdir/justo después deRewriteEngine On.
Nota: Si quieres proteger todos los archivos del servidor, como una base de datos o un archivo env. Pon esto en tu archivo
.htaccess:
RewriteEngine On
RewriteRule ^(.*)$ index.php
Nginx
Asegúrate de que Nginx ya esté instalado en tu sistema. Si no, busca en Google cómo instalar Nginx en tu sistema.
Para Nginx, agrega lo siguiente a tu declaración de servidor:
server {
location / {
try_files $uri $uri/ /index.php;
}
}
Crea tu archivo index.php
Si estás haciendo una instalación básica, necesitarás algo de código para comenzar.
<?php
// Si estás usando Composer, requiere el autoloader.
require 'vendor/autoload.php';
// si no estás usando Composer, carga el framework directamente
// require 'flight/Flight.php';
// Luego define una ruta y asigna una función para manejar la solicitud.
Flight::route('/', function () {
echo 'hello world!';
});
// Finalmente, inicia el framework.
Flight::start();
Con la aplicación esqueleto, la entrada pública solo arranca la aplicación. Las rutas se registran en app/config/routes.php (típicamente [App\Controller\…::class, 'method'] para que Dice pueda inyectar dependencias). Los servicios, Twig, SimplePdo y el contenedor se conectan en app/config/services.php. Esa estructura es intencional para que las herramientas de IA y los humanos editen los mismos lugares cada vez.
Instalando PHP
Si ya tienes php instalado en tu sistema, continúa y omite estas instrucciones y ve a la sección de descarga
macOS
Instalando PHP usando Homebrew
-
Instala Homebrew (si aún no está instalado):
- Abre Terminal y ejecuta:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
- Abre Terminal y ejecuta:
-
Instala PHP:
- Instala la última versión:
brew install php - Para instalar una versión específica, por ejemplo, PHP 8.1:
brew tap shivammathur/php brew install shivammathur/php/php@8.1
- Instala la última versión:
-
Cambia entre versiones de PHP:
- Desvincula la versión actual y vincula la versión deseada:
brew unlink php brew link --overwrite --force php@8.1 - Verifica la versión instalada:
php -v
- Desvincula la versión actual y vincula la versión deseada:
Windows 10/11
Instalando PHP manualmente
-
Descarga PHP:
- Visita PHP para Windows y descarga la última versión o una versión específica (por ejemplo, 7.4, 8.0) como un archivo zip no seguro para subprocesos.
-
Extrae PHP:
- Extrae el archivo zip descargado a
C:\php.
- Extrae el archivo zip descargado a
-
Agrega PHP al PATH del sistema:
- Ve a Propiedades del sistema > Variables de entorno.
- Bajo Variables del sistema, busca Path y haz clic en Editar.
- Agrega la ruta
C:\php(o donde hayas extraído PHP). - Haz clic en Aceptar para cerrar todas las ventanas.
-
Configura PHP:
- Copia
php.ini-developmentaphp.ini. - Edita
php.inipara configurar PHP según sea necesario (por ejemplo, estableciendoextension_dir, habilitando extensiones).
- Copia
-
Verifica la instalación de PHP:
- Abre el Símbolo del sistema y ejecuta:
php -v
- Abre el Símbolo del sistema y ejecuta:
Instalando Múltiples Versiones de PHP
-
Repite los pasos anteriores para cada versión, colocando cada una en un directorio separado (por ejemplo,
C:\php7,C:\php8). -
Cambia entre versiones ajustando la variable PATH del sistema para que apunte al directorio de la versión deseada.
Ubuntu (20.04, 22.04, etc.)
Instalando PHP usando apt
-
Actualiza las listas de paquetes:
- Abre Terminal y ejecuta:
sudo apt update
- Abre Terminal y ejecuta:
-
Instala PHP:
- Instala la última versión de PHP:
sudo apt install php - Para instalar una versión específica, por ejemplo, PHP 8.1:
sudo apt install php8.1
- Instala la última versión de PHP:
-
Instala módulos adicionales (opcional):
- Por ejemplo, para instalar soporte para MySQL:
sudo apt install php8.1-mysql
- Por ejemplo, para instalar soporte para MySQL:
-
Cambia entre versiones de PHP:
- Usa
update-alternatives:sudo update-alternatives --set php /usr/bin/php8.1
- Usa
-
Verifica la versión instalada:
- Ejecuta:
php -v
- Ejecuta:
Rocky Linux
Instalando PHP usando yum/dnf
-
Habilita el repositorio EPEL:
- Abre Terminal y ejecuta:
sudo dnf install epel-release
- Abre Terminal y ejecuta:
-
Instala el repositorio de Remi:
- Ejecuta:
sudo dnf install https://rpms.remirepo.net/enterprise/remi-release-8.rpm sudo dnf module reset php
- Ejecuta:
-
Instala PHP:
- Para instalar la versión predeterminada:
sudo dnf install php - Para instalar una versión específica, por ejemplo, PHP 7.4:
sudo dnf module install php:remi-7.4
- Para instalar la versión predeterminada:
-
Cambia entre versiones de PHP:
- Usa el comando de módulo
dnf:sudo dnf module reset php sudo dnf module enable php:remi-8.0 sudo dnf install php
- Usa el comando de módulo
-
Verifica la versión instalada:
- Ejecuta:
php -v
- Ejecuta:
Notas Generales
- Para entornos de desarrollo, es importante configurar los ajustes de PHP según los requisitos de tu proyecto.
- Al cambiar las versiones de PHP, asegúrate de que todas las extensiones de PHP relevantes estén instaladas para la versión específica que planeas usar.
- Reinicia tu servidor web (Apache, Nginx, etc.) después de cambiar las versiones de PHP o actualizar configuraciones para aplicar los cambios.
Guides
Guías
Flight PHP está diseñado para ser simple pero poderoso, y nuestras guías te ayudarán a construir aplicaciones del mundo real paso a paso. Estos tutoriales prácticos te guían a través de proyectos completos para demostrar cómo Flight puede usarse de manera efectiva.
Guías Oficiales
Construyendo un Blog
Aprende cómo crear una aplicación de blog funcional con Flight PHP. Esta guía te guía a través de:
- Configurando una estructura de proyecto
- Trabajando con plantillas usando Latte
- Implementando rutas para publicaciones
- Almacenando y recuperando datos
- Manejo de envíos de formularios
- Manejo básico de errores
Este tutorial es perfecto para principiantes que quieran ver cómo todas las piezas encajan en una aplicación real.
Pruebas Unitarias y Principios SOLID
Esta guía cubre los fundamentos de las pruebas unitarias en aplicaciones Flight PHP. Incluye:
- Configurando PHPUnit
- Escribiendo código probables usando principios SOLID
- Burlando dependencias
- Fosas comunes para evitar
- Escalando tus pruebas a medida que crece tu aplicación Este tutorial es ideal para desarrolladores que busquen mejorar la calidad y mantenibilidad del código.
Guías No Oficiales
Aunque estas guías no son mantenidas oficialmente por el equipo de Flight, son recursos valiosos creados por la comunidad. Cubren varios temas y casos de uso, proporcionando insights adicionales sobre el uso de Flight PHP.
Creating a RESTful API with Flight Framework
Esta guía te guía a través de la creación de una API RESTful usando el framework Flight PHP. Cubre los conceptos básicos de configurar una API, definir rutas y devolver respuestas JSON.
Building a Simple Blog
Esta guía te guía a través de la creación de un blog básico usando el framework Flight PHP. De hecho, tiene 2 partes: una para cubrir los conceptos básicos y la otra para cubrir temas más avanzados y refinamientos para un blog listo para producción.
- Building a Simple Blog with Flight - Part 1 - Comenzando con un blog simple.
- Building a Simple Blog with Flight - Part 2 - Refinando el blog para producción.
Building a Pokémon API in PHP: A Beginner's Guide
Esta guía divertida te guía a través de la creación de una simple API de Pokémon usando Flight PHP. Cubre los conceptos básicos de configurar una API, definir rutas y devolver respuestas JSON.
Contribuyendo
¿Tienes una idea para una guía? ¿Encontraste un error? ¡Bienvenidas las contribuciones! Nuestras guías se mantienen en el repositorio de documentación de FlightPHP.
Si has construido algo interesante con Flight y quieres compartirlo como una guía, por favor envía una solicitud de extracción. Compartir tu conocimiento ayuda a que la comunidad de Flight crezca.
¿Buscando Documentación de API?
Si estás buscando información específica sobre las características y métodos principales de Flight, consulta la sección Aprender de nuestra documentación.