Search by

oswa / formify-php

oswa

A flexible and easy-to-use PHP form builder library

0.0.1 2025-12-21 22:53 UTC

This package is auto-updated.

Last update: 2026-09-25 22:28:25 UTC


README

Latest Version on Packagist Total Downloads

Una librería flexible y fácil de usar para construir formularios HTML en PHP.

Instalación

Puedes instalar el paquete via Composer:

composer require oswa/formify-php

Uso

Uso Básico

use Oswa\FormifyPhp\FormifyPhp;

// Crear un nuevo formulario
$form = new FormifyPhp('POST', '/submit');

// Agregar campos
$form->addField('text', 'name', [
    'label' => 'Nombre',
    'required' => true,
    'placeholder' => 'Tu nombre',
    'class' => 'form-control'
]);

$form->addField('email', 'email', [
    'label' => 'Email',
    'required' => true
]);

$form->addField('textarea', 'message', [
    'label' => 'Mensaje',
    'rows' => 5
]);

// Agregar botón de submit
$form->submitButton([
    'text' => 'Enviar',
    'class' => 'btn btn-primary'
]);

// O simplemente con string
$form->submitButton('Enviar');

// Renderizar el formulario
echo $form->render();

Personalización Global de Labels

Puedes customizar cómo se renderizan todos los labels del formulario:

$form = new FormifyPhp('POST', '/submit');

// Personalizar el renderizado de labels para todos los campos
$form->setRenderLabel(function ($field) {
    return sprintf(
        '<label for="%s" class="font-bold text-blue-600">%s%s</label>',
        $field['name'],
        $field['label'],
        $field['required'] ? ' *' : ''
    );
});

$form->addField('text', 'name', ['label' => 'Nombre', 'required' => true]);

Personalización por Campo Individual

Cada campo puede tener su propio renderizado completamente personalizado usando customRenderer dentro de las opciones:

$form = new FormifyPhp('POST', '/submit');

// Campo con diseño personalizado (input dentro del label)
$form->addField('email', 'email', [
    'label' => 'Correo Electrónico',
    'placeholder' => 'tu@email.com',
    'required' => true,
    'customRenderer' => function ($field) {
        return sprintf(
            '<div class="mb-4">' .
            '<label class="block">' .
            '<span class="text-gray-700">%s</span>' .
            '<input type="%s" name="%s" class="mt-1 block w-full" />' .
            '</label>' .
            '</div>',
            htmlspecialchars($field['options']['label']),
            $field['type'],
            $field['name']
        );
    }
]);

Wrapper Personalizado para Campos

Define cómo se envuelve cada campo globalmente:

$form->setFieldWrapper(function ($field) {
    $html = '<div class="mb-6">' . PHP_EOL;
    $html .= $field['label'];
    $html .= $field['input'];
    
    // 'error' es el mensaje en crudo; 'errorHtml' ya viene pasado por setRenderErrors()
    if ($field['error']) {
        $html .= sprintf('<p class="text-red-500 text-sm mt-1">%s</p>', $field['error']);
    }
    
    $html .= '</div>' . PHP_EOL;
    return $html;
});

Ejemplo con Tailwind CSS

$form = new FormifyPhp('POST', '/submit');

$form->setAttributes(['class' => 'space-y-6']);

$form->setRenderLabel(function ($field) {
    return sprintf(
        '<label for="%s" class="block text-sm font-medium text-gray-700 mb-2">%s%s</label>',
        $field['name'],
        $field['label'],
        $field['required'] ? ' <span class="text-red-500">*</span>' : ''
    );
});

$form->addField('text', 'name', [
    'label' => 'Nombre completo',
    'required' => true,
    'class' => 'mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500'
]);

// Campo con custom renderer
$form->addField('email', 'email', [
    'label' => 'Email',
    'required' => true,
    'class' => 'mt-1 block w-full rounded-md border-gray-300',
    'customRenderer' => function ($field) {
        return sprintf(
            '<label class="flex items-center gap-2">' .
            '<svg class="w-5 h-5">...</svg>' .
            '%s' .
            '<input type="email" name="%s" class="%s" />' .
            '</label>',
            $field['options']['label'],
            $field['name'],
            $field['options']['class']
        );
    }
]);

// Submit button con custom renderer
$form->submitButton([
    'text' => 'Enviar',
    'class' => 'w-full bg-indigo-600 text-white rounded-lg px-6 py-3',
    'customRenderer' => function ($field) {
        return sprintf(
            '<button type="submit" class="%s hover:bg-indigo-700">' .
            '<span>%s</span>' .
            '</button>',
            $field['options']['class'] ?? '',
            $field['text']
        );
    }
]);

echo $form->render();

Manejo de Errores

$form = new FormifyPhp('POST', '/submit');
$form->addField('email', 'email', ['label' => 'Email']);

// Establecer errores de validación
$form->setErrors([
    'email' => 'El email no es válido'
]);

echo $form->render();

Tipos de Campo

Cualquier tipo de input HTML5 (text, email, tel, url, password, number, date, range, color…), más textarea, select, checkbox, radio y file.

