26. Validación de las fuentes de conocimiento
26: Validación de las Fuentes de Conocimiento
En este tutorial, implementaremos la validación completa para las fuentes de conocimiento. Aprenderemos a definir reglas de validación condicionales según el tipo de fuente, manejar archivos y almacenarlos correctamente en el servidor.
1. Configurar la Validación en el Request
1.1. Definir las Reglas de Validación
app/Http/Requests/KnowledgeSourceRequest.php
<?php namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rule; class KnowledgeSourceRequest extends FormRequest { /** * Determina si el usuario está autorizado para hacer esta petición. */ public function authorize(): bool { return true; } /** * Obtiene las reglas de validación que se aplican a la petición. */ public function rules(): array { return [ // Campo nombre: obligatorio, string, máximo 255 caracteres 'name' => 'required|string|max:255', // Campo tipo: obligatorio, string, debe ser 'pdf' o 'website' 'type' => ['required', 'string', Rule::in(['pdf', 'website'])], // Campo file (PDF): requerido solo si type es 'pdf' 'file' => 'required_if:type,pdf|file|mimes:pdf|max:10240', // Campo url (Website): requerido solo si type es 'website' 'url' => 'required_if:type,website|url|max:2048', ]; } /** * Obtiene los mensajes de error personalizados. */ public function messages(): array { return [ 'name.required' => 'El nombre de la fuente es obligatorio', 'name.string' => 'El nombre debe ser texto', 'name.max' => 'El nombre no puede tener más de 255 caracteres', 'type.required' => 'El tipo de fuente es obligatorio', 'type.in' => 'El tipo de fuente seleccionado no es válido', 'file.required_if' => 'Debes seleccionar un archivo PDF', 'file.file' => 'El campo debe ser un archivo válido', 'file.mimes' => 'El archivo debe ser un PDF', 'file.max' => 'El PDF no puede ser mayor a 10MB', 'url.required_if' => 'La URL del website es obligatoria', 'url.url' => 'La URL debe ser válida', 'url.max' => 'La URL no puede tener más de 2048 caracteres', ]; } }
1.2. Explicación de las Reglas
| Regla | Descripción |
|---|---|
required|string|max:255 | Obligatorio, texto, máximo 255 caracteres |
required|in:pdf,website | Obligatorio, debe ser 'pdf' o 'website' |
required_if:type,pdf | Obligatorio solo si type es 'pdf' |
file|mimes:pdf|max:10240 | Debe ser un archivo PDF de máximo 10MB |
required_if:type,website | Obligatorio solo si type es 'website' |
url|max:2048 | Debe ser una URL válida de máximo 2048 caracteres |
2. Procesar el Formulario en el Controlador
2.1. Método Store Actualizado
app/Http/Controllers/KnowledgeSourceController.php
<?php namespace App\Http\Controllers; use App\Models\Chatbot; use App\Models\KnowledgeSource; use App\Http\Requests\KnowledgeSourceRequest; use Illuminate\Support\Facades\Storage; class KnowledgeSourceController extends Controller { /** * Almacena una nueva fuente de conocimiento. */ public function store(KnowledgeSourceRequest $request, Chatbot $chatbot) { // 1. Autorizar la acción $this->authorize('update', $chatbot); // 2. Obtener los datos validados $validated = $request->validated(); // 3. Crear una nueva instancia de KnowledgeSource $knowledgeSource = new KnowledgeSource(); $knowledgeSource->name = $validated['name']; $knowledgeSource->type = $validated['type']; $knowledgeSource->status = 'pending'; // 4. Procesar según el tipo if ($validated['type'] === 'pdf') { // Guardar el archivo PDF $file = $request->file('file'); $path = $file->store("knowledge-sources/{$chatbot->id}/pdfs", 'public'); $knowledgeSource->path = $path; } if ($validated['type'] === 'website') { // Guardar la URL del website $knowledgeSource->path = $validated['url']; } // 5. Asociar con el chatbot y guardar $chatbot->knowledgeSources()->save($knowledgeSource); // 6. Disparar job para procesar la fuente (opcional) // dispatch(new ProcessKnowledgeSource($knowledgeSource)); // 7. Redireccionar con mensaje de éxito return redirect() ->route('chatbots.knowledge-sources.index', $chatbot) ->with('success', 'Fuente de conocimiento creada exitosamente.'); } }
2.2. Explicación del Código
Autorización: Verificamos que el usuario tenga permisos para actualizar el chatbot
Datos Validados: Obtenemos los datos ya validados del request
Creación: Instanciamos un nuevo KnowledgeSource
Procesamiento por Tipo:
PDF: Guardamos el archivo en storage y asignamos la ruta
Website: Guardamos la URL directamente
Asociación: Guardamos la fuente asociada al chatbot
Job (opcional): Disparar un job para procesamiento en background
Redirección: Volvemos al listado con mensaje de éxito
3. Manejo de Errores en el Frontend
3.1. Mostrar Errores en el Modal
resources/js/Components/KnowledgeSources/KnowledgeSourceCreateModal.vue
<template> <DialogModal :show="show" @close="handleClose"> <template #title> <div class="flex items-center"> <BookIcon class="w-6 h-6 mr-2 text-indigo-600" /> <span>Agregar Fuente de Conocimiento</span> </div> </template> <template #content> <div class="space-y-4"> <!-- Mostrar errores generales --> <div v-if="form.hasErrors" class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-md p-4"> <div class="flex"> <div class="flex-shrink-0"> <svg class="h-5 w-5 text-red-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"> <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z" clip-rule="evenodd" /> </svg> </div> <div class="ml-3"> <h3 class="text-sm font-medium text-red-800 dark:text-red-200"> Por favor corrige los siguientes errores: </h3> <div class="mt-2 text-sm text-red-700 dark:text-red-300"> <ul class="list-disc pl-5 space-y-1"> <li v-for="error in allErrors" :key="error"> {{ error }} </li> </ul> </div> </div> </div> </div> <!-- Formulario --> <KnowledgeSourceForm :form="form" /> </div> </template> <template #footer> <div class="flex justify-between w-full"> <SecondaryButton @click="handleClose"> Cerrar </SecondaryButton> <PrimaryButton @click="handleSubmit" :disabled="form.processing" > <span v-if="form.processing" class="flex items-center"> <svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> </svg> Guardando... </span> <span v-else>Guardar</span> </PrimaryButton> </div> </template> </DialogModal> </template> <script setup> import DialogModal from '@/Components/DialogModal.vue'; import PrimaryButton from '@/Components/PrimaryButton.vue'; import SecondaryButton from '@/Components/SecondaryButton.vue'; import BookIcon from '@/Components/Icons/BookIcon.vue'; import KnowledgeSourceForm from './KnowledgeSourceForm.vue'; import { useForm } from '@inertiajs/vue3'; import { watch, computed } from 'vue'; const props = defineProps({ show: { type: Boolean, default: false, }, chatbotId: { type: String, required: true, }, }); const emit = defineEmits(['close']); // Estado del formulario const form = useForm({ name: '', type: 'pdf', pdf_file: null, website_url: '', }); // Obtener todos los errores como array const allErrors = computed(() => { if (!form.errors) return []; return Object.values(form.errors).flat(); }); // Manejar el envío const handleSubmit = () => { const data = new FormData(); data.append('name', form.name); data.append('type', form.type); if (form.type === 'pdf' && form.pdf_file) { data.append('file', form.pdf_file); } if (form.type === 'website' && form.website_url) { data.append('url', form.website_url); } form.post(route('chatbots.knowledge-sources.store', props.chatbotId), { data: data, forceFormData: true, preserveScroll: true, onSuccess: () => { handleClose(); form.reset(); }, onError: (errors) => { console.error('Errores de validación:', errors); }, }); }; const handleClose = () => { emit('close'); }; watch( () => props.show, (newValue) => { if (!newValue) { form.reset(); } } ); </script>
4. Validación en el Frontend (Opcional)
4.1. Validación Básica antes del Envío
// En el componente KnowledgeSourceForm.vue const validateForm = () => { const errors = {}; if (!form.name || form.name.trim() === '') { errors.name = 'El nombre es obligatorio'; } if (form.type === 'pdf' && !form.pdf_file) { errors.pdf_file = 'Debes seleccionar un archivo PDF'; } if (form.type === 'website' && !form.website_url) { errors.website_url = 'La URL es obligatoria'; } if (form.type === 'website' && form.website_url) { try { new URL(form.website_url); } catch { errors.website_url = 'La URL no es válida'; } } return errors; }; // En el modal const handleSubmit = () => { // Validación en frontend const errors = validateForm(); if (Object.keys(errors).length > 0) { form.errors = errors; return; } // Si pasa la validación, enviar // ... código de envío };
5. Visualización de Errores en el Formulario
5.1. Componente InputError
resources/js/Components/InputError.vue
<template> <div v-if="message" class="mt-2 text-sm text-red-600 dark:text-red-400"> {{ message }} </div> </template> <script setup> defineProps({ message: { type: String, default: null, }, }); </script>
5.2. Campos con Errores
<template> <div class="col-span-6 sm:col-span-4"> <InputLabel for="name" value="Nombre" /> <TextInput id="name" v-model="form.name" type="text" class="mt-1 block w-full" :class="{ 'border-red-500 focus:border-red-500 focus:ring-red-500': form.errors.name }" placeholder="Ej: Manual de Usuario" /> <InputError :message="form.errors.name" class="mt-2" /> </div> </template>
6. Verificación en la Base de Datos
6.1. Estructura de la Tabla
knowledge_sources ( id UUID PRIMARY KEY, chatbot_id UUID FOREIGN KEY, name VARCHAR(255) NOT NULL, type ENUM('pdf', 'website') NOT NULL, path VARCHAR(2048) NOT NULL, status ENUM('pending', 'processing', 'processed', 'failed') DEFAULT 'pending', extracted_content LONGTEXT NULL, metadata JSON NULL, created_at TIMESTAMP, updated_at TIMESTAMP );
6.2. Verificación de Datos Guardados
// En el controlador, después de guardar dd($knowledgeSource->toArray()); // Resultado esperado: [ 'id' => 'uuid...', 'chatbot_id' => 'uuid...', 'name' => 'Mi Fuente', 'type' => 'pdf', 'path' => 'knowledge-sources/uuid/pdfs/archivo.pdf', 'status' => 'pending', 'extracted_content' => null, 'metadata' => null, 'created_at' => '2024-01-01 12:00:00', 'updated_at' => '2024-01-01 12:00:00', ]
7. Mejores Prácticas de Validación
7.1. Validación en el Backend
✅ Siempre validar en el servidor: La validación del lado del servidor es imprescindible para la seguridad
✅ Usar FormRequest: Centraliza las reglas de validación en clases dedicadas
✅ Mensajes personalizados: Proporciona mensajes claros y útiles
✅ Validación condicional: Usa required_if, required_with, etc.
✅ Sanitización: Los datos se limpian automáticamente con los castings del modelo
7.2. Validación en el Frontend
✅ Validación en tiempo real: Mejora la experiencia del usuario
✅ Feedback inmediato: Muestra errores mientras el usuario escribe
✅ Estilos visuales: Resalta los campos con errores
✅ Prevención de envíos: Bloquea el envío hasta que el formulario sea válido
8. Resumen del Flujo de Validación
8.1. Flujo Completo
Usuario completa el formulario
Frontend valida los campos (opcional)
Usuario hace clic en "Guardar"
Datos se envían al servidor vía Inertia
Backend valida con
KnowledgeSourceRequestSi hay errores: Se devuelven al frontend
Frontend muestra los errores en el formulario
Usuario corrige los errores
Se repite el proceso hasta que sea válido
Datos válidos: Se guardan en la base de datos
Redirección con mensaje de éxito
8.2. Posibles Errores y Soluciones
| Error | Causa | Solución |
|---|---|---|
| "El campo file es obligatorio" | No se subió el PDF | Asegurar que el archivo se seleccione correctamente |
| "El archivo debe ser un PDF" | Se subió otro tipo de archivo | Verificar la extensión del archivo |
| "La URL debe ser válida" | URL mal formada | Usar un validador de URL adecuado |
| "El nombre ya existe" | Nombre duplicado | Agregar validación de unicidad |
9. Código Completo del Request
Aquí tienes el código completo para copiar:
app/Http/Requests/KnowledgeSourceRequest.php
<?php namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rule; class KnowledgeSourceRequest extends FormRequest { public function authorize(): bool { return true; } public function rules(): array { return [ 'name' => 'required|string|max:255', 'type' => ['required', 'string', Rule::in(['pdf', 'website'])], 'file' => 'required_if:type,pdf|file|mimes:pdf|max:10240', 'url' => 'required_if:type,website|url|max:2048', ]; } public function messages(): array { return [ 'name.required' => 'El nombre de la fuente es obligatorio', 'name.string' => 'El nombre debe ser texto', 'name.max' => 'El nombre no puede tener más de 255 caracteres', 'type.required' => 'El tipo de fuente es obligatorio', 'type.in' => 'El tipo de fuente seleccionado no es válido', 'file.required_if' => 'Debes seleccionar un archivo PDF', 'file.file' => 'El campo debe ser un archivo válido', 'file.mimes' => 'El archivo debe ser un PDF', 'file.max' => 'El PDF no puede ser mayor a 10MB', 'url.required_if' => 'La URL del website es obligatoria', 'url.url' => 'La URL debe ser válida', 'url.max' => 'La URL no puede tener más de 2048 caracteres', ]; } }
10. Resumen
En este tutorial hemos implementado la validación completa para las fuentes de conocimiento:
✅ Reglas de validación con condiciones según el tipo
✅ Mensajes personalizados para cada campo
✅ Procesamiento de archivos y URLs en el controlador
✅ Manejo de errores en el frontend
✅ Visualización de errores en el formulario
✅ Almacenamiento seguro de archivos
✅ Mejores prácticas de validación
¡Excelente trabajo! Ahora tienes un sistema robusto de validación para tus fuentes de conocimiento.
Comentarios
Publicar un comentario