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

Cons comparado con Flight

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 adaptarlos a las necesidades de su aplicación. Algunos se establecen por defecto, pero puede anularlos según sea necesario. También puede establecer sus propias variables para usarlas en toda su aplicación.

Comprensión

Puede personalizar ciertos comportamientos de Flight estableciendo valores de configuración a través del método set.

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

En el archivo app/config/config.php, puede ver todas las variables de configuración predeterminadas disponibles para usted.

Uso Básico

Opciones de Configuración de Flight

A continuación se presenta una lista de todas las configuraciones disponibles:

Configuración del Cargador

Adicionalmente, hay otra configuración para el cargador. Esto le permitirá cargar clases automáticamente con _ en el nombre de la clase.

// Habilitar la carga de clases con guiones bajos
// Predeterminado a true
Loader::$v2ClassLoading = false;

Variables

Flight le permite guardar variables para que puedan usarse en cualquier lugar de su aplicación.

// Guardar su variable
Flight::set('id', 123);

// En otro lugar de su aplicación
$id = Flight::get('id');

Para ver si se ha establecido una variable, puede hacer:

if (Flight::has('id')) {
  // Hacer algo
}

Puede eliminar una variable haciendo:

// Elimina la variable id
Flight::clear('id');

// Elimina todas las variables
Flight::clear();

Nota: Solo porque puede establecer una variable no significa que deba hacerlo. Use 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 lugar de su aplicación, lo que hace difícil rastrear errores. Además, esto puede complicar cosas como pruebas unitarias.

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 algo de información de error.

Puede anular este comportamiento para sus propias necesidades:

Flight::map('error', function (Throwable $error) {
  // Manejar error
  echo $error->getTraceAsString();
});

Por defecto, los errores no se registran en el servidor web. Puede habilitar esto cambiando la configuración:

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

404 No Encontrado

Cuando no se puede encontrar una URL, Flight llama al método notFound. El comportamiento predeterminado es enviar una respuesta HTTP 404 Not Found con un mensaje simple.

Puede anular este comportamiento para sus propias necesidades:

Flight::map('notFound', function () {
  // Manejar no encontrado
});

Ver También

Solución de Problemas

Registro de Cambios

Learn/ai

IA y Experiencia del Desarrollador con Flight

Resumen

Flight facilita potenciar tus proyectos PHP con herramientas impulsadas por IA y flujos de trabajo modernos para desarrolladores. Con comandos integrados para conectar con proveedores de LLM (Modelo de Lenguaje Grande) y generar instrucciones de codificación específicas del proyecto impulsadas por IA, Flight te ayuda a ti y a tu equipo a sacar el máximo provecho de asistentes de IA como GitHub Copilot, Cursor y Windsurf.

Comprensión

Los asistentes de codificación con IA son más útiles cuando entienden el contexto, las convenciones y los objetivos de tu proyecto. Los ayudantes de IA de Flight te permiten:

Estas funciones están integradas en el CLI principal de Flight y en el proyecto inicial oficial flightphp/skeleton.

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 con un proveedor de LLM.

php runway ai:init

Se te pedirá que:

Esto crea un archivo .runway-creds.json en la raíz de tu proyecto (y asegura que esté en tu .gitignore).

Ejemplo:

¡Bienvenido a AI Init!
¿Cuál API de LLM quieres usar? [1] openai, [2] grok, [3] claude: 1
Ingresa la URL base para la API de LLM [https://api.openai.com]:
Ingresa tu clave API para openai: sk-...
Ingresa el nombre del modelo que quieres usar (p.ej. gpt-4, claude-3-opus, etc) [gpt-4o]:
Credenciales guardadas en .runway-creds.json

Generación de Instrucciones de IA Específicas del Proyecto

El comando ai:generate-instructions te ayuda a crear o actualizar instrucciones para asistentes de codificación con IA, adaptadas a tu proyecto.

php runway ai:generate-instructions

Responderás a unas pocas preguntas sobre tu proyecto (descripción, base de datos, plantillas, seguridad, tamaño del equipo, etc.). Flight usa tu proveedor de LLM para generar instrucciones y luego las escribe en:

Ejemplo:

Por favor describe para qué es tu proyecto? Mi increíble API
¿Qué base de datos planeas usar? MySQL
¿Qué motor de plantillas HTML planeas usar (si aplica)? latte
¿Es la seguridad un elemento importante de este proyecto? (y/n) y
...
Instrucciones de IA actualizadas exitosamente.

Ahora, tus herramientas de IA darán sugerencias más inteligentes y relevantes basadas en las necesidades reales de tu proyecto.

Uso Avanzado

Ver También

Solución de Problemas

Registro de Cambios

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:

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:

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 asunto importante cuando se trata de aplicaciones web. Quieres asegurarte de que tu aplicación sea segura y de que los datos de tus usuarios estén seguros. Flight proporciona una serie de funciones para ayudarte a asegurar tus aplicaciones web.

Comprensión

Hay una serie de amenazas de seguridad comunes de las que debes estar al tanto al construir aplicaciones web. Algunas de las amenazas más comunes incluyen:

Templates ayudan con XSS escapando la salida por defecto para que no tengas que recordarlo. Sessions puede ayudar con CSRF almacenando un token CSRF en la sesión del usuario como se describe a continuación. Usar declaraciones preparadas con PDO puede ayudar a prevenir ataques de inyección SQL (o usar métodos útiles en la clase PdoWrapper). CORS puede manejarse con un gancho simple antes de que se llame Flight::start().

Todos estos métodos trabajan juntos para ayudar a mantener tus aplicaciones web seguras. Siempre debe estar a la vanguardia de tu mente aprender y entender las mejores prácticas de seguridad.

Uso Básico

Encabezados

Los encabezados HTTP son una de las formas más fáciles de asegurar tus aplicaciones web. Puedes usar encabezados para prevenir clickjacking, XSS y otros ataques. Hay varias formas en que puedes 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 estén funcionando con esos dos sitios web.

Agregar Manualmente

Puedes agregar estos encabezados manualmente usando el método header en el objeto Flight\Response.

// Establecer el encabezado X-Frame-Options para prevenir clickjacking
Flight::response()->header('X-Frame-Options', 'SAMEORIGIN');

// Establecer el encabezado Content-Security-Policy para prevenir XSS
// Nota: este encabezado puede volverse muy complejo, así que querrás
//  consultar ejemplos en internet para tu aplicación
Flight::response()->header("Content-Security-Policy", "default-src 'self'");

// Establecer el encabezado X-XSS-Protection para prevenir XSS
Flight::response()->header('X-XSS-Protection', '1; mode=block');

// Establecer el encabezado X-Content-Type-Options para prevenir MIME sniffing
Flight::response()->header('X-Content-Type-Options', 'nosniff');

// Establecer el encabezado Referrer-Policy para controlar cuánta información de referrer se envía
Flight::response()->header('Referrer-Policy', 'no-referrer-when-downgrade');

// Establecer el encabezado Strict-Transport-Security para forzar HTTPS
Flight::response()->header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');

// Establecer el encabezado Permissions-Policy para controlar qué funciones y APIs pueden usarse
Flight::response()->header('Permissions-Policy', 'geolocation=()');

Estos pueden agregarse al inicio de tus archivos routes.php o index.php.

Agregar como un Filtro

También puedes agregarlos en un filtro/gancho como el siguiente:

// Agregar 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 un Middleware

También puedes agregarlos como una clase middleware que proporciona la mayor flexibilidad para qué rutas aplicar esto. En general, estos encabezados deben aplicarse a todas las respuestas HTML y API.

// app/middlewares/SecurityHeadersMiddleware.php

namespace app\middlewares;

use flight\Engine;

class SecurityHeadersMiddleware
{
    protected Engine $app;

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

    public function before(array $params): void
    {
        $response = $this->app->response();
        $response->header('X-Frame-Options', 'SAMEORIGIN');
        $response->header("Content-Security-Policy", "default-src 'self'");
        $response->header('X-XSS-Protection', '1; mode=block');
        $response->header('X-Content-Type-Options', 'nosniff');
        $response->header('Referrer-Policy', 'no-referrer-when-downgrade');
        $response->header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
        $response->header('Permissions-Policy', 'geolocation=()');
    }
}

// index.php o donde tengas tus rutas
// FYI, este grupo de cadena vacía actúa como un middleware global para
// todas las rutas. Por supuesto, podrías hacer lo mismo y solo agregar
// esto a rutas específicas.
Flight::group('', function(Router $router) {
    $router->get('/users', [ 'UserController', 'getUsers' ]);
    // más rutas
}, [ SecurityHeadersMiddleware::class ]);

Cross Site Request Forgery (CSRF)

Cross Site Request Forgery (CSRF) es un tipo de ataque donde 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 incorporado, 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 el formulario se envíe. Usaremos el plugin flightphp/session para manejar sesiones.

// Generar un token CSRF y almacenarlo 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 sesión para más información
Flight::register('session', flight\Session::class);

// Solo necesitas generar un solo token por sesión (para que funcione 
// a través de 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 Flight Predeterminada
<!-- Usar 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 Latte

También puedes establecer 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...

    // Establecer 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/middlewares/CsrfMiddleware.php

namespace app\middleware;

use flight\Engine;

class CsrfMiddleware
{
    protected Engine $app;

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

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

// index.php o donde tengas tus rutas
use app\middlewares\CsrfMiddleware;

Flight::group('', function(Router $router) {
    $router->get('/users', [ 'UserController', 'getUsers' ]);
    // más rutas
}, [ CsrfMiddleware::class ]);
Filtros de Eventos
// Este middleware verifica si la solicitud es una solicitud POST y si lo es, verifica si el token CSRF es válido
Flight::before('start', function() {
    if(Flight::request()->method == 'POST') {

        // capturar 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 donde 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 confíes en la salida de tus usuarios! Siempre asume que todos ellos 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 otro motor de plantillas como Latte, puedes escapar fácilmente la salida para prevenir ataques XSS.

// Supongamos que el usuario es astuto e intenta usar esto como su nombre
$name = '<script>alert("XSS")</script>';

// Esto escapará la salida
Flight::view()->set('name', $name);
// Esto mostrará: &lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;

// Si usas algo como Latte registrado como tu clase de vista, también escapará esto automáticamente.
Flight::view()->render('template', ['name' => $name]);

SQL Injection

SQL Injection es un tipo de ataque donde 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 tu base de datos. De nuevo, ¡nunca confíes en la entrada de tus usuarios! Siempre asume que están buscando sangre. Puedes usar declaraciones preparadas en tus objetos PDO para prevenir inyección SQL.

// Asumiendo que tienes Flight::db() registrado como tu objeto PDO
$statement = Flight::db()->prepare('SELECT * FROM users WHERE username = :username');
$statement->execute([':username' => $username]);
$users = $statement->fetchAll();

// Si usas la clase PdoWrapper, esto puede hacerse fácilmente en una línea
$users = Flight::db()->fetchAll('SELECT * FROM users WHERE username = :username', [ 'username' => $username ]);

// Puedes hacer lo mismo con un objeto PDO con marcadores de posición ?
$statement = Flight::db()->fetchAll('SELECT * FROM users WHERE username = ?', [ $username ]);

Ejemplo Inseguro

A continuación se explica por qué usamos declaraciones SQL preparadas para protegernos de ejemplos inocentes como el de abajo:

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

// Se ve 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 nombre de usuario único

CORS

Cross-Origin Resource Sharing (CORS) es un mecanismo que permite que muchos recursos (p. ej., fuentes, JavaScript, etc.) en una página web se soliciten desde otro dominio fuera del dominio desde el cual se originó el recurso. Flight no tiene funcionalidad incorporada, pero esto puede manejarse fácilmente con un gancho para ejecutar antes de que se llame el método Flight::start().

// app/utils/CorsUtil.php

namespace app\utils;

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

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

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

    private function allowOrigins(): void
    {
        // personaliza tus hosts permitidos aquí.
        $allowed = [
            'capacitor://localhost',
            'ionic://localhost',
            'http://localhost',
            'http://localhost:4200',
            'http://localhost:8080',
            'http://localhost:8100',
        ];

        $request = Flight::request();

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

// index.php o donde tengas tus rutas
$CorsUtil = new CorsUtil();

// Esto necesita ejecutarse antes de que start se ejecute.
Flight::before('start', [ $CorsUtil, 'setupCors' ]);

Manejo de Errores

Oculta detalles de errores sensibles en producción para evitar filtrar información a atacantes. En producción, registra errores en lugar de mostrarlos con display_errors establecido en 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); // Deshabilitar la visualización de errores
    ini_set('log_errors', 1);     // Registrar 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');

Sanitización de Entrada

Nunca confíes en la entrada del usuario. Sánitala usando filter_var antes de procesarla para prevenir que datos maliciosos se cuelen.


// Supongamos una solicitud $_POST con $_POST['input'] y $_POST['email']

// Sanitizar una entrada de cadena
$clean_input = filter_var(Flight::request()->data->input, FILTER_SANITIZE_STRING);
// Sanitizar un email
$clean_email = filter_var(Flight::request()->data->email, FILTER_SANITIZE_EMAIL);

Hash de Contraseñas

Almacena contraseñas de manera segura y verifícalas de forma segura usando las funciones integradas de PHP como password_hash y password_verify. Las contraseñas nunca deben almacenarse en texto plano, ni deben encriptarse con métodos reversibles. El hashing asegura que incluso si tu base de datos es comprometida, las contraseñas reales permanezcan protegidas.

$password = Flight::request()->data->password;
// Hashear una contraseña al almacenar (p. ej., durante el registro)
$hashed_password = password_hash($password, PASSWORD_DEFAULT);

// Verificar una contraseña (p. ej., durante el inicio de sesión)
if (password_verify($password, $stored_hash)) {
    // La contraseña coincide
}

Limitación de Tasa

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); // Reiniciar después de 60 segundos
});

Ver También

Solución de Problemas

Registro de Cambios

Learn/routing

Enrutamiento

Resumen

El enrutamiento en Flight PHP mapea patrones de URL a funciones de devolución de llamada o métodos de clase, permitiendo un manejo rápido y simple de solicitudes. Está diseñado para un overhead mínimo, un uso amigable para principiantes y extensibilidad sin dependencias externas.

Comprensión

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 a través de funciones, métodos de clase o acciones de controladores. El sistema de enrutamiento de Flight es flexible, soporta patrones básicos, parámetros con nombre, expresiones regulares y características avanzadas como inyección de dependencias y enrutamiento con recursos. Este enfoque mantiene tu código organizado y fácil de mantener, mientras permanece rápido y simple para principiantes y extensible para usuarios avanzados.

Nota: ¿Quieres entender más sobre enrutamiento? Consulta la página "¿por qué un framework?" para una explicación más detallada.

Uso Básico

Definiendo una Ruta Simple

El enrutamiento básico en Flight se realiza coincidiendo un patrón de URL con una función de devolución de llamada o un array de una clase y un método.

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

Las rutas se coinciden 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. Así que puedes usar una función regular:

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

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

Usando Clases y Métodos como un 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: Por defecto, cuando un controlador es llamado dentro del framework, la clase flight\Engine siempre se inyecta a menos que especifiques a través de un contenedor de inyección de dependencias

Enrutamiento Específico de Método

Por defecto, los patrones de ruta se coinciden contra 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 ese es un método 
//    para obtener variables, no para crear una ruta.
Flight::post('/', function() { /* code */ });
Flight::patch('/', function() { /* code */ });
Flight::put('/', function() { /* code */ });
Flight::delete('/', function() { /* code */ });

También puedes mapear 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 un manejo integrado para solicitudes HTTP HEAD y OPTIONS:

Solicitudes HEAD

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.

// Para una ruta definida como:
Flight::route('GET|POST /users', function() { /* ... */ });

// Una solicitud OPTIONS a /users responderá con:
//
// Status: 204 No Content
// Allow: GET, POST, HEAD, OPTIONS

Usando el Objeto Router

Además, puedes obtener el objeto Router que tiene algunos métodos auxiliares para usar:


$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() { /* code */});
$router->put('/users/update/@id',   function() { /* code */});
$router->delete('/users/@id',       function() { /* code */});
$router->patch('/users/@id',        function() { /* code */});

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 con nombre, o parámetros con nombre con expresiones regulares, ya que son más legibles y fáciles de mantener.

Parámetros con Nombre

Puedes especificar parámetros con nombre en tus rutas que se pasarán a tu función de devolución de llamada. Esto es más para la legibilidad de la ruta que para cualquier otra cosa. Por favor, ve la sección a continuación sobre una 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 con nombre 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: La coincidencia de grupos regex () con parámetros posicionales no está soportada. Ej: :'\(

Advertencia Importante

Aunque en el ejemplo anterior, parece que @name está directamente ligado a la variable $name, no lo está. El orden de los parámetros en la función de devolución de llamada es lo que determina qué se pasa a ella. Si cambiaras el orden de los parámetros en la función de devolución de llamada, las variables también se cambiarían. Aquí hay 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)!. ¡Por favor ten cuidado cuando configures tus rutas y tus funciones de devolución de llamada!

