*/ protected $casts = []; // ===================== MÉTODOS ABSTRACTOS ===================== /** * Define el modelo Eloquent asociado con el formulario. * * @return string */ abstract protected function model(): string; /** * Define los campos del formulario. * * @return array */ abstract protected function fields(): array; /** * Retorna los valores por defecto para los campos del formulario. * * @return array Valores predeterminados. */ abstract protected function defaults(): array; /** * Campo que se debe enfocar cuando se abra el formulario. * * @return string */ abstract protected function focusOnOpen(): string; /** * Define reglas de validación dinámicas según el modo del formulario. * * @param string $mode Modo actual del formulario ('create', 'edit', 'delete'). * @return array Reglas de validación. */ abstract protected function dynamicRules(string $mode): array; /** * Devuelve las opciones que se mostrarán en los selectores del formulario. * * @return array Opciones para los campos del formulario. */ abstract protected function options(): array; /** * Retorna la ruta de la vista asociada al formulario. * * @return string Ruta de la vista Blade. */ abstract protected function viewPath(): string; // ===================== VALIDACIONES ===================== protected function attributes(): array { return []; } protected function messages(): array { return []; } // ===================== INICIALIZACIÓN DEL COMPONENTE ===================== /** * Se ejecuta cuando el componente se monta por primera vez. * * Inicializa propiedades y carga datos iniciales. * * @return void */ public function mount(): void { $this->uniqueId = uniqid(); $model = new ($this->model()); $this->tagName = $model->tagName; $this->columnNameLabel = $model->columnNameLabel; $this->singularName = $model->singularName; $this->offcanvasId = 'offcanvas' . ucfirst(Str::camel($model->tagName)); $this->formId = Str::camel($model->tagName) .'Form'; $this->focusOnOpen = "{$this->focusOnOpen()}_{$this->uniqueId}"; $this->loadDefaults(); $this->loadOptions(); } // ===================== INICIALIZACIÓN Y CONFIGURACIÓN ===================== /** * Devuelve los valores por defecto para los campos del formulario. * * @return array Valores por defecto. */ private function loadDefaults(): void { $this->defaultValues = $this->defaults(); } /** * Carga las opciones necesarias para los campos del formulario. * * @return void */ private function loadOptions(): void { foreach ($this->options() as $key => $value) { $this->$key = $value; } } /** * Carga los datos de un modelo específico en el formulario para su edición. * * @param int $id ID del registro a editar. * @return void */ public function loadFormModel(int $id): void { if ($this->loadData($id)) { $this->mode = 'edit'; $this->dispatch($this->getDispatche('refresh-offcanvas')); } } /** * Carga el modelo para confirmar su eliminación. * * @param int $id ID del registro a eliminar. * @return void */ public function loadFormModelForDeletion(int $id): void { if ($this->loadData($id)) { $this->mode = 'delete'; $this->confirmDeletion = false; $this->dispatch($this->getDispatche('refresh-offcanvas')); } } private function getDispatche(string $name): string { $model = new ($this->model()); $dispatches = [ 'refresh-offcanvas' => 'refresh-' . Str::kebab($model->tagName) . '-offcanvas', 'reload-table' => 'reload-bt-' . Str::kebab($model->tagName) . 's', ]; return $dispatches[$name] ?? null; } /** * Carga los datos del modelo según el ID proporcionado. * * @param int $id ID del modelo. * @return bool True si los datos fueron cargados correctamente. */ protected function loadData(int $id): bool { $model = $this->model()::find($id); if ($model) { $data = $model->only(['id', ...$this->fields()]); $this->applyCasts($data); $this->fill($data); return true; } return false; } // ===================== OPERACIONES CRUD ===================== /** * Método principal para enviar el formulario. * * @return void */ public function onSubmit(): void { $this->successProcess = false; $this->validationError = false; if(!$this->mode) $this->mode = 'create'; DB::beginTransaction(); // Iniciar transacción try { if($this->mode === 'delete'){ $this->delete(); }else{ $this->save(); } DB::commit(); } catch (ValidationException $e) { DB::rollBack(); $this->handleValidationException($e); } catch (QueryException $e) { DB::rollBack(); $this->handleDatabaseException($e); } catch (ModelNotFoundException $e) { DB::rollBack(); $this->handleException('danger', 'Registro no encontrado.'); } catch (Exception $e) { DB::rollBack(); // Revertir la transacción si ocurre un error $this->handleException('danger', 'Error al eliminar el registro: ' . $e->getMessage()); } } /** * Guarda o actualiza un registro en la base de datos. * * @return void * @throws ValidationException */ protected function save(): void { // Valida incluyendo atributos personalizados $validatedData = $this->validate( $this->dynamicRules($this->mode), $this->messages(), $this->attributes() ); $this->convertEmptyValuesToNull($validatedData); $this->applyCasts($validatedData); $this->beforeSave($validatedData); $record = $this->model()::updateOrCreate(['id' => $this->id], $validatedData); $this->afterSave($record); $this->handleSuccess('success', ucfirst($this->singularName) . " guardado correctamente."); } /** * Elimina un registro en la base de datos. * * @return void */ protected function delete(): void { $this->validate($this->dynamicRules( 'delete', $this->messages(), $this->attributes() )); $record = $this->model()::findOrFail($this->id); $this->beforeDelete($record); $record->delete(); $this->afterDelete($record); $this->handleSuccess('warning', ucfirst($this->singularName) . " eliminado."); } // ===================== HOOKS DE ACCIONES CRUD ===================== /** * Hook que se ejecuta antes de guardar datos en la base de datos. * * Este método permite realizar modificaciones o preparar los datos antes de ser validados * y almacenados. Es útil para formatear datos, agregar valores calculados o realizar * operaciones previas a la persistencia. * * @param array $data Datos validados que se almacenarán. Se pasan por referencia, * por lo que cualquier cambio aquí afectará directamente los datos guardados. * * @return void */ protected function beforeSave(array &$data): void {} /** * Hook que se ejecuta después de guardar o actualizar un registro en la base de datos. * * Ideal para ejecutar tareas posteriores al guardado, como enviar notificaciones, * registrar auditorías o realizar acciones en otros modelos relacionados. * * @param \Illuminate\Database\Eloquent\Model $record El modelo que fue guardado, conteniendo * los datos actualizados. * * @return void */ protected function afterSave($record): void {} /** * Hook que se ejecuta antes de eliminar un registro de la base de datos. * * Permite validar si el registro puede ser eliminado o realizar tareas previas * como desasociar relaciones, eliminar archivos relacionados o verificar restricciones. * * @param \Illuminate\Database\Eloquent\Model $record El modelo que está por ser eliminado. * * @return void */ protected function beforeDelete($record): void {} /** * Hook que se ejecuta después de eliminar un registro de la base de datos. * * Útil para realizar acciones adicionales tras la eliminación, como limpiar datos relacionados, * eliminar archivos vinculados o registrar eventos de auditoría. * * @param \Illuminate\Database\Eloquent\Model $record El modelo eliminado. Aunque ya no existe en la base de datos, * se conserva la información del registro en memoria. * * @return void */ protected function afterDelete($record): void {} // ===================== MANEJO DE VALIDACIONES Y EXCEPCIONES ===================== /** * Maneja las excepciones de validación. * * Este método captura los errores de validación, los agrega al error bag de Livewire * y dispara un evento para manejar el fallo de validación, útil en formularios modales. * * @param ValidationException $e Excepción de validación capturada. * @return void */ protected function handleValidationException(ValidationException $e): void { $this->setErrorBag($e->validator->errors()); // Notifica al usuario que ocurrió un error de validación $this->handleException('danger', 'Error en la validación de los datos.'); } /** * Maneja las excepciones relacionadas con la base de datos. * * Analiza el código de error de la base de datos y genera un mensaje de error específico * para la situación. También se encarga de enviar una notificación de error. * * @param QueryException $e Excepción capturada durante la ejecución de una consulta. * @return void */ protected function handleDatabaseException(QueryException $e): void { $errorMessage = match ($e->errorInfo[1]) { 1452 => "Una clave foránea no es válida.", 1062 => $this->extractDuplicateField($e->getMessage()), 1451 => "No se puede eliminar el registro porque está en uso.", default => env('APP_DEBUG') ? $e->getMessage() : "Error inesperado en la base de datos.", }; $this->handleException('danger', $errorMessage, 'form', 120000); } /** * Maneja cualquier tipo de excepción general y envía una notificación al usuario. * * @param string $type El tipo de notificación (success, danger, warning). * @param string $message El mensaje que se mostrará al usuario. * @param string $target El contenedor donde se mostrará la notificación (por defecto 'form'). * @param int $delay Tiempo en milisegundos que durará la notificación en pantalla. * @return void */ protected function handleException($type, $message, $target = 'form', $delay = 9000): void { $this->validationError = true; $this->dispatch($this->getDispatche('refresh-offcanvas')); $this->dispatchNotification($type, $message, $target, $delay); } /** * Extrae el nombre del campo duplicado de un error de base de datos MySQL. * * Esta función se utiliza para identificar el campo específico que causó un error * de duplicación de clave única, y genera un mensaje personalizado para el usuario. * * @param string $errorMessage El mensaje de error completo proporcionado por MySQL. * @return string Mensaje de error amigable para el usuario. */ private function extractDuplicateField($errorMessage): string { preg_match("/for key 'unique_(.*?)'/", $errorMessage, $matches); return isset($matches[1]) ? "El valor ingresado para '" . str_replace('_', ' ', $matches[1]) . "' ya está en uso." : "Ya existe un registro con este valor."; } // ===================== NOTIFICACIONES Y ÉXITO ===================== /** * Despacha una notificación tras el éxito de una operación. * * @param string $type Tipo de notificación (success, warning, danger) * @param string $message Mensaje a mostrar. * @return void */ protected function handleSuccess(string $type, string $message): void { $this->successProcess = true; $this->dispatch($this->getDispatche('refresh-offcanvas')); $this->dispatch($this->getDispatche('reload-table')); $this->dispatchNotification($type, $message, 'index'); } /** * Envía una notificación al navegador. * * @param string $type Tipo de notificación (success, danger, etc.) * @param string $message Mensaje de la notificación * @param string $target Destino (form, index) * @param int $delay Duración de la notificación en milisegundos */ protected function dispatchNotification($type, $message, $target = 'form', $delay = 9000): void { $model = new ($this->model()); $this->tagName = $model->tagName; $this->columnNameLabel = $model->columnNameLabel; $this->singularName = $model->singularName; $tagOffcanvas = ucfirst(Str::camel($model->tagName)); $targetNotifies = [ "index" => '#bt-' . Str::kebab($model->tagName) . 's .notification-container', "form" => "#offcanvas{$tagOffcanvas} .notification-container", ]; $this->dispatch( 'notification', target: $target === 'index' ? $targetNotifies['index'] : $targetNotifies['form'], type: $type, message: $message, delay: $delay ); } // ===================== FORMULARIO Y CONVERSIÓN DE DATOS ===================== /** * Convierte los valores vacíos a `null` en los campos que son configurados como `nullable`. * * Esta función verifica las reglas de validación actuales y transforma todos los campos vacíos * en valores `null` si las reglas permiten valores nulos. Es útil para evitar insertar cadenas vacías * en la base de datos donde se espera un valor nulo. * * @param array $data Los datos del formulario que se deben procesar. * @return void */ protected function convertEmptyValuesToNull(array &$data): void { $nullableFields = array_keys(array_filter($this->dynamicRules($this->mode), function ($rules) { return in_array('nullable', (array) $rules); })); foreach ($nullableFields as $field) { if (isset($data[$field]) && $data[$field] === '') { $data[$field] = null; } } } /** * Aplica tipos de datos definidos en `$casts` a los campos del formulario. * * Esta función toma los datos de entrada y los transforma en el tipo de datos esperado según * lo definido en la propiedad `$casts`. Es útil para asegurar que los datos se almacenen en * el formato correcto, como convertir cadenas a números enteros o booleanos. * * @param array $data Los datos del formulario que necesitan ser casteados. * @return void */ protected function applyCasts(array &$data): void { foreach ($this->casts as $field => $type) { if (array_key_exists($field, $data)) { $data[$field] = $this->castValue($type, $data[$field]); } } } /** * Castea un valor a su tipo de dato correspondiente. * * Convierte un valor dado al tipo especificado, manejando adecuadamente los valores vacíos * o nulos. También asegura que valores como `0` o `''` sean tratados correctamente * para evitar errores al almacenarlos en la base de datos. * * @param string $type El tipo de dato al que se debe convertir (`boolean`, `integer`, `float`, `string`, `array`). * @param mixed $value El valor que se debe castear. * @return mixed El valor convertido al tipo especificado. */ protected function castValue($type, $value): mixed { // Convertir valores vacíos o cero a null si corresponde if (is_null($value) || $value === '' || $value === '0' || $value === 0.0) { return match ($type) { 'boolean' => false, // No permitir null en booleanos 'integer' => 0, // Valor por defecto para enteros 'float', 'double' => 0.0, // Valor por defecto para decimales 'string' => "", // Convertir cadena vacía en null 'array' => [], // Evitar null en arrays default => null, // Valor por defecto para otros tipos }; } // Castear el valor si no es null ni vacío return match ($type) { 'boolean' => (bool) $value, 'integer' => (int) $value, 'float', 'double' => (float) $value, 'string' => (string) $value, 'array' => (array) $value, default => $value, }; } // ===================== RENDERIZACIÓN DE VISTA ===================== /** * Renderiza la vista del formulario. * * @return \Illuminate\View\View */ public function render(): View { return view($this->viewPath()); } }