uniqueId = uniqid(); $this->mode = $mode; $this->id = $id; $model = new ($this->model()); $this->tagName = $model->tagName; $this->columnNameLabel = $model->columnNameLabel; $this->singularName = $model->singularName; $this->formId = Str::camel($model->tagName) .'Form'; $this->setBtnSubmitText(); if ($this->mode !== 'create' && $this->id) { // Si no es modo 'create', cargamos el registro desde la BD $record = $this->model()::findOrFail($this->id); $this->initializeFormData($record, $mode); } else { // Modo 'create', o sin ID: iniciamos datos vacíos $this->initializeFormData(null, $mode); } } /** * Configura el texto del botón principal de envío, basado en la propiedad $mode. * * @return void */ private function setBtnSubmitText(): void { $this->btnSubmitText = match ($this->mode) { 'create' => 'Crear ' . $this->singularName(), 'edit' => 'Guardar cambios', 'delete' => 'Eliminar ' . $this->singularName(), default => 'Enviar' }; } /** * Retorna el "singularName" definido en el modelo asociado. * Permite también decidir si se devuelve con la primera letra en mayúscula * o en minúscula. * * @param string $type Puede ser 'uppercase' o 'lowercase'. Por defecto, 'lowercase'. * @return string Nombre en singular del modelo, formateado. */ private function singularName($type = 'lowercase'): string { /** @var Model $model */ $model = new ($this->model()); return $type === 'uppercase' ? ucfirst($model->singularName) : lcfirst($model->singularName); } /** * Método del ciclo de vida de Livewire que se llama en cada hidratación. * Puedes disparar eventos o manejar lógica que suceda en cada request * una vez que Livewire 'rehidrate' el componente en el servidor. * * @return void */ public function hydrate(): void { $this->dispatch($this->dispatches()['on-hydrate']); } // ====================================================================== // OPERACIONES CRUD // ====================================================================== /** * Método principal de envío del formulario (submit). Gestiona los flujos * de crear, editar o eliminar un registro dentro de una transacción de BD. * * @return void */ public function onSubmit(): void { DB::beginTransaction(); 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(); $this->handleException('danger', 'Error al eliminar el registro: ' . $e->getMessage()); } } /** * Crea o actualiza un registro en la base de datos, * aplicando validaciones y llamadas a hooks antes y después de guardar. * * @return void * @throws ValidationException */ protected function save(): void { // Validamos los datos, con posibles atributos y mensajes personalizados $validatedData = $this->validate( $this->dynamicRules($this->mode), $this->messages(), $this->attributes() ); // Hook previo (por referencia) $this->beforeSave($validatedData); // Ajustamos/convertimos los datos finales $data = $this->prepareData($validatedData); $record = $this->model()::updateOrCreate(['id' => $this->id], $data); // Hook posterior $this->afterSave($record); // Notificamos éxito $this->handleSuccess('success', $this->singularName('uppercase') . " guardado correctamente."); } /** * Elimina un registro de la base de datos (modo 'delete'), * aplicando validaciones y hooks antes y después de la eliminación. * * @return void * @throws ValidationException */ protected function delete(): void { $this->validate($this->dynamicRules('delete', $this->messages(), $this->attributes())); $record = $this->model()::findOrFail($this->id); // Hook antes de la eliminación $this->beforeDelete($record); $record->delete(); // Hook después de la eliminación $this->afterDelete($record); $this->handleSuccess('warning', $this->singularName('uppercase') . " eliminado."); } // ====================================================================== // HOOKS DE ACCIONES // ====================================================================== /** * Hook que se ejecuta antes de guardar o actualizar un registro. * Puede usarse para ajustar o limpiar datos antes de la operación en base de datos. * * @param array $data Datos validados que se van a guardar. * Se pasa por referencia para permitir cambios. * @return void */ protected function beforeSave(array &$data): void {} /** * Hook que se ejecuta después de guardar o actualizar un registro. * Puede usarse para acciones como disparar eventos, notificaciones a otros sistemas, etc. * * @param mixed $record Instancia del modelo recién creado o actualizado. * @return void */ protected function afterSave($record): void {} /** * Hook que se ejecuta antes de eliminar un registro. * Puede emplearse para validaciones adicionales o limpieza de datos relacionados. * * @param mixed $record Instancia del modelo que se eliminará. * @return void */ protected function beforeDelete($record): void {} /** * Hook que se ejecuta después de eliminar un registro. * Útil para operaciones finales, como remover archivos relacionados o * disparar un evento de "elemento eliminado". * * @param mixed $record Instancia del modelo que se acaba de eliminar. * @return void */ protected function afterDelete($record): void {} // ====================================================================== // MANEJO DE VALIDACIONES Y ERRORES // ====================================================================== /** * Maneja las excepciones de validación (ValidationException). * Asigna los errores al error bag de Livewire y muestra notificaciones. * * @param ValidationException $e Excepción de validación. * @return void */ protected function handleValidationException(ValidationException $e): void { $this->setErrorBag($e->validator->errors()); $this->handleException('danger', 'Error en la validación de los datos.'); $this->dispatch($this->dispatches()['on-failed-validation']); } /** * Maneja las excepciones de base de datos (QueryException). * Incluye casos especiales para claves foráneas y duplicadas. * * @param QueryException $e Excepción de consulta a la base de datos. * @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 excepciones o errores generales, mostrando una notificación al usuario. * * @param string $type Tipo de notificación (por ejemplo, 'success', 'warning', 'danger'). * @param string $message Mensaje que se mostrará en la notificación. * @param string $target Objetivo/área donde se mostrará la notificación ('form', 'index', etc.). * @param int $delay Tiempo en milisegundos que la notificación permanecerá visible. * @return void */ protected function handleException($type, $message, $target = 'form', $delay = 9000): void { $this->dispatchNotification($type, $message, $target, $delay); } /** * Extrae el campo duplicado de un mensaje de error MySQL, para mostrar un mensaje amigable. * * @param string $errorMessage Mensaje de error completo de la base de datos. * @return string Mensaje simplificado indicando cuál campo está duplicado. */ 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 REDIRECCIONAMIENTOS // ====================================================================== /** * Maneja el flujo de notificación y redirección cuando una operación * (guardar, eliminar) finaliza satisfactoriamente. * * @param string $type Tipo de notificación ('success', 'warning', etc.). * @param string $message Mensaje a mostrar. * @return void */ protected function handleSuccess($type, $message): void { $this->dispatchNotification($type, $message, 'index'); $this->redirectRoute($this->getRedirectRoute()); } /** * Envía una notificación al navegador (mediante eventos de Livewire) * indicando el tipo, el mensaje y el destino donde debe visualizarse. * * @param string $type Tipo de notificación (success, danger, etc.). * @param string $message Mensaje de la notificación. * @param string $target Destino para mostrarla ('form', 'index', etc.). * @param int $delay Duración de la notificación en milisegundos. * @return void */ protected function dispatchNotification($type, $message, $target = 'form', $delay = 9000): void { $this->dispatch( $target == 'index' ? 'store-notification' : 'notification', target: $target === 'index' ? $this->targetNotifies()['index'] : $this->targetNotifies()['form'], type: $type, message: $message, delay: $delay ); } // ====================================================================== // RENDERIZACIÓN // ====================================================================== /** * Renderiza la vista Blade asociada a este componente. * Retorna un objeto Illuminate\View\View. * * @return View */ public function render(): View { return view($this->viewPath()); } }