Parámetros Opcionales

Puedes especificar parámetros con nombre que sean opcionales para la coincidencia envolviendo segmentos en 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 Comodín

La coincidencia solo se realiza en segmentos individuales de URL. Si quieres coincidir 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 () {
  // Haz algo
});

Manejador de 404 No Encontrado

Por defecto, si una URL no se encuentra, Flight enviará una respuesta HTTP 404 Not Found que es muy simple y plana. Si quieres 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>My Custom 404 Not Found</h1>
        <h3>The page you have requested {$url} could not be found.</h3>
        HTML;

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

Manejador de Método No Encontrado

Por defecto, si una URL se encuentra pero el método no está permitido, Flight enviará una respuesta HTTP 405 Method Not Allowed que es 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 quieres 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>My Custom 405 Method Not Allowed</h1>
        <h3>The method you have requested for {$url} is not allowed.</h3>
        <p>Allowed Methods are: {$methods}</p>
        HTML;

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

Uso Avanzado

Inyección de Dependencias en Rutas

Si quieres usar inyección de dependencias a través de un contenedor (PSR-11, PHP-DI, Dice, etc.), el único tipo de rutas donde eso 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 más información.

Aquí hay un ejemplo rápido:


use flight\database\PdoWrapper;

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

    public function hello(int $id) {
        // haz algo con $this->pdoWrapper
        $name = $this->pdoWrapper->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
// Ve 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('flight\database\PdoWrapper', [
    '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 de costumbre
Flight::route('/hello/@id', [ 'Greeting', 'hello' ]);
// o
Flight::route('/hello/@id', 'Greeting->hello');
// o
Flight::route('/hello/@id', 'Greeting::hello');

Flight::start();

Pasando la Ejecución a la Siguiente Ruta

Obsoleto Puedes pasar la ejecución a la siguiente ruta coincidente devolviendo true desde tu función de devolución de llamada.

Flight::route('/user/@name', function (string $name) {
  // Verifica alguna condición
  if ($name !== "Bob") {
    // Continúa 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 Ruta

Al asignar un alias a una ruta, puedes llamar más tarde ese alias en tu aplicación dinámicamente para que se genere después en tu código (ej: un enlace en una plantilla HTML, o generando 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 tarde 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 cambia. En el ejemplo anterior, supongamos que users se movió a /admin/users/@id en su lugar. Con el alias en su lugar para la ruta, ya no necesitas encontrar 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 ruta todavía funciona en grupos también:

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 Ruta

Si quieres inspeccionar la información de la ruta coincidente, hay 2 formas de hacerlo:

  1. Puedes usar una propiedad executedRoute en el objeto Flight::router().
  2. Puedes solicitar que el objeto ruta se pase a tu devolución de llamada pasando true como el tercer parámetro en el método ruta. El objeto 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;
  // Haz algo con $route
  // Array de métodos HTTP coincidentes
  $route->methods;

  // Array de parámetros con nombre
  $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 la necesitas
  $route->pattern;

  // Muestra qué middleware está asignado a esto
  $route->middleware;

  // Muestra el alias asignado a esta ruta
  $route->alias;
});

Nota: La propiedad executedRoute solo se establecerá después de que una ruta haya sido ejecutada. Si intentas acceder a ella antes de que una ruta haya sido ejecutada, será NULL. También puedes usar executedRoute en middleware ¡también!

Pasar true a la definición de ruta

Flight::route('/', function(\flight\net\Route $route) {
  // Array de métodos HTTP coincidentes
  $route->methods;

  // Array de parámetros con nombre
  $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 la necesitas
  $route->pattern;

  // Muestra qué middleware está asignado a esto
  $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 veces cuando quieras agrupar rutas relacionadas juntas (como /api/v1). Puedes hacerlo 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! Ver contexto de 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! Ver contexto de 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 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 de 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 quieres usar una instancia

Ver más detalles en la página de middleware de grupo.

Enrutamiento con 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 runway ejecutando php runway routes.

Personalizando Rutas de Recursos

Hay algunas opciones para configurar las rutas de recursos.

Alias Base

Puedes configurar el aliasBase. Por defecto, 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 quieres cambiar el alias, establece el aliasBase al valor que quieras.

Flight::resource('/users', UsersController::class, [ 'aliasBase' => 'user' ]);
Solo y Excepto

También puedes especificar qué rutas quieres crear usando las opciones only y except.

// Lista blanca solo estos métodos y lista negra el resto
Flight::resource('/users', UsersController::class, [ 'only' => [ 'index', 'show' ] ]);
// Lista negra solo estos métodos y lista blanca el resto
Flight::resource('/users', UsersController::class, [ 'except' => [ 'create', 'store', 'edit', 'update', 'destroy' ] ]);

Estas son básicamente opciones de lista blanca y lista negra para que puedas especificar qué rutas quieres crear.

Middleware

También puedes especificar middleware para ejecutarse en cada una de las rutas creadas por el método resource.

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

Respuestas en 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 un poco diferente a una ruta regular.

Nota: Las respuestas en streaming solo están disponibles si tienes flight.v2.output_buffering establecido en false.

Streaming 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 a mano antes de que salgas algo al cliente. Esto se hace con la función php header() o el método Flight::response()->setRealHeader().

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

    $response = Flight::response();

    // obviamente sanitizarías la ruta y demás.
    $fileNameSafe = basename($filename);

    // Si tienes encabezados adicionales para establecer aquí después de que la ruta se haya ejecutado
    // debes definirlos antes de que nada se haga eco.
    // Deben ser todos una llamada cruda 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();

Streaming 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 que quieras aquí
    // solo debes usar header() o Flight::response()->setRealHeader()

    // sin embargo, como 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 requerido para enviar los datos al cliente
        ob_flush();
    }
    echo '}';

// Esta es la forma en que establecerás los encabezados antes de comenzar a transmitir.
})->streamWithHeaders([
    'Content-Type' => 'application/json',
    'Content-Disposition' => 'attachment; filename="users.json"',
    // código de estado opcional, por defecto 200
    'status' => 200
]);

Ver También

Solución de Problemas

404 No Encontrado o Comportamiento de Ruta Inesperado

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 devuelves un valor en tu punto final de ruta en lugar de solo hacer eco de él. La razón para 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 quieres
Flight::route('/hello', function(){
    echo 'Hello World';
});

La razón para esto es por un mecanismo especial incorporado en el router que maneja la salida de retorno como una señal para "ir a la siguiente ruta". Puedes ver el comportamiento documentado en la sección de Enrutamiento.

Registro de Cambios

Learn/learn

Aprende Sobre Flight

Flight es un framework rápido, simple y extensible para PHP. Es bastante versátil y se puede usar para construir cualquier tipo de aplicación web. Está construido con la simplicidad en mente y está escrito de una manera que es fácil de entender y usar.

Nota: Verás ejemplos que usan Flight:: como una variable estática y algunos que usan el objeto Engine $app->. Ambos funcionan de manera intercambiable con el otro. $app y $this->app en un controlador/middleware es el enfoque recomendado por el equipo de Flight.

Componentes Principales

Enrutamiento

Aprende cómo gestionar rutas para tu aplicación web. Esto también incluye agrupar rutas, parámetros de ruta y middleware.

Middleware

Aprende cómo usar middleware para filtrar solicitudes y respuestas en tu aplicación.

Carga Automática

Aprende cómo cargar automáticamente tus propias clases en tu aplicación.

Solicitudes

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 usar el motor de vistas incorporado para renderizar tus plantillas HTML.

Seguridad

Aprende cómo asegurar tu aplicación contra amenazas de seguridad comunes.

Configuración

Aprende cómo configurar el framework para tu aplicación.

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

Ganchos de Métodos y Filtrado

Aprende cómo agregar ganchos de eventos a tus métodos y 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

Colecciones

Las colecciones se usan para contener datos y ser accesibles como un arreglo o como un objeto para facilitar su uso.

Envoltorio JSON

Esto tiene unas pocas funciones simples para hacer que la codificación y decodificación de tu JSON sea consistente.

Envoltorio PDO

PDO a veces puede causar más dolores de cabeza de los necesarios. Esta clase envolvente simple puede hacer que sea significativamente más fácil interactuar con tu base de datos.

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 una buena idea entender los beneficios de usar un framework antes de comenzar a usar uno.

Además, se ha creado un excelente tutorial 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 los dos.

Otros Temas

Pruebas Unitarias

Sigue esta guía para aprender cómo realizar pruebas unitarias en tu código de Flight para que sea sólido como una roca.

IA y Experiencia del Desarrollador

Aprende cómo Flight funciona con herramientas de IA y flujos de trabajo modernos de desarrolladores para ayudarte a codificar más rápido e inteligente.

Migrando v2 -> 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.

Learn/unit_testing

Pruebas Unitarias

Resumen

Las pruebas unitarias en Flight te ayudan a asegurar que tu aplicación se comporte como se espera, detecta errores tempranamente y hace que tu código sea más fácil de mantener. Flight está diseñado para trabajar sin problemas con PHPUnit, el framework de pruebas para PHP más popular.

Comprensión

Las pruebas unitarias verifican el comportamiento de pequeñas piezas de tu aplicación (como controladores o servicios) de manera aislada. En Flight, esto significa probar cómo responden tus rutas, controladores y lógica a diferentes entradas—sin depender de estados globales o servicios externos reales.

Principios clave:

Uso Básico

Configuración de PHPUnit

  1. Instala PHPUnit con Composer:
    composer require --dev phpunit/phpunit
  2. Crea un directorio tests en la raíz de tu proyecto.
  3. Agrega un script de prueba a tu composer.json:
    "scripts": {
       "test": "phpunit --configuration phpunit.xml"
    }
  4. 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 Manipulador de Ruta Simple

Supongamos 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:

Usando Inyección de Dependencias para Controladores Probables

Inyecta dependencias (como la base de datos o el remitente de correo) en tus controladores para hacerlos fáciles de simular en pruebas:

use flight\database\PdoWrapper;

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

Y una prueba con simulaciones:

use PHPUnit\Framework\TestCase;

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

Uso Avanzado

Ver También

Solución de Problemas

Registro de Cambios

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

Contras en comparación con Vuelo

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

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 aplicaciones (APM) para casos de uso avanzados.

Entendiendo

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 aplicación 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

Solución de Problemas

Registro de Cambios

Learn/dependency_injection_container

Contenedor de Inyección de Dependencias

Resumen

El Contenedor de Inyección de Dependencias (DIC) es una potente mejora que te permite gestionar las dependencias de tu aplicación.

Comprensión

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 permitirte crear y gestionar tus clases en un lugar centralizado. Esto es útil cuando necesitas pasar el mismo objeto a múltiples clases (como tus controladores o middleware, por ejemplo).

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 crece, encontrará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 cambió
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());
    }
}