Atributos HTML. Toda clave de options que no sea de la librería se emite como atributo, así que min, max, step, accept, pattern o autocomplete funcionan sin más. Para nombres que chocan con claves reservadas, o para data-* y aria-*, usa attributes:

$form->addField('number', 'edad', [
    'label' => 'Edad',
    'min'   => 18,
    'max'   => 99,
    'step'  => 1,
]);

$form->addField('text', 'sku', [
    'label'      => 'SKU',
    'attributes' => ['data-role' => 'sku', 'aria-describedby' => 'sku-help'],
]);

Las claves reservadas son: label, options, choices, value, checked, inline, groupClass, choiceClass, attributes, text y meta. Usa meta para tus propios datos (textos de ayuda, clases del wrapper…): llega a los renderizadores pero nunca al HTML.

Checkbox y radio. Con choices se genera un grupo; sin él, un checkbox suelto:

// Checkbox suelto: envía value (por defecto "1") cuando está marcado
$form->addField('checkbox', 'terms', [
    'label'   => 'Acepto los términos',
    'checked' => true,
    'rules'   => ['accepted'],
]);

// Grupo de checkboxes: envía interests[] como array
$form->addField('checkbox', 'interests', [
    'label'   => 'Temas',
    'choices' => ['forms' => 'Formularios', 'validation' => 'Validación'],
    'value'   => ['forms'],          // marcados
    'rules'   => ['required', 'array'],
]);

// Grupo de radios: un único valor
$form->addField('radio', 'plan', [
    'label'   => 'Plan',
    'choices' => ['free' => 'Gratis', 'pro' => 'Pro'],
    'value'   => 'pro',
    'inline'  => true,               // en línea en lugar de apilados
    'rules'   => ['required', 'in:free,pro'],
]);

Cada opción del grupo recibe su propio id (plan_pro) y su <label for>. Puedes darles clases con groupClass (el contenedor) y choiceClass (cada opción).

Grupos obligatorios. No pongas 'required' => true en un grupo de checkboxes: el atributo se emitiría en cada casilla y en HTML eso obliga a marcarlas todas. Basta con la regla required, que ya marca el campo como obligatorio en el label. En los grupos de radios sí puedes usarlo, porque el navegador lo interpreta sobre el grupo entero.

Campos obligatorios. El label muestra el campo como obligatorio si lleva el atributo required o si alguna regla lo exige (required, accepted), así que no hace falta declararlo dos veces. Las condicionales (required_if, required_with…) no cuentan, porque dependen de otros campos. Puedes consultarlo con $form->isRequired('campo').

Select múltiple y archivos. Con multiple, el name pasa a campo[] automáticamente, y un campo file añade enctype="multipart/form-data" al formulario:

$form->addField('select', 'paises', [
    'label'    => 'Países',
    'multiple' => true,
    'choices'  => ['pe' => 'Perú', 'mx' => 'México'],
    'value'    => ['mx'],
]);

$form->addField('file', 'adjuntos', [
    'label'    => 'Adjuntos',
    'accept'   => '.pdf,.png',
    'multiple' => true,
]);

Las reglas de validación de archivos (file, image, mimes, dimensions) no están incluidas: el campo se renderiza, pero valida su contenido por tu cuenta.

Validación

Las reglas se declaran junto al campo y usan la misma sintaxis que Laravel:

$form->addField('text', 'name', [
    'label'    => 'Nombre',
    'rules'    => ['required', 'min:3', 'max:100'],
    'messages' => ['min' => 'El nombre debe tener al menos 3 caracteres.'],
]);

if ($form->validate($form->request()->getPost())) {
    // datos válidos
}

$form->getErrors(); // ['name' => 'El nombre debe tener al menos 3 caracteres.']

El validador también se puede usar por separado, con la API de Illuminate\Validation\Validator:

use Oswa\FormifyPhp\Validation\Validator;

$validator = Validator::make($data, [
    'name'        => 'required|min:3',
    'email'       => 'required|email',
    'password'    => 'required|min:8|confirmed',
    'edad'        => 'nullable|integer|between:18,120',
    'items.*.sku' => 'required|alpha_num',
], [
    'email.required' => 'Necesitamos tu correo.',
]);

$validator->passes();          // bool
$validator->fails();           // bool
$validator->errors();          // MessageBag
$validator->errors()->first('email');
$validator->validated();       // solo los campos con reglas que pasaron
$validator->validate();        // devuelve los datos validados o lanza ValidationException

Soporta notación de puntos (user.email), comodines (items.*.sku), los modificadores nullable, sometimes y bail, mensajes personalizados por campo o por regla, nombres legibles de campos, stopOnFirstFailure() y sometimes().