// crear un nuevo contenedor
$container = new \Dice\Dice;

// agregar una regla para decirle al contenedor cómo crear un objeto PDO
// ¡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' ]
]);

// 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 ocurre 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 a continuación 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 simulado y pasarlo a tu clase. ¡Esto es un gran beneficio cuando estás escribiendo pruebas para tu aplicación!

Creando un manejador DIC centralizado

Puedes crear un manejador DIC centralizado en tu archivo de servicios extendiendo tu app. Aquí hay un ejemplo:

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

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í hay 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);
    // ¡imprimirá esto correctamente!
  }
}

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

Flight::start();

Uso Avanzado de flightphp/container

También puedes resolver dependencias de manera recursiva. Aquí hay 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 quieres usar que no es PSR-11 (Dice). Consulta la sección de uso básico para saber cómo hacerlo.

Además, hay algunos valores predeterminados útiles que facilitarán tu vida al usar Flight.

Instancia de Engine

Si estás usando la instancia Engine en tus controladores/middleware, aquí está cómo la configurarías:


// En algún lugar de tu archivo de bootstrap
$engine = Flight::app();

$container = new \Dice\Dice;
$container = $container->addRule('*', [
    'substitutions' => [
        // Aquí es donde pasas la instancia
        Engine::class => $engine
    ]
]);

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

// Ahora puedes usar la instancia de Engine en tus controladores/middleware

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

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

Agregando Otras Clases

Si tienes otras clases que quieres agregar al contenedor, con Dice es fácil ya que serán resueltas automáticamente por el contenedor. Aquí hay 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í hay un ejemplo usando el contenedor PSR-11 de League:


require 'vendor/autoload.php';

// misma clase UserController que arriba

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

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

Flight::start();

Esto puede ser un poco más verboso que el ejemplo anterior de Dice, ¡aún así cumple el trabajo con los mismos beneficios!

Ver También

Solución de Problemas

Registro de Cambios

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 comportamiento after() 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\Engine en 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:

  1. Puedes devolver false desde la función de middleware y Flight devolverá automáticamente un error 403 Forbidden, pero sin personalización.
  2. Puedes redirigir al usuario a una página de inicio de sesión usando Flight::redirect().
  3. 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

Solución de Problemas

Registro de Cambios

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 map y register no 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

Registro de cambios

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:

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 encabezado Accept definido, el método retornará el primer tipo en el array $availableTypes.

Ver también

Solución de problemas

Registro de cambios

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:

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í:

¿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:

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/json con la respuesta. También usará las banderas JSON_THROW_ON_ERROR y JSON_UNESCAPED_SLASHES al 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 usar exit o return después de llamar a Flight::stop() para prevenir la ejecución adicional, pero en general se recomienda usar Flight::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

Solución de problemas

Registro de cambios

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:

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:

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

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

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

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?

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

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

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

Mejor Práctica para Flight

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

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

Ver También

Solución de Problemas

Registro de Cambios

Learn/templates

Vistas y Plantillas HTML

Resumen

Flight proporciona funcionalidad básica de plantillas HTML por defecto. El templado es una forma muy efectiva para que desconectes la lógica de tu aplicación de la capa de presentación.

Comprensión

Cuando estás construyendo una aplicación, probablemente tendrás HTML que querrás entregar de vuelta al usuario final. PHP por sí solo es un lenguaje de plantillas, pero es muy fácil envolver lógica de negocio como llamadas a bases de datos, llamadas a API, etc., en tu archivo HTML y hacer que las pruebas y el desacoplamiento sean un proceso muy difícil. Al empujar datos a una plantilla y dejar que la plantilla se renderice a sí misma, se vuelve mucho más fácil desacoplar y realizar pruebas unitarias en tu código. ¡Nos lo agradecerás si usas plantillas!

Uso Básico

Flight te permite intercambiar el motor de vistas predeterminado simplemente registrando tu propia clase de vista. ¡Desplázate hacia abajo para ver ejemplos de cómo usar Smarty, Latte, Blade y más!

Latte

recomendado

Aquí te explico cómo usar el motor de plantillas Latte para tus vistas.

Instalación

composer require latte/latte

Configuración Básica

La idea principal es que sobrescribas 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;

    // Dónde Latte almacena específicamente su caché
    $latte->setTempDirectory(__DIR__ . '/../cache/');

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

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

Usar Latte en Flight

Ahora que puedes renderizar con Latte, puedes hacer algo como esto:

<!-- app/views/home.latte -->
<html>
  <head>
    <title>{$title ? $title . ' - '}Mi App</title>
    <link rel="stylesheet" href="style.css">
  </head>
  <body>
    <h1>¡Hola, {$name}!</h1>
  </body>
</html>
// routes.php
Flight::route('/@name', function ($name) {
    Flight::render('home.latte', [
        'title' => 'Página de Inicio',
        'name' => $name
    ]);
});

Cuando visites /Bob en tu navegador, la salida sería:

<html>
  <head>
    <title>Página de Inicio - Mi App</title>
    <link rel="stylesheet" href="style.css">
  </head>
  <body>
    <h1>¡Hola, Bob!</h1>
  </body>
</html>

Lectura Adicional

Un ejemplo más complejo de usar Latte con layouts se muestra en la sección de plugins increíbles de esta documentación.

Puedes aprender más sobre las capacidades completas de Latte, incluyendo traducción y capacidades de idioma, leyendo la documentación oficial.

Motor de Vistas Integrado

deprecado

Nota: Aunque esta sigue siendo la funcionalidad predeterminada y técnicamente aún funciona.

Para mostrar una plantilla de vista, llama al método render con el nombre del archivo de plantilla y datos de plantilla opcionales:

Flight::render('hello.php', ['name' => 'Bob']);

Los datos de plantilla que pasas se inyectan automáticamente en la plantilla y pueden referenciarse como una variable local. Los archivos de plantilla son simplemente archivos PHP. Si el contenido del archivo de plantilla hello.php es:

¡Hola, <?= $name ?>!

La salida sería:

¡Hola, Bob!

También puedes establecer variables de vista manualmente usando el método set:

Flight::view()->set('name', 'Bob');

La variable name ahora está disponible en todas tus vistas. Así que puedes simplemente 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');

Layouts

Es común que los sitios web tengan un solo archivo de plantilla de layout con contenido intercambiable. Para renderizar contenido que se use en un layout, puedes pasar un parámetro opcional al método render.

Flight::render('header', ['heading' => 'Hola'], 'headerContent');
Flight::render('body', ['body' => 'Mundo'], 'bodyContent');

Tu vista entonces tendrá variables guardadas llamadas headerContent y bodyContent. Puedes renderizar tu layout haciendo:

Flight::render('layout', ['title' => 'Página de Inicio']);

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>Página de Inicio</title>
  </head>
  <body>
    <h1>Hola</h1>
    <div>Mundo</div>
  </body>
</html>

Smarty

Aquí te explico cómo usar el motor de plantillas Smarty para tus vistas:

// Cargar la biblioteca 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 mayor completitud, también deberías sobrescribir el método render predeterminado de Flight:

Flight::map('render', function(string $template, array $data): void {
  Flight::view()->assign($data);
  Flight::view()->display($template);
});

Blade

Aquí te explico cómo usar el motor de plantillas Blade para tus vistas:

Primero, necesitas instalar la biblioteca BladeOne a través de Composer:

composer require eftec/bladeone

Luego, puedes configurar BladeOne como la clase de vista en Flight:

<?php
// Cargar la biblioteca 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 mayor completitud, también deberías sobrescribir el método render predeterminado 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
¡Hola, {{ $name }}!

La salida sería:

¡Hola, Bob!

Ver También

Solución de Problemas

Registro de Cambios

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 utilizando tanto notación de array como de objeto, haciendo que tu código sea más limpio y flexible.

Entendiendo

Una Collection es básicamente un envoltorio alrededor de un array, pero con algunos poderes extra. Puedes usarla como un array, iterar sobre ella, contar sus elementos e incluso acceder a los elementos como si fueran propiedades de un objeto. Esto es especialmente útil cuando quieres pasar datos estructurados en tu aplicación, o cuando quieres hacer que tu código sea un poco más legible.

Las colecciones implementan varias interfaces de PHP:

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 Elementos

Puedes acceder a los elementos utilizando 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 utilizando cualquiera de las dos notaciones:

// Notación de array
$collection['author'] = 'Mike Cao';

// Notación de objeto
$collection->license = 'MIT';

Verificando y Eliminando Elementos

Verifica 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 se pueden convertir fácilmente a JSON:

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

Uso Avanzado

Puedes reemplazar completamente el array de datos interno si es necesario:

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

Las colecciones son especialmente útiles cuando quieres pasar datos estructurados entre componentes, o cuando quieres proporcionar una interfaz más orientada a objetos para datos de array.

Ver También

Solución de Problemas

Registro de Cambios

Learn/flight_vs_fat_free

Flight vs Fat-Free

¿Qué es Fat-Free?

Fat-Free (conocido afectuosamente como F3) es un micro-framework PHP potente pero fácil de usar diseñado para ayudarte a construir aplicaciones web dinámicas y robustas - ¡rápidamente!

Flight se compara con Fat-Free en muchos aspectos y probablemente es 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 no es tan popular como lo fue una vez.

Las actualizaciones se están volviendo menos frecuentes y la comunidad no es tan activa como lo fue antes. El código es lo suficientemente simple, pero a veces la falta de disciplina en la sintaxis puede hacerlo difícil de leer y entender. Funciona para PHP 8.3, pero el código en sí todavía parece vivir en PHP 5.3.

Pros comparado con Flight

Cons comparado con Flight

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:

  1. 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.
  2. 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

Solución de Problemas

Registro de Cambios

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:

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

Solución de Problemas

Registro de Cambios

Learn/flight_vs_slim

Flight vs Slim

¿Qué es Slim?

Slim es un micro framework de PHP que te ayuda a escribir rápidamente aplicaciones web y APIs simples pero potentes.

Mucho de la inspiración para algunas de las características de la versión 3 de Flight en realidad provino de Slim. Agrupar rutas y ejecutar middleware en un orden específico son dos características que fueron inspiradas por Slim. Slim v3 salió orientado a la simplicidad, pero ha habido reseñas mixtas respecto a v4.

Pros en comparación con Flight

Cons en comparación con Flight

Learn/autoloading

Autocarga

Resumen

La autocarga es un concepto en PHP donde especificas un directorio o directorios para cargar clases. Esto es mucho más beneficioso que usar require o include para cargar clases. También es un requisito para usar paquetes de Composer.

Comprensión

Por defecto, cualquier clase de Flight se autocarga automáticamente para ti gracias a Composer. Sin embargo, si quieres autocargar tus propias clases, puedes usar el método Flight::path() para especificar un directorio desde el cual cargar clases.

Usar un autocargador puede ayudar a simplificar tu código de manera significativa. En lugar de tener archivos que comiencen con una multitud de declaraciones include o require al principio para capturar todas las clases que se usan en ese archivo, puedes en su lugar llamar dinámicamente a tus clases y se incluirán automáticamente.

Uso Básico

Supongamos que tenemos un árbol de directorios como el siguiente:

# Ruta de ejemplo
/home/user/project/my-flight-project/
├── app
│   ├── cache
│   ├── config
│   ├── controllers - contiene los controladores para este proyecto
│   ├── translations
│   ├── UTILS - contiene clases solo para esta aplicación (esto está en mayúsculas a propósito para un ejemplo posterior)
│   └── views
└── public
    └── css
    └── js
    └── index.php

Puede que hayas notado que esta es la misma estructura de archivos que el sitio de documentación.

Puedes especificar cada directorio desde el cual cargar de esta manera:


/**
 * public/index.php
 */

// Agregar una ruta al autocargador
Flight::path(__DIR__.'/../app/controllers/');
Flight::path(__DIR__.'/../app/utils/');

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

// no se requiere nombres de espacio

// Se recomienda que todas las clases autocargadas estén en Pascal Case (cada palabra capitalizada, sin espacios)
class MyController {

    public function index() {
        // hacer algo
    }
}

Espacios de Nombres

Si tienes espacios de nombres, en realidad se vuelve muy fácil implementarlo. Debes usar el método Flight::path() para especificar el directorio raíz (no la raíz del documento o la carpeta public/) de tu aplicación.


/**
 * public/index.php
 */

// Agregar una ruta al autocargador
Flight::path(__DIR__.'/../');

Ahora es así como podría verse tu controlador. Mira el ejemplo a continuación, pero presta atención a los comentarios para información importante.

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

// los espacios de nombres son requeridos
// los espacios de nombres son los mismos que la estructura de directorios
// los espacios de nombres deben seguir el mismo caso que la estructura de directorios
// los espacios de nombres y directorios no pueden tener guiones bajos (a menos que Loader::setV2ClassLoading(false) esté configurado)
namespace app\controllers;

// Se recomienda que todas las clases autocargadas estén en Pascal Case (cada palabra capitalizada, 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 utils, harías básicamente lo mismo:


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

// el espacio de nombres debe coincidir con la estructura de directorios y el caso (nota que el directorio UTILS está en mayúsculas
//     como en el árbol de archivos arriba)
namespace app\UTILS;

class ArrayHelperUtil {

    public function changeArrayCase(array $array) {
        // hacer algo
    }
}

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 aquellos que lo necesiten.

use flight\core\Loader;

/**
 * public/index.php
 */

// Agregar 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 nombres de espacio

class My_Controller {

    public function index() {
        // hacer algo
    }
}

Ver También

Solución de Problemas

Clase No Encontrada (autocarga no funciona)

Podría haber un par de razones para que esto no suceda. A continuación hay algunos ejemplos, pero asegúrate de revisar también la sección de autocarga.

Nombre de Archivo Incorrecto

El 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 debería llamarse MyClass.php. Si tienes una clase llamada MyClass y el archivo se llama myclass.php entonces el autocargador no podrá encontrarla.

Espacio de Nombres Incorrecto

Si estás usando espacios de nombres, entonces el espacio de nombres debería coincidir con la estructura de directorios.

// ...código...

// si tu MyController está en el directorio app/controllers y está con espacio de nombres
// esto no funcionará.
Flight::route('/hello', 'MyController->hello');

// necesitarás elegir una de estas opciones
Flight::route('/hello', 'app\controllers\MyController->hello');
// o si tienes una declaración use arriba

use app\controllers\MyController;

Flight::route('/hello', [ MyController::class, 'hello' ]);
// también puede escribirse
Flight::route('/hello', MyController::class.'->hello');
// también...
Flight::route('/hello', [ 'app\controllers\MyController', 'hello' ]);

path() no definido

En la aplicación esqueleto, esto se define dentro del archivo config.php, pero para que se encuentren tus clases, necesitas asegurarte de que el método path() esté definido (probablemente a la raíz de tu directorio) antes de intentar usarlo.

// Agregar una ruta al autocargador
Flight::path(__DIR__.'/../');

Registro de Cambios

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:

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

Solución de Problemas

Registro de Cambios

Guides/unit_testing

Pruebas Unitarias en Flight PHP con PHPUnit

Esta guía introduce las pruebas unitarias en Flight PHP utilizando PHPUnit, dirigida a principiantes que desean entender por qué las pruebas unitarias son importantes y cómo aplicarlas de manera práctica. Nos enfocaremos en probar comportamientos—asegurando que tu aplicación haga lo que esperas, como enviar un correo electrónico o guardar un registro—en lugar de cálculos triviales. Comenzaremos con un simple manejador de rutas y avanzaremos a un controlador más complejo, incorporando inyección de dependencias (DI) y simulando 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. Es especialmente valioso en Flight, donde el enrutamiento ligero y la flexibilidad pueden llevar a interacciones complejas. Para desarrolladores individuales o equipos, las pruebas unitarias actúan como una red de seguridad, documentando el comportamiento esperado y previniendo regresiones cuando revisitas el código más tarde. También mejoran el diseño: el código difícil de probar a menudo indica clases excesivamente complejas o fuertemente acopladas.

A diferencia de ejemplos simplistas (por ejemplo, probar x * y = z), nos enfocaremos en comportamientos del mundo real, como validar entradas, guardar datos o activar acciones como correos electrónicos. Nuestro objetivo es hacer que las pruebas sean accesibles y significativas.

Principios Guía Generales

  1. Probar Comportamiento, No Implementación: Enfócate en resultados (por ejemplo, “correo enviado” o “registro guardado”) en lugar de detalles internos. Esto hace que las pruebas sean robustas frente a refactorizaciones.
  2. Deja de usar Flight::: Los métodos estáticos de Flight son terriblemente convenientes, pero hacen que las pruebas sean difíciles. Debes acostumbrarte a usar la variable $app de $app = Flight::app();. $app tiene todos los mismos métodos que Flight::. Todavía 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 podrás usar $router->get(), $router->post(), $router->group(), etc. Ver Enrutamiento.
  3. Mantén las Pruebas Rápidas: Las pruebas rápidas fomentan ejecuciones frecuentes. 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.
  4. Usa Nombres Descriptivos: Los nombres de las pruebas deben describir claramente el comportamiento que se está probando. Esto mejora la legibilidad y el mantenimiento.
  5. Evita Globales Como la Peste: Minimiza el uso de $app->set() y $app->get(), ya que actúan como estado global, requiriendo simulaciones en cada prueba. Prefiere DI o un contenedor DI (ver 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 biblioteca de sesiones como flightphp/session para que puedas simular el objeto de sesión en tus pruebas. No llames a $_SESSION directamente en tu código, ya que eso inyecta una variable global en tu código, haciendo que sea difícil de probar.
  6. Usa Inyección de Dependencias: Inyecta dependencias (por ejemplo, PDO, remitentes de correo) en los controladores para aislar la lógica y simplificar la simulación. Si tienes una clase con demasiadas dependencias, considera refactorizarla en clases más pequeñas que cada una tenga una sola responsabilidad siguiendo los principios SOLID.
  7. 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 principal se ejecute. Por ejemplo, si tu aplicación envía un mensaje de texto, NO quieres enviar realmente un mensaje de texto cada vez que ejecutes tus pruebas porque esos cargos se acumularán (y será más lento). En su lugar, simula el servicio de mensaje de texto y solo verifica que tu código llamó al servicio de mensaje de texto con los parámetros correctos.
  8. Apunta a una Alta Cobertura, No a la Perfección: 100% de cobertura de líneas es bueno, pero no significa realmente que todo en tu código se pruebe de la manera que debería (adelante, investiga cobertura de rama/camino en PHPUnit). Prioriza comportamientos críticos (por ejemplo, registro de usuarios, respuestas de API y capturar respuestas fallidas).
  9. Usa Controladores para Rutas: En tus definiciones de rutas, usa controladores no closures. La instancia flight\Engine $app se 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, inyectarla en tu controlador y llamar métodos directamente (por ejemplo, $controller->register()). Ver Extensión de Flight y Enrutamiento.
  10. Elige un estilo de simulación y mantente con él: PHPUnit soporta varios estilos de simulación (por ejemplo, prophecy, simulaciones integradas), o puedes usar clases anónimas que tienen sus propios beneficios como completado de código, romper si cambias la definición del método, etc. Solo sé consistente en todas tus pruebas. Ver Objetos Mock de PHPUnit.
  11. Usa visibilidad protected para métodos/propiedades que quieras probar en subclases: Esto te permite sobrescribirlos en subclases de prueba sin hacerlos públicos, esto es especialmente útil para simulaciones de clases anónimas.

Configurando PHPUnit

Primero, configura PHPUnit en tu proyecto Flight PHP usando Composer para pruebas fáciles. Ver la guía de inicio de PHPUnit para más detalles.

  1. En el directorio de tu proyecto, ejecuta:

    composer require --dev phpunit/phpunit

    Esto instala la última versión de PHPUnit como una dependencia de desarrollo.

  2. Crea un directorio tests en la raíz de tu proyecto para archivos de prueba.

  3. Agrega un script de prueba a composer.json para mayor comodidad:

    // otro contenido de composer.json
    "scripts": {
       "test": "phpunit --configuration phpunit.xml"
    }
  4. Crea un archivo phpunit.xml en 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 construidas, puedes ejecutar composer test para ejecutar las pruebas.

Probando un Manejador de Ruta Simple

Comencemos con una ruta básica ruta que valida la entrada de correo electrónico de un usuario. Probaremos su comportamiento: devolviendo 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. Ver Pruebas Unitarias y Principios SOLID para más sobre estructuración de 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'; // Simulate POST data
        $UserController = new UserController($app);
        $UserController->register($request->data->email);
        $response = $app->response()->getBody();
        $output = json_decode($response, true);
        $this->assertEquals('success', $output['status']);
        $this->assertEquals('Valid email', $output['message']);
    }

    public function testInvalidEmailReturnsError() {
        $app = new Engine();
        $request = $app->request();
        $request->data->email = 'invalid-email'; // Simulate POST data
        $UserController = new UserController($app);
        $UserController->register($request->data->email);
        $response = $app->response()->getBody();
        $output = json_decode($response, true);
        $this->assertEquals('error', $output['status']);
        $this->assertEquals('Invalid email', $output['message']);
    }
}

Puntos Clave:

Ejecuta composer test para verificar que la ruta se comporte como se espera. Para más sobre requests y responses en Flight, ver la documentación relevante.

Usando Inyección de Dependencias para Controladores Probables

Para escenarios más complejos, usa inyección de dependencias (DI) para hacer controladores probables. Evita los globales de Flight (por ejemplo, Flight::set(), Flight::map(), Flight::register()) ya que actúan como estado global, requiriendo simulaciones para cada prueba. En su lugar, usa el contenedor DI de Flight, DICE, PHP-DI o DI manual.

Usemos flight\database\PdoWrapper en lugar de PDO crudo. ¡Este wrapper es mucho más fácil de simular y probar unitariamente!