Reglas incluidas: accepted, accepted_if, active_url, after, after_or_equal, alpha, alpha_dash, alpha_num, array, before, before_or_equal, between, boolean, confirmed, date, date_equals, date_format, declined, different, digits, digits_between, distinct, doesnt_end_with, doesnt_start_with, email, ends_with, filled, gt, gte, in, in_array, integer, ip, ipv4, ipv6, json, lowercase, lt, lte, mac_address, max, min, not_in, not_regex, numeric, present, prohibited, regex, required, required_if, required_unless, required_with, required_with_all, required_without, required_without_all, same, size, starts_with, string, timezone, uppercase, url, uuid.

No se incluyen las reglas que dependen de una base de datos (exists, unique) ni de archivos subidos (file, image, mimes, dimensions).

Reglas propias

// Con un closure, igual que en Laravel
$validator = Validator::make($data, [
    'sku' => [function (string $attribute, mixed $value, Closure $fail) {
        if (!str_starts_with($value, 'SKU-')) {
            $fail('El campo :attribute debe empezar por SKU-.');
        }
    }],
]);

// O registrando una regla reutilizable
$validator->extend('par', fn($attribute, $value) => $value % 2 === 0, 'El campo :attribute debe ser par.');

Idiomas

Los mensajes por defecto están en inglés. También viene incluido el español, y puedes registrar cualquier otro idioma. Las claves son las mismas que usa Laravel, así que puedes reutilizar un lang/es/validation.php existente:

use Oswa\FormifyPhp\Validation\Translator;

// Cambiar el idioma de todo el formulario
$form->setLocale('es');

// O directamente sobre el validador
$validator = Validator::make($data, $rules, [], [], new Translator('es'));

// Registrar o sobrescribir mensajes de cualquier idioma
$translator = new Translator('fr');
$translator->addMessages('fr', [
    'validation' => [
        'required'   => 'Le champ :attribute est obligatoire.',
        'attributes' => ['email' => 'adresse e-mail'],
    ],
]);

$validator = Validator::make($data, $rules, [], [], $translator);

Protección CSRF

Los formularios que no usan GET incluyen automáticamente un token CSRF oculto. El token se genera con random_bytes(), se guarda en la sesión y se compara con hash_equals().

session_start(); // o deja que la librería la inicie al renderizar

$form = new FormifyPhp('POST', '/submit');
$form->addField('email', 'email', ['label' => 'Email', 'rules' => ['required', 'email']]);
$form->submitButton('Enviar');

if ($form->isSubmitted()) {
    if (!$form->verifyCsrf()) {
        // Token ausente, inválido o sesión expirada
        exit('Token CSRF inválido');
    }

    if ($form->validate($form->request()->getPost())) {
        // Datos válidos: guardar, enviar email, redirigir...
    }
}

echo $form->render(); // el <input type="hidden" name="csrf_token"> ya viene incluido

El token se rota tras cada verificación correcta. Puedes ajustar el comportamiento:

$form->disableCsrf();                        // desactivar la protección
$form->antiCsrf()->setTokenName('_token');   // cambiar el nombre del campo
$form->antiCsrf()->regenerateToken();        // forzar un token nuevo

Uso fuera de una petición HTTP

Request y AntiCsrf se construyen solo cuando se usan, así que puedes generar formularios en tests o en CLI. Para simular una petición, inyéctala:

use Oswa\FormifyPhp\Http\Request;

$form->setRequest(new Request('POST', ['email' => 'ana@test.com']));

$form->isSubmitted();               // true
$form->request()->getPost('email'); // 'ana@test.com'

Características

  • ✅ Cero dependencias de terceros — solo PHP y sus extensiones estándar
  • ✅ Constructor de formularios fluido y encadenable
  • ✅ Validación con la API de Laravel: misma sintaxis de reglas, notación de puntos y comodines
  • ✅ Protección CSRF integrada, sin dependencias HTTP externas
  • ✅ Mensajes en inglés por defecto, con español incluido y ampliable a cualquier idioma
  • ✅ Soporte para todos los tipos de campos HTML5, incluidos checkbox, radio y file
  • ✅ Atributos HTML libres (min, max, step, accept, data-*, aria-*)
  • ✅ Renderizado personalizable a nivel global y por campo
  • ✅ Labels personalizables - puedes poner el input dentro del label o cualquier diseño
  • ✅ Wrapper personalizable para controlar cómo se envuelve cada campo
  • ✅ customRenderer en options - API limpia y consistente
  • ✅ Compatible con Tailwind CSS, Bootstrap y cualquier framework CSS
  • ✅ Validación de campos y mensajes de error
  • ✅ Clases CSS completamente personalizables
  • ✅ Botones de submit con atributos y renderers personalizados

Requisitos

  • PHP 8.1 o superior
  • ext-session (solo si usas la protección CSRF)

Testing

Los tests están escritos con Pest:

composer test

O ejecutar Pest directamente:

./vendor/bin/pest

Changelog

Por favor ver CHANGELOG para más información sobre cambios recientes.

Contribuir

Las contribuciones son bienvenidas. Por favor ver CONTRIBUTING para más detalles.

Seguridad

Si descubres algún problema de seguridad, por favor envía un email a oswa@example.com.

Créditos

Licencia

La licencia MIT (MIT). Por favor ver License File para más información.