Aquí hay un controlador que guarda un usuario en una base de datos y envía un correo de bienvenida:

use flight\database\PdoWrapper;

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

    public function __construct(Engine $app, PdoWrapper $db, MailerInterface $mailer) {
        $this->app = $app;
        $this->db = $db;
        $this->mailer = $mailer;
    }

    public function register() {
        $email = $this->app->request()->data->email;
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            // adding the return here helps unit testing to stop execution
            return $this->app->jsonHalt(['status' => 'error', 'message' => 'Invalid email']);
        }

        $this->db->runQuery('INSERT INTO users (email) VALUES (?)', [$email]);
        $this->mailer->sendWelcome($email);

        return $this->app->json(['status' => 'success', 'message' => 'User registered']);
    }
}

Puntos Clave:

Probando el Controlador con Simulaciones

Ahora, probemos el comportamiento de UserController: validando correos, guardando en la base de datos y enviando correos. Simularemos la base de datos y el remitente para aislar el controlador.

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

class UserControllerDICTest extends TestCase {
    public function testValidEmailSavesAndSendsEmail() {

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

            // When we mock it this way, we are not really making a database call.
            // We can further setup this to alter the PDOStatement mock to simulate failures, etc.
            public function runQuery(string $sql, array $params = []): PDOStatement {
                return $this->statementMock;
            }
        };
        $mockMailer = new class implements MailerInterface {
            public $sentEmail = null;
            public function sendWelcome($email): bool {
                $this->sentEmail = $email;
                return true;    
            }
        };
        $app = new Engine();
        $app->request()->data->email = 'test@example.com';
        $controller = new UserControllerDIC($app, $mockDb, $mockMailer);
        $controller->register();
        $response = $app->response()->getBody();
        $result = json_decode($response, true);
        $this->assertEquals('success', $result['status']);
        $this->assertEquals('User registered', $result['message']);
        $this->assertEquals('test@example.com', $mockMailer->sentEmail);
    }

    public function testInvalidEmailSkipsSaveAndEmail() {
         $mockDb = new class() extends PdoWrapper {
            // An empty constructor bypasses the parent constructor
            public function __construct() {}
            public function runQuery(string $sql, array $params = []): PDOStatement {
                throw new Exception('Should not be called');
            }
        };
        $mockMailer = new class implements MailerInterface {
            public $sentEmail = null;
            public function sendWelcome($email): bool {
                throw new Exception('Should not be called');
            }
        };
        $app = new Engine();
        $app->request()->data->email = 'invalid-email';

        // Need to map jsonHalt to avoid exiting
        $app->map('jsonHalt', function($data) use ($app) {
            $app->json($data, 400);
        });
        $controller = new UserControllerDIC($app, $mockDb, $mockMailer);
        $controller->register();
        $response = $app->response()->getBody();
        $result = json_decode($response, true);
        $this->assertEquals('error', $result['status']);
        $this->assertEquals('Invalid email', $result['message']);
    }
}

Puntos Clave:

Simulando Demasiado

Ten cuidado de no simular demasiado de tu código. Déjame darte un ejemplo abajo sobre por qué esto podría ser algo malo usando nuestro UserController. Cambiaremos esa verificación en un método llamado isEmailValid (usando filter_var) y las otras nuevas adiciones en un método separado llamado registerUser.

use flight\database\PdoWrapper;
use flight\Engine;

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

    public function __construct(Engine $app, PdoWrapper $db, MailerInterface $mailer) {
        $this->app = $app;
        $this->db = $db;
        $this->mailer = $mailer;
    }

    public function register() {
        $email = $this->app->request()->data->email;
        if (!$this->isEmailValid($email)) {
            // adding the return here helps unit testing to stop execution
            return $this->app->jsonHalt(['status' => 'error', 'message' => 'Invalid email']);
        }

        $this->registerUser($email);

        $this->app->json(['status' => 'success', 'message' => 'User registered']);
    }

    protected function isEmailValid($email) {
        return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
    }

    protected function registerUser($email) {
        $this->db->runQuery('INSERT INTO users (email) VALUES (?)', [$email]);
        $this->mailer->sendWelcome($email);
    }
}

Y ahora la prueba unitaria sobremapeada que no prueba realmente nada:

use PHPUnit\Framework\TestCase;

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

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

            // Bypass the actual DB and mailer calls
            protected function registerUser($email) {
                return false;
            }
        };
        $controller->register();
        $response = $app->response()->getBody();
        $result = json_decode($response, true);
        $this->assertEquals('success', $result['status']);
        $this->assertEquals('User registered', $result['message']);
    }
}

¡Hurra, tenemos pruebas unitarias y están pasando! Pero espera, ¿qué pasa 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 {

    // ... other methods ...

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

Si ejecuto mis pruebas unitarias anteriores, ¡todavía pasan! Pero porque no estaba probando por comportamiento (dejando que algo del código se ejecute realmente), he codificado potencialmente un error esperando ocurrir en producción. La prueba debería modificarse para tener en cuenta el nuevo comportamiento, y también lo opuesto de cuando el comportamiento no es lo 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, ver Pruebas Unitarias y Principios SOLID.

Errores Comunes

Escalando con Pruebas Unitarias

Las pruebas unitarias brillan en proyectos más grandes o cuando revisitas código después de meses. Documentan comportamiento y detectan regresiones, ahorrándote de re-aprender tu aplicación. Para desarrolladores individuales, prueba rutas críticas (por ejemplo, registro de usuarios, procesamiento de pagos). Para equipos, las pruebas aseguran comportamiento consistente a través de contribuciones. Ver ¿Por qué Frameworks? para más 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

Construyendo un Blog Simple con Flight PHP

Esta guía te guía a través de la creación de un blog básico utilizando el framework Flight PHP. Configurarás un proyecto, definirás rutas, administrará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 de publicaciones individuales y un formulario de creación.

Requisitos Previos

Paso 1: Configura Tu Proyecto

Comienza creando un nuevo directorio de proyecto e instalando Flight a través de Composer.

  1. Crear un Directorio:

    mkdir flight-blog
    cd flight-blog
  2. Instalar Flight:

    composer require flightphp/core
  3. Crear un Directorio Público: Flight utiliza un único punto de entrada (index.php). Crea una carpeta public/ para ello:

    mkdir public
  4. Básico index.php: Crea public/index.php con una ruta simple de “hola mundo”:

    <?php
    require '../vendor/autoload.php';
    
    Flight::route('/', function () {
       echo '¡Hola, Flight!';
    });
    
    Flight::start();
  5. Ejecutar el Servidor Integrado: Prueba tu configuración con el servidor de desarrollo de PHP:

    php -S localhost:8000 -t public/

    Visita http://localhost:8000 para ver “¡Hola, Flight!”.

Paso 2: Organizar 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

Paso 3: Instalar y Configurar Latte

Latte es un motor de plantillas ligero que se integra bien con Flight.

  1. Instalar Latte:

    composer require latte/latte
  2. Configurar Latte en Flight: Actualiza public/index.php para registrar Latte como el motor de vista:

    <?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' => 'Mi Blog']);
    });
    
    Flight::start();
  3. Crear una Plantilla de Diseño: En app/views/layout.latte:

    <!DOCTYPE html>
    <html>
    <head>
    <title>{$title}</title>
    </head>
    <body>
    <header>
        <h1>Mi Blog</h1>
        <nav>
            <a href="/">Inicio</a> | 
            <a href="/create">Crear una Publicación</a>
        </nav>
    </header>
    <main>
        {block content}{/block}
    </main>
    <footer>
        <p>&copy; {date('Y')} Blog Flight</p>
    </footer>
    </body>
    </html>
  4. Crear 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 te has salido de él y visita http://localhost:8000 para ver la página renderizada.

  5. Crear un Archivo de Datos:

    Utiliza un archivo JSON para simular una base de datos por simplicidad.

    En data/posts.json:

    [
       {
           "slug": "primer-post",
           "title": "Mi Primer Post",
           "content": "¡Esta es mi primera publicación de blog con Flight PHP!"
       }
    ]

Paso 4: Definir Rutas

Separa tus rutas en un archivo de configuración para mejor organización.

  1. Crear routes.php: En app/config/routes.php:

    <?php
    Flight::route('/', function () {
       Flight::view()->render('home.latte', ['title' => 'Mi Blog']);
    });
    
    Flight::route('/post/@slug', function ($slug) {
       Flight::view()->render('post.latte', ['title' => 'Publicación: ' . $slug, 'slug' => $slug]);
    });
    
    Flight::route('GET /create', function () {
       Flight::view()->render('create.latte', ['title' => 'Crear una Publicación']);
    });
  2. Actualizar 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: Almacenar y Recuperar Publicaciones de Blog

Agrega los métodos para cargar y guardar publicaciones.

  1. Agregar 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);
    });
  2. Actualizar Rutas: Modifica app/config/routes.php para usar publicaciones:

    <?php
    Flight::route('/', function () {
       $posts = Flight::posts();
       Flight::view()->render('home.latte', [
           'title' => 'Mi 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' => 'Crear una Publicación']);
    });

Paso 6: Crear Plantillas

Actualiza tus plantillas para mostrar publicaciones.

  1. 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: Agregar Creación de Publicación

Manejar la presentación del formulario para agregar nuevas publicaciones.

  1. Crear 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">Título:</label>
                <input type="text" name="title" id="title" required>
            </div>
            <div class="form-group">
                <label for="content">Contenido:</label>
                <textarea name="content" id="content" required></textarea>
            </div>
            <button type="submit">Guardar Publicación</button>
        </form>
    {/block}
  2. Agregar 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('/');
    });
  3. Prueba:

    • Visita http://localhost:8000/create.
    • Envía una nueva publicación (por ejemplo, “Segundo Post” con algo de contenido).
    • Verifica la página de inicio para verlo listado.

Paso 8: Mejorar con Manejo de Errores

Sobrescribe el método notFound para una mejor experiencia 404.

En index.php:

Flight::map('notFound', function () {
    Flight::view()->render('404.latte', ['title' => 'Página No Encontrada']);
});

Crea app/views/404.latte:

{extends 'layout.latte'}

{block content}
    <h2>404 - {$title}</h2>
    <p>¡Lo siento, esa página no existe!</p>
{/block}

Próximos Pasos

Conclusión

¡Has construido un blog simple con Flight PHP! Esta guía demuestra características básicas como enrutamiento, templating con Latte y manejo de envíos de formularios, todo mientras mantienes las cosas ligeras. ¡Explora la documentación de Flight para más características avanzadas que lleven 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

Marco de trabajo PHP Flight

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 experimentando con las últimas herramientas impulsadas por IA, la huella baja de Flight y su diseño directo lo convierten en una opción perfecta. Flight está destinado a ser ligero, pero también puede manejar requisitos de arquitectura empresarial.

¿Por qué elegir Flight?

Resumen en video

¿Suficientemente simple, verdad?
Aprende más sobre Flight en la documentación!

Inicio rápido

Para hacer una instalación básica y rápida, instálalo con Composer:

composer require flightphp/core

O puedes descargar un zip del repositorio aquí. Luego, tendrías un archivo básico index.php 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 básica de Flight. Ahora puedes ejecutar este archivo con php -S localhost:8000 y visitar http://localhost:8000 en tu navegador para ver la salida.

Aplicación esqueleto/plantilla

Hay una aplicación de ejemplo para ayudarte a comenzar tu proyecto con Flight. Tiene una estructura organizada, configuraciones básicas listas y maneja scripts de composer desde el principio. Echa un vistazo a flightphp/skeleton para un proyecto listo para usar, o visita la página de ejemplos para inspiración. ¿Quieres ver cómo encaja la IA? Explora ejemplos impulsados por IA.

Instalando la aplicación esqueleto

¡Fácil!

# Crea el nuevo proyecto
composer create-project flightphp/skeleton my-project/
# Entra en el directorio de tu nuevo proyecto
cd my-project/
# Inicia el servidor de desarrollo local para comenzar de inmediato
composer start

¡Creará la estructura del proyecto, configurará los archivos que necesitas y estarás listo para comenzar!

Alto rendimiento

Flight es uno de los frameworks de PHP más rápidos disponibles. Su núcleo ligero significa menos sobrecarga y más velocidad, perfecto para aplicaciones tradicionales y proyectos modernos impulsados por IA. Puedes ver todos los benchmarks en TechEmpower.

Ve el benchmark a continuación con algunos otros frameworks de PHP populares.

Framework Reqs/sec en texto plano Reqs/sec en 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 y la IA

¿Curioso de cómo maneja la IA? Descubre cómo Flight facilita trabajar con tu LLM de codificación favorito.

Comunidad

Estamos en Matrix Chat

Matrix

Y Discord

Contribuyendo

Hay dos formas en que puedes contribuir a Flight:

  1. Contribuye al framework principal visitando el repositorio principal.
  2. ¡Ayuda a mejorar los documentos! Este sitio web de documentación se aloja en Github. Si encuentras un error o quieres mejorar algo, no dudes en enviar una solicitud de extracción. ¡Amamos las actualizaciones y nuevas ideas, especialmente alrededor de la IA y las nuevas tecnologías!

Requisitos

Flight requiere PHP 7.4 o superior.

Nota: PHP 7.4 es compatible porque, en el momento de escribir esto (2024), PHP 7.4 es la versión predeterminada para algunas distribuciones LTS de Linux. Forzar un cambio a PHP >8 causaría muchos problemas para esos usuarios. El framework también soporta PHP >8.

Licencia

Flight se libera 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 y autónoma de caché en archivo PHP bifurcada de Wruczek/PHP-File-Cache

Ventajas

¡Este sitio de documentación está utilizando esta biblioteca para almacenar en caché cada una de las páginas!

Haz clic aquí para ver el código.

Instalación

Instala mediante composer:

composer require flightphp/cache

Uso

El uso es bastante directo. Esto guarda un archivo de caché en el directorio de caché.

use flight\Cache;

$app = Flight::app();

// Pasas el directorio donde se almacenará el caché al constructor
$app->register('cache', Cache::class, [ __DIR__ . '/../cache/' ], function(Cache $cache) {

    // Esto asegura que el caché solo se use en modo producción
    // ENVIRONMENT es una constante que se establece en tu archivo de bootstrap o en otra parte de tu app
    $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 conveniente que actualice el caché si ha expirado, 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 ser almacenados 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 el caché.

Flight::cache()->set('simple-cache-test', 'my cached data', 10); // 10 segundos

Borrar un Valor de Caché

Usas el método delete() para borrar un valor en el caché.

Flight::cache()->delete('simple-cache-test');

Verificar si Existe un Valor de Caché

Usas el método exists() para verificar si un valor existe en el caché.

if(Flight::cache()->exists('simple-cache-test')) {
    // hacer algo
}

Limpiar el Caché

Usas el método flush() para limpiar todo el 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 el parámetro correcto.

$data = $cache->refreshIfExpired("simple-cache-meta-test", function () {
    echo "Refreshing data!" . PHP_EOL;
    return date("H:i:s"); // devolver datos para ser almacenados 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 expiran los datos 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";

Documentación

Visita https://github.com/flightphp/cache para ver el código. Asegúrate de ver la carpeta examples para formas adicionales de usar el caché.

Awesome-plugins/permissions

FlightPHP/Permisos

Se trata de un módulo de permisos que se puede utilizar en tus proyectos si tienes múltiples roles en tu aplicación y cada rol tiene una funcionalidad un poco diferente. Este módulo te permite definir permisos para cada rol y luego verificar si el usuario actual tiene el permiso para acceder a una página específica o realizar una acción determinada.

Haz clic aquí para acceder al repositorio en GitHub.

Instalación

¡Ejecuta composer require flightphp/permissions y estás listo/a!

Uso

Primero necesitas configurar tus permisos, luego le indicas a tu aplicación lo que significan los permisos. Finalmente verificarás tus permisos con $Permissions->has(), ->can(), o is(). has() y can() tienen la misma funcionalidad, pero están nombrados de manera diferente para que tu código sea más legible.

Ejemplo Básico

Imaginemos que tienes una función en tu aplicación que verifica si un usuario ha iniciado sesión. Puedes crear un objeto de permisos de esta manera:

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

// algún código

// luego probablemente tengas algo que te diga cuál es el rol actual de la persona
// probablemente tengas algo donde obtienes el rol actual
// de una variable de sesión que lo define
// después de que alguien inicie sesión, de lo contrario tendrán un rol de 'invitado' o 'público'.
$current_role = 'admin';

// configurar permisos
$permiso = new \flight\Permiso($current_role);
$permiso->defineRegla('iniciadoSesion', function($current_role) {
    return $current_role !== 'invitado';
});

// Probablemente querrás persistir este objeto en Flight en algún lugar
Flight::set('permiso', $permiso);

Luego en un controlador en algún lugar, podrías tener algo como esto.

<?php

// algún controlador
class AlgunControlador {
    public function algunaAccion() {
        $permiso = Flight::get('permiso');
        if ($permiso->has('iniciadoSesion')) {
            // hacer algo
        } else {
            // hacer algo más
        }
    }
}

También puedes usar esto para rastrear si tienen permiso para hacer algo en tu aplicación. Por ejemplo, si tienes una forma en la que los usuarios pueden interactuar con la publicación en tu software, puedes verificar si tienen permiso para realizar ciertas acciones.

$current_role = 'admin';

// configurar permisos
$permiso = new \flight\Permiso($current_role);
$permiso->defineRegla('publicación', function($current_role) {
    if($current_role === 'admin') {
        $permisos = ['crear', 'leer', 'actualizar', 'eliminar'];
    } else if($current_role === 'editor') {
        $permisos = ['crear', 'leer', 'actualizar'];
    } else if($current_role === 'autor') {
        $permisos = ['crear', 'leer'];
    } else if($current_role === 'colaborador') {
        $permisos = ['crear'];
    } else {
        $permisos = [];
    }
    return $permisos;
});
Flight::set('permiso', $permiso);

Luego en un controlador en algún lugar...

class ControladorPublicación {
    public function crear() {
        $permiso = Flight::get('permiso');
        if ($permiso->can('publicación.crear')) {
            // hacer algo
        } else {
            // hacer algo más
        }
    }
}

Inyectar dependencias

Puedes inyectar dependencias en el cierre que define los permisos. Esto es útil si tienes algún tipo de interruptor, identificación, u otro punto de datos que deseas verificar. Lo mismo funciona para llamadas de tipo Clase->Método, excepto que defines los argumentos en el método.

Cierres

$Permiso->defineRegla('orden', function(string $current_role, MiDependencia $MiDependencia = null) {
    // ... código
});

// en tu archivo de controlador
public function crearOrden() {
    $MiDependencia = Flight::miDependencia();
    $permiso = Flight::get('permiso');
    if ($permiso->can('orden.crear', $MiDependencia)) {
        // hacer algo
    } else {
        // hacer algo más
    }
}

Clases

namespace MiApp;

class Permisos {

    public function orden(string $current_role, MiDependencia $MiDependencia = null) {
        // ... código
    }
}

Atajo para establecer permisos con clases

También puedes usar clases para definir tus permisos. Esto es útil si tienes muchos permisos y deseas mantener tu código limpio. Puedes hacer algo como esto:

<?php

// código de inicio
$Permisos = new \flight\Permiso($current_role);
$Permisos->defineRegla('orden', 'MiApp\Permisos->orden');

// myapp/Permisos.php
namespace MiApp;

class Permisos {

    public function orden(string $current_role, int $user_id) {
        // Suponiendo que configuraste esto de antemano
        /** @var \flight\database\PdoWrapper $db */
        $db = Flight::db();
        $permisos_permitidos = [ 'leer' ]; // todos pueden ver una orden
        if($current_role === 'gerente') {
            $permisos_permitidos[] = 'crear'; // los gerentes pueden crear órdenes
        }
        $algún_interruptor_especial_de_db = $db->fetchField('SELECT algún_interruptor_especial FROM ajustes WHERE id = ?', [ $user_id ]);
        if($algún_interruptor_especial_de_db) {
            $permisos_permitidos[] = 'actualizar'; // si el usuario tiene un interruptor especial, pueden actualizar órdenes
        }
        if($current_role === 'admin') {
            $permisos_permitidos[] = 'eliminar'; // los administradores pueden eliminar órdenes
        }
        return $permisos_permitidos;
    }
}

La parte genial es que también hay un atajo que puedes usar (¡también puede ser almacenado en caché!) donde simplemente indicas a la clase de permisos que mapee todos los métodos de una clase en permisos. Por lo tanto, si tienes un método llamado orden() y un método llamado empresa(), se mapearán automáticamente para que puedas simplemente ejecutar $Permisos->has('orden.leer') o $Permisos->has('empresa.leer') y funcionará. Definir esto es muy complejo, así que quédate conmigo aquí. Solo necesitas hacer esto:

Crea la clase de permisos que deseas agrupar.

class MisPermisos {
    public function orden(string $current_role, int $orden_id = 0): array {
        // código para determinar permisos
        return $arreglo_permisos;
    }

    public function empresa(string $current_role, int $empresa_id): array {
        // código para determinar permisos
        return $arreglo_permisos;
    }
}

Luego haz que los permisos sean descubribles usando esta biblioteca.

$Permisos = new \flight\Permiso($current_role);
$Permisos->defineReglasDesdeMétodosDeClase(MiApp\Permisos::class);
Flight::set('permisos', $Permisos);

Finalmente, llama al permiso en tu base de código para verificar si el usuario tiene permiso para realizar un permiso dado.

class AlgunControlador {
    public function crearOrden() {
        if(Flight::get('permisos')->can('orden.crear') === false) {
            die('¡No puedes crear una orden. ¡Lo siento!');
        }
    }
}

Caché

Para habilitar la caché, consulta la sencilla biblioteca wruczak/phpfilecache. Un ejemplo de cómo habilitar esto se muestra a continuación.


// esta variable $app puede ser parte de tu código, o
// simplemente puedes pasar null y se
// obtendrá de Flight::app() en el constructor
$app = Flight::app();

// Por ahora solo acepta esto como una caché de archivos. Otros pueden agregarse fácilmente en el futuro.
$Caché = new Wruczek\PhpFileCaché\PhpFileCaché;

$Permisos = new \flight\Permiso($current_role, $app, $Caché);
$Permisos->defineReglasDesdeMétodosDeClase(MiApp\Permisos::class, 3600); // 3600 indica cuántos segundos almacenar en caché. Omite esto para no usar la 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:

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

Instalación

  1. Sube la carpeta flight-integration a tu directorio /wp-content/plugins/.
  2. Activa el plugin en la administración de WordPress (menú de Plugins).
  3. Ve a Settings > Flight Framework para configurar el plugin.
  4. Establece la ruta del proveedor a tu instalación de Flight (o usa Composer para instalar Flight).
  5. Configura la ruta de tu carpeta de aplicación y crea la estructura de carpetas (¡el plugin puede ayudarte con esto!).
  6. ¡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/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

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

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

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:

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:

Los Scripts SQL

Los scripts se dividen en tres conjuntos de scripts:

El directorio de scripts es:

 <root dir>
     |
     +-- base.sql
     |
     +-- /migrations
              |
              +-- /up
                   |
                   +-- 00001.sql
                   +-- 00002.sql
              +-- /down
                   |
                   +-- 00000.sql
                   +-- 00001.sql

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:

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

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

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

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:

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:

Todos los métodos excepto get() y id() devuelven la instancia de Session para encadenamiento.

¿Por Qué Usar Este Plugin?

Detalles Técnicos

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

Pista

Pista es una aplicación CLI que te ayuda a gestionar tus aplicaciones Flight. Puede generar controladores, mostrar todas las rutas y más. Está basado en la excelente biblioteca adhocore/php-cli.

Haz clic aquí para ver el código.

Instalación

Instala con composer.

composer require flightphp/runway

Configuración Básica

La primera vez que ejecutes Pista, te guiará a través de un proceso de configuración y creará un archivo de configuración .runway.json en la raíz de tu proyecto. Este archivo contendrá algunas configuraciones necesarias para que Pista funcione correctamente.

Uso

Pista tiene varios comandos que puedes usar para gestionar tu aplicación Flight. Hay dos formas fáciles de usar Pista.

  1. Si estás usando el proyecto esqueleto, puedes ejecutar php runway [comando] desde la raíz de tu proyecto.
  2. Si estás usando Pista como un paquete instalado a través de composer, puedes ejecutar vendor/bin/runway [comando] desde la raíz de tu proyecto.

Para cualquier comando, puedes agregar la bandera --help para obtener más información sobre cómo usar el comando.

php runway routes --help

Aquí tienes algunos ejemplos:

Generar un Controlador

Basado en la configuración en tu archivo .runway.json, la ubicación predeterminada generará un controlador para ti en el directorio app/controllers/.

php runway make:controller MiControlador

Generar un Modelo de Active Record

Basado en la configuración en tu archivo .runway.json, la ubicación predeterminada generará un modelo de Active Record para ti en el directorio app/records/.

php runway make:record usuarios

Si por ejemplo tienes la tabla usuarios con el siguiente esquema: id, nombre, correo, creado_en, actualizado_en, se creará un archivo similar al siguiente en el archivo app/records/UserRecord.php:

<?php

declare(strict_types=1);

namespace app\records;

/**
 * Clase ActiveRecord para la tabla de usuarios.
 * @link https://docs.flightphp.com/awesome-plugins/active-record
 * 
 * @property int $id
 * @property string $nombre
 * @property string $correo
 * @property string $creado_en
 * @property string $actualizado_en
 * // también puedes añadir relaciones aquí una vez las definas en el array $relations
 * @property CompanyRecord $company Ejemplo de una relación
 */
class UserRecord extends \flight\ActiveRecord
{
    /**
     * @var array $relations Establece las relaciones para el modelo
     *   https://docs.flightphp.com/awesome-plugins/active-record#relationships
     */
    protected array $relations = [];

    /**
     * Constructor
     * @param mixed $conexionBaseDeDatos La conexión a la base de datos
     */
    public function __construct($conexionBaseDeDatos)
    {
        parent::__construct($conexionBaseDeDatos, 'usuarios');
    }
}

Mostrar Todas las Rutas

Esto mostrará todas las rutas que están actualmente registradas en Flight.

php runway routes

Si deseas ver solo rutas específicas, puedes agregar una bandera para filtrar las rutas.

# Mostrar solo rutas GET
php runway routes --get

# Mostrar solo rutas POST
php runway routes --post

# etc.

Personalización de Pista

Si estás creando un paquete para Flight, o deseas añadir tus propios comandos personalizados a tu proyecto, puedes hacerlo creando un directorio src/commands/, flight/commands/, app/commands/ o commands/ para tu proyecto/paquete.

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 flight\commands;

class ExampleCommand extends AbstractBaseCommand
{
    /**
     * Constructor
     *
     * @param array<string,mixed> $config JSON config de .runway-config.json
     */
    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(string $controlador)
    {
        $io = $this->app()->io();

        $io->info('Creando ejemplo...');

        // Haz algo aquí

        $io->ok('¡Ejemplo creado!');
    }
}

Consulta la Documentación de adhocore/php-cli para obtener más información sobre cómo crear tus propios comandos personalizados en tu aplicación Flight.

Awesome-plugins/tracy_extensions

Tracy Flight Panel Extensions

Este es un conjunto de extensiones para hacer que trabajar con Flight sea un poco más completo.

Este es el Panel

Flight Bar

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

Flight Data Flight Database Flight Request

Haz clic aquí para ver el código.

Installation

Ejecuta composer require flightphp/tracy-extensions --dev y estarás listo para empezar!

Configuration

Hay muy poca configuración que necesites hacer para comenzar. Deberás iniciar el depurador de Tracy antes de usar esto https://tracy.nette.org/en/guide:

<?php

use Tracy\Debugger;
use flight\debug\tracy\TracyExtensionLoader;

// bootstrap code
require __DIR__ . '/vendor/autoload.php';

Debugger::enable();
// Es posible que necesites especificar tu entorno con Debugger::enable(Debugger::DEVELOPMENT)

// si usas conexiones a la base de datos en tu app, hay un 
// wrapper PDO requerido para usar SOLO EN DESARROLLO (¡no en producción por favor!)
// Tiene los mismos parámetros que una conexión PDO regular
$pdo = new PdoQueryCapture('sqlite:test.db', 'user', 'pass');
// o si lo adjuntas al framework Flight
Flight::register('db', PdoQueryCapture::class, ['sqlite:test.db', 'user', 'pass']);
// ahora cada vez que hagas 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 podrá renderizar :(
    Flight::set('flight.content_length', false);
    new TracyExtensionLoader(Flight::app());
}

// more code

Flight::start();

Additional Configuration

Session Data

Si tienes un manejador de sesiones personalizado (como ghostff/session), puedes pasar cualquier array de datos de sesión a Tracy y lo mostrará automáticamente por ti. Lo pasas 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 podrá renderizar :(
    Flight::set('flight.content_length', false);
    new TracyExtensionLoader(Flight::app(), [ 'session_data' => Flight::session()->getAll() ]);
}

// routes and other things...

Flight::start();

Latte

Se requiere PHP 8.1+ para esta sección.

Si tienes Latte instalado en tu proyecto, Tracy tiene una integración nativa con Latte para analizar tus plantillas. Simplemente registra la extensión con tu instancia de Latte.


require 'vendor/autoload.php';

$app = Flight::app();

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

    // other configurations...

    // solo agrega 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($template, $data, $block);
});

Awesome-plugins/apm

Documentación de APM de FlightPHP

¡Bienvenido a FlightPHP APM—el entrenador personal de rendimiento de tu aplicación! Esta guía es tu mapa de ruta para configurar, usar y dominar el Monitoreo de Rendimiento de Aplicaciones (APM) con FlightPHP. Ya sea que estés cazando solicitudes lentas o solo quieras geekearte con gráficos de latencia, te tenemos cubierto. ¡Hagamos que tu app sea más rápida, tus usuarios más felices y tus sesiones de depuración una brisa!

Mira una demo del dashboard para el sitio de Flight Docs.

FlightPHP APM

Por qué APM importa

Imagina esto: tu app es un restaurante concurrido. Sin una forma de rastrear cuánto tiempo tardan los pedidos o dónde se atasca la cocina, estás adivinando por qué los clientes se van gruñendo. 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 APM te ayuda a capturar esos problemas antes de que duelan. Es una paz de mente proactiva—menos momentos de “¿por qué esto está roto?”, más victorias de “¡mira qué fluido corre esto!”.

Instalación

Comienza con Composer:

composer require flightphp/apm

Necesitarás:

Bases de datos compatibles

FlightPHP APM actualmente soporta las siguientes bases de datos para almacenar métricas:

Puedes elegir el 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 (p.ej., pdo_sqlite o pdo_mysql).

Comenzando

Aquí tienes tu guía paso a paso hacia la genialidad de APM:

1. Registrar el APM

Coloca esto en tu index.php o un archivo services.php para comenzar el rastreo:

use flight\apm\logger\LoggerFactory;
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
// Debe ser PdoWrapper o PdoQueryCapture de Tracy Extensions
$pdo = new PdoWrapper('mysql:host=localhost;dbname=example', 'user', 'pass', null, true); // <-- True requerido para habilitar el rastreo en el APM.
$Apm->addPdoConnection($pdo);

¿Qué está pasando aquí?

Consejo Pro: Muestreo Si tu app 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 aún te da datos sólidos.

2. Configurarlo

Ejecuta esto para crear tu .runway-config.json:

php vendor/bin/runway apm:init

¿Qué hace esto?

Este proceso también preguntará si quieres ejecutar las migraciones para esta configuración. Si estás configurándolo por primera vez, la respuesta es sí.

¿Por qué dos ubicaciones? Las métricas crudas se acumulan rápido (piensa en logs sin filtrar). El worker las procesa en un destino estructurado para el dashboard. ¡Mantiene las cosas ordenadas!

3. Procesar métricas con el Worker

El worker convierte las métricas crudas en data lista para el dashboard. Ejecútalo una vez:

php vendor/bin/runway apm:worker

¿Qué está haciendo?

Mantenerlo ejecutándose Para apps en vivo, querrás procesamiento continuo. Aquí tienes tus opciones:

¿Por qué molestarse? Sin el worker, tu dashboard está vacío. Es el puente entre logs crudos e insights accionables.

4. Lanzar el Dashboard

Ve los vitales de tu app:

php vendor/bin/runway apm:dashboard

¿Qué es esto?

Personalízalo:

php vendor/bin/runway apm:dashboard --host 0.0.0.0 --port 8080 --php-path=/usr/local/bin/php

¡Abre la URL en tu navegador y explora!

Modo Producción

Para producción, podrías tener que probar unas técnicas para hacer que el dashboard funcione, ya que probablemente hay firewalls y otras medidas de seguridad en su lugar. Aquí hay unas opciones:

¿Quieres un dashboard diferente?

¡Puedes construir tu propio dashboard si quieres! Mira el directorio vendor/flightphp/apm/src/apm/presenter para ideas sobre cómo presentar la data para tu propio dashboard.

Características del Dashboard

El dashboard es tu HQ de APM—aquí está lo que verás:

Extras:

Ejemplo: Una solicitud a /users podría mostrar:

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 dashboard 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 app hacia abajo!

Monitoreo de Base de Datos

Rastrea consultas PDO así:

use flight\database\PdoWrapper;

$pdo = new PdoWrapper('sqlite:/path/to/db.sqlite', null, null, null, true); // <-- True requerido para habilitar el rastreo en el APM.
$Apm->addPdoConnection($pdo);

Lo que Obtienes:

Atención:

Salida de Ejemplo:

Opciones del Worker

Ajusta el worker a tu gusto:

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 rastreo. Puedes usar este ID en tu app para correlacionar logs y métricas. Por instancia, puedes agregar el ID de solicitud a una página de error:

Flight::map('error', function($message) {
    // Obtén el ID de solicitud del header 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)";
});

Actualizando

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 hacerlo 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 versión más reciente.

Nota: Si tu base de datos de APM es grande en tamaño, estas migraciones pueden tardar algo de tiempo en ejecutarse. Podrías querer ejecutar este comando durante horas de bajo pico.

Purgando Datos Antiguos

Para mantener tu base de datos ordenada, puedes purgar datos antiguos. Esto es especialmente útil si estás ejecutando una app ocupada y quieres mantener el tamaño de la base de datos manejable. Puedes hacerlo ejecutando el siguiente comando:

php vendor/bin/runway apm:purge

Esto eliminará todos los datos más antiguos que 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 que 7 días de la base de datos.

Solución de Problemas

¿Atascado? Prueba estos:

Awesome-plugins/tracy

Tracy

Tracy es un increíble manejador de errores que se puede utilizar 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 de Flight con el complemento flightphp/tracy-extensions.

Instalación

Instala con composer. Y realmente 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 comenzar. 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 una matriz de direcciones IP

// Aquí es donde se registrarán los 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 Debugger es visible, entonces la longitud del contenido no puede ser establecida por Flight

    // Esto es específico para la Extensión de 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.

Awesome-plugins/active_record

Flight Active Record

Un registro activo es el mapeo de una entidad de base de datos a un objeto PHP. Hablando claramente, 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 base de código. Consulta 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 mira cómo sucede la magia!

// para sqlite
$database_connection = new PDO('sqlite:test.db'); // esto es solo un ejemplo, probablemente uses una conexión a 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 se puede usar $user->save() aquí o pensará que es una actualización!

echo $user->id; // 2

¡Y así de fácil fue agregar un nuevo usuario! Ahora que hay una fila de usuario en la base de datos, ¿cómo la extraes?

$user->find(1); // encuentra id = 1 en la base de datos y lo retorna.
echo $user->name; // 'Bobby Tables'

¿Y si quieres encontrar todos los usuarios?

$users = $user->findAll();

¿Qué tal con una cierta condición?

$users = $user->like('name', '%mamma%')->findAll();

¿Ves cuánto puede ser divertido esto? ¡Instalémoslo y comencemos!

Instalación

Simplemente instala con Composer

composer require flightphp/active-record 

Uso

Esto puede ser utilizado como una biblioteca independiente o junto con el Flight PHP Framework. Depende completamente de ti.

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 uses una conexión a base de datos real

$User = new User($pdo_connection);

¿No quieres siempre establecer tu conexión a la base de datos en el constructor? Consulta Gestión de Conexiones a la Base de Datos para más ideas.

Registrar como un método en Flight

Si estás utilizando 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 Establece 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 asígnalo 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 pasarlo otros métodos auxiliares para consultar tu tabla.

// encontrar un registro con algunas condiciones previamente
$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)

Retorna true si el registro actual ha sido hidratado (obtenido 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 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 hacer que la clave primaria se genere 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 de la matriz anterior.
        $this->primaryKey = 'uuid';
    }

    protected function beforeInsert(self $self) {
        $self->uuid = uniqid(); // o como sea necesario para 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 utilizar 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 se guardarán recursivamente esas relaciones si han sido definidas, instanciadas y tienen datos modificados para actualizar. (v0.4.0 y superiores)

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 sido cambiados en un registro.

$user->greaterThan('id', 0)->orderBy('id desc')->find();

// nada está "sucio" hasta este punto.

$user->email = 'test@example.com'; // ahora el email se considera "sucio" ya que ha cambiado.
$user->update();
// ahora no hay datos sucios porque han sido actualizados y persistidos en la base de datos

$user->password = password_hash('newpassword'); // ahora esto está sucio
$user->dirty(); // pasar nada limpiará todas las entradas sucias.
$user->update(); // no se actualizará nada porque nada fue capturado como sucio.

$user->dirty([ 'name' => 'something', 'password' => password_hash('a different password') ]);
$user->update(); // tanto el nombre como la contraseña son actualizados.

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 el nombre como la contraseña son actualizados.

isDirty(): boolean (v0.4.0)

Retorna 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

Restablece el registro actual a su estado inicial. Esto es realmente bueno para usar en comportamientos de tipo bucle. Si pasas true, también restablecerá los datos de consulta que se utilizaron 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(); // empezar con un lienzo limpio
    $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 unas pocas de las columnas en una tabla si lo deseas (es más eficiente en tablas muy amplias con muchas columnas).

$user->select('id', 'name')->find();

from(string $table)

¡Técnicamente puedes elegir otra tabla también! ¿Por qué no hacerlo?!

$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 parámetros 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 manera correcta 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 escapar de ello 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, se desplazará, limitará 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 realiza con el método startWrap() y endWrap() o completando el tercer parámetro de la condición después del campo y el 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 utilizando esta biblioteca. Puedes establecer relaciones uno->muchos y uno->uno entre tablas. Esto requiere un poco de configuración adicional en la clase de antemano.

Establecer la matriz $relations no es difícil, pero adivinar la sintaxis correcta puede ser confuso.

protected array $relations = [
    // puedes nombrar la clave como desees. El nombre del ActiveRecord es probablemente 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 hará referencia

        // requerido
        // dependiendo del tipo de relación
        // self::HAS_ONE = la clave externa que referencia la unión
        // self::HAS_MANY = la clave externa 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 se une solo a la clave primaria del "otro" modelo

        // opcional
        [ 'eq' => [ 'client_id', 5 ], 'select' => 'COUNT(*) as count', 'limit' 5 ], // condiciones adicionales que deseas al unir la relación
        // $record->eq('client_id', 5)->select('COUNT(*) as count')->limit(5))

        // opcional
        'back_reference_name' // esto es si deseas referenciar esta relación de vuelta 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 que podamos usarlas muy fácilmente!

$user = new User($pdo_connection);

// encuentra el usuario más reciente.
$user->notNull('id')->orderBy('id desc')->find();

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

// obtener usuario usando la relación:
echo $contact->user->name; // este es el nombre del usuario

¿Bastante genial, eh?

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 simplemente adjuntar al objeto que luego se pasaría a decir un template.

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 normal del objeto.

echo $user->page_view_count;

Eventos

Una característica más super increíble de esta biblioteca es sobre los eventos. Los eventos se activan en ciertos momentos basados en ciertos métodos que llamas. Son 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 sea útil si necesitas manipular la consulta cada vez.

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function beforeFind(self $self) {
        // siempre ejecutar id >= 0 si eso es lo que prefieres
        $self->gte('id', 0); 
    } 
}

afterFind(ActiveRecord $ActiveRecord)

Este probablemente sea más útil si siempre necesitas ejecutar alguna lógica cada vez que se recoge este registro. ¿Necesitas desencriptar algo? ¿Necesitas ejecutar una consulta personalizada cada vez (no muy eficiente, pero bueno)?

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function afterFind(self $self) {
        // desencriptando 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 sea útil si necesitas manipular la consulta cada vez.

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function beforeFindAll(self $self) {
        // siempre ejecutar id >= 0 si eso es lo que prefieres
        $self->gte('id', 0); 
    } 
}

afterFindAll(array<int,ActiveRecord> $results)

Similar a afterFind(), pero ¡puedes hacerlo a todos los registros en su lugar!

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function afterFindAll(array $results) {

        foreach($results as $self) {
            // hacer algo genial como afterFind()
        }
    } 
}

beforeInsert(ActiveRecord $ActiveRecord)

Realmente útil si necesitas establecer algunos valores predeterminados cada vez.

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function beforeInsert(self $self) {
        // establecer algunos valores predeterminados
        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 tengas un caso de uso para cambiar datos después de que se inserte.

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function afterInsert(self $self) {
        // tú decides
        Flight::cache()->set('most_recent_insert_id', $self->id);
        // o lo que sea....
    } 
}

beforeUpdate(ActiveRecord $ActiveRecord)

Realmente útil si necesitas establecer algunos valores predeterminados 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) {
        // establecer algunos valores predeterminados
        if(!$self->updated_date) {
            $self->updated_date = gmdate('Y-m-d');
        }
    } 
}

afterUpdate(ActiveRecord $ActiveRecord)

Tal vez tengas un caso de uso para cambiar datos después de que se actualice.

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function afterInsert(self $self) {
        // tú decides
        Flight::cache()->set('most_recently_updated_user_id', $self->id);
        // o lo que sea....
    } 
}

beforeSave(ActiveRecord $ActiveRecord)/afterSave(ActiveRecord $ActiveRecord)

Esto es útil si deseas que se produzcan eventos tanto cuando se inserten como cuando se actualicen. 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 lo que querrías hacer aquí, pero no hago juicios! ¡Adelante!

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function beforeDelete(self $self) {
        echo 'Él fue un valiente soldado... :cry-face:';
    } 
}

Gestión de Conexiones a la Base de Datos

Al utilizar esta biblioteca, puedes establecer la conexión a la base de datos de varias maneras. Puedes establecer la conexión en el constructor, puedes configurarla 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 deseas evitar establecer siempre una $database_connection cada vez que llamas a un registro activo, ¡hay formas de hacerlo!

// index.php o bootstrap.php
// Establecer 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, no se requieren argumentos!
$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 debido a que puedes inyectar tu conexión con setDatabaseConnection() o $config['connection'], no es tan malo.

Si necesitas refrescar la conexión a la base de datos, por ejemplo, si estás ejecutando un script CLI de larga duración y necesitas refrescar la conexión de vez en cuando, puedes restablecer 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 un 100% de cobertura de pruebas (esto no es una cobertura de pruebas unitarias verdadera, más bien pruebas de integración).

Además, asegúrate de ejecutar composer beautify y composer phpcs para corregir cualquier error de sintaxis.

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">
            &copy; 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 Geniales

Flight es increíblemente extensible. Hay una serie de plugins que se pueden usar para agregar funcionalidad a tu aplicación Flight. Algunos son soportados oficialmente por el Equipo de Flight y otros son bibliotecas micro/lite para ayudarte a comenzar.

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.

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.

Async

Flight ya es un framework rápido, pero agregarle un motor turbo lo hace todo más divertido (¡y desafiante)!

Autorización/Permisos

La autorización y los permisos son cruciales para cualquier aplicación que requiera controles en su lugar para quién puede acceder a qué.

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.

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.

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.

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.

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 envolturas para escribir consultas y otras son ORMs completos.

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 cometerla en tu repositorio de código.

Cola de Trabajos

Las colas de trabajos son realmente útiles para procesar tareas de manera asíncrona. Esto puede ser enviar correos electrónicos, procesar imágenes o cualquier cosa que no necesite hacerse en tiempo real.

Sesión

Las sesiones no son realmente útiles para las API, pero para construir una aplicación web, las sesiones pueden ser cruciales para mantener el estado e información de inicio de sesión.

Plantillas

Las plantillas son el núcleo de cualquier aplicación web con una UI. Hay una serie de motores de plantillas que se pueden usar con Flight.

Integración con WordPress

¿Quieres usar Flight en tu proyecto de WordPress? ¡Hay un plugin práctico para eso!

Contribuir

¿Tienes un plugin que te gustaría compartir? ¡Envía una solicitud de pull 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

Videos y tutoriales

¿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:

Ejemplos aportados por la comunidad:

¿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!

¿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 prerrequisitos básicos antes de que puedas instalar Flight. Específicamente, necesitarás:

  1. Instalar PHP en tu sistema
  2. 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, diseño, dependencias, configuraciones, carga automática, 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.

Instalación Recomendada

Se recomienda altamente comenzar con la aplicación flightphp/skeleton para cualquier proyecto nuevo. La instalación es muy sencilla.

composer create-project flightphp/skeleton my-project/

Esto configurará la estructura de tu proyecto, configurará la carga automática con espacios de nombres, configurará una configuración y proporcionará otras herramientas como Tracy, Extensiones de Tracy y Runway.

Configura tu Servidor Web

Servidor de Desarrollo PHP Integrado

Esta es, con mucho, la forma más simple de comenzar. Puedes usar el servidor integrado para ejecutar tu aplicación e incluso usar SQLite para una base de datos (siempre y cuando sqlite3 esté instalado en tu sistema) y no requerir mucho más. Solo ejecuta el siguiente comando una vez que PHP esté instalado:

php -S localhost:8000
# o con la aplicación skeleton
composer start

Luego abre tu navegador e ingresa a http://localhost:8000.

Si quieres hacer que la raíz de documentos de tu proyecto sea un directorio diferente (Ej: tu proyecto es ~/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 skeleton, esto ya está configurado
composer start

Luego abre tu navegador e ingresa 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 de RewriteEngine On.

Nota: Si quieres proteger todos los archivos del servidor, como un archivo db o 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 cargador automático.
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 skeleton, esto ya está configurado y manejado en tu archivo app/config/routes.php. Los servicios se configuran en app/config/services.php.

Instalando PHP

Si ya tienes php instalado en tu sistema, salta estas instrucciones y ve a la sección de descarga.

macOS

Instalando PHP usando Homebrew

  1. Instala Homebrew (si no está ya instalado):

    • Abre Terminal y ejecuta:
      /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
  2. Instala PHP:

    • Instala la versión más reciente:
      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
  3. 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

Windows 10/11

Instalando PHP manualmente

  1. Descarga PHP:

    • Visita PHP for Windows y descarga la versión más reciente o una específica (p.ej., 7.4, 8.0) como un archivo zip no seguro para hilos.
  2. Extrae PHP:

    • Extrae el archivo zip descargado a C:\php.
  3. Agrega PHP al PATH del sistema:

    • Ve a Propiedades del Sistema > Variables de Entorno.
    • Bajo Variables del sistema, encuentra Path y haz clic en Editar.
    • Agrega la ruta C:\php (o dondequiera que hayas extraído PHP).
    • Haz clic en Aceptar para cerrar todas las ventanas.
  4. Configura PHP:

    • Copia php.ini-development a php.ini.
    • Edita php.ini para configurar PHP según sea necesario (p.ej., estableciendo extension_dir, habilitando extensiones).
  5. Verifica la instalación de PHP:

    • Abre el Símbolo del sistema y ejecuta:
      php -v

Instalando Múltiples Versiones de PHP

  1. Repite los pasos anteriores para cada versión, colocándolas en un directorio separado (p.ej., C:\php7, C:\php8).

  2. Cambia entre versiones ajustando la variable PATH del sistema para apuntar al directorio de la versión deseada.

Ubuntu (20.04, 22.04, etc.)

Instalando PHP usando apt

  1. Actualiza las listas de paquetes:

    • Abre Terminal y ejecuta:
      sudo apt update
  2. Instala PHP:

    • Instala la versión más reciente de PHP:
      sudo apt install php
    • Para instalar una versión específica, por ejemplo, PHP 8.1:
      sudo apt install php8.1
  3. Instala módulos adicionales (opcional):

    • Por ejemplo, para instalar soporte para MySQL:
      sudo apt install php8.1-mysql
  4. Cambia entre versiones de PHP:

    • Usa update-alternatives:
      sudo update-alternatives --set php /usr/bin/php8.1
  5. Verifica la versión instalada:

    • Ejecuta:
      php -v

Rocky Linux

Instalando PHP usando yum/dnf

  1. Habilita el repositorio EPEL:

    • Abre Terminal y ejecuta:
      sudo dnf install epel-release
  2. Instala el repositorio de Remi:

    • Ejecuta:
      sudo dnf install https://rpms.remirepo.net/enterprise/remi-release-8.rpm
      sudo dnf module reset php
  3. 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
  4. 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
  5. Verifica la versión instalada:

    • Ejecuta:
      php -v

Notas Generales

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:

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:

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