Rev 612 | Rev 614 | Ir a la última revisión | Autoría | Comparar con el anterior | Ultima modificación | Ver Log |
<?phpdeclare(strict_types = 1);namespace LeadersLinked\Command;use Symfony\Component\Console\Command\Command;use Symfony\Component\Console\Input\InputInterface;use Symfony\Component\Console\Output\OutputInterface;use Laminas\Db\Adapter\AdapterInterface;use Laminas\Log\LoggerInterface;use LeadersLinked\Mapper\EmailMapper;use PHPMailer\PHPMailer\PHPMailer;use LeadersLinked\Model\Email;use Laminas\Mvc\I18n\Translator;use LeadersLinked\Cache\CacheInterface;use Laminas\Db\Adapter\Exception\InvalidQueryException; // Importar para manejar excepciones de BDclass ProcessQueueEmailCommand extends Command{/**** @var \Laminas\Db\Adapter\AdapterInterface*/private $adapter;/**** @var \LeadersLinked\Cache\CacheInterface*/private $cache;/**** @var \Laminas\Log\LoggerInterface*/private $logger;/**** @var array*/private $config;/**** @var \Laminas\Mvc\I18n\Translator*/private $translator;/**** @param \Laminas\Db\Adapter\AdapterInterface $adapter* @param \LeadersLinked\Cache\CacheInterface $cache* @param \Laminas\Log\LoggerInterface $logger* @param array $config* @param \Laminas\Mvc\I18n\Translator $translator*/public function __construct($adapter, $cache, $logger, $config, $translator){$this->adapter = $adapter;$this->cache = $cache;$this->logger = $logger;$this->config = $config;$this->translator = $translator;parent::__construct();}protected function execute($input, $output){$sandbox = $this->config['leaderslinked.runmode.sandbox'];if ($sandbox) {$batch_size = $this->config['leaderslinked.email.sandbox_batch_size'];$from_address = $this->config['leaderslinked.email.sandbox_from_address'];$from_name = $this->config['leaderslinked.email.sandbox_from_name'];$host = $this->config['leaderslinked.email.sandbox_host'];$port = $this->config['leaderslinked.email.sandbox_port'];$username = $this->config['leaderslinked.email.sandbox_username'];$password = $this->config['leaderslinked.email.sandbox_password'];} else {$batch_size = $this->config['leaderslinked.email.production_batch_size'];$from_address = $this->config['leaderslinked.email.production_from_address'];$from_name = $this->config['leaderslinked.email.production_from_name'];$host = $this->config['leaderslinked.email.production_host'];$port = $this->config['leaderslinked.email.production_port'];$username = $this->config['leaderslinked.email.production_username'];$password = $this->config['leaderslinked.email.production_password'];}$this->logger->info('Inicio del proceso de la cola de Email');$output->writeln('<info>Iniciando el procesamiento de la cola de emails...</info>');$emailCompleted = 0;$emailError = 0; // Para correos con errores que se descartan$emailRetried = 0; // Para correos que se reintentan$emailMapper = EmailMapper::getInstance($this->adapter);$emails = $emailMapper->fetchBatch($batch_size);if (!$emails) {$this->logger->info('No hay emails en la cola');$output->writeln('<comment>No hay emails en la cola para procesar.</comment>');return Command::SUCCESS;}foreach ($emails as $email) {// Iniciar transacción para cada email para asegurar atomicidad$this->adapter->getDriver()->getConnection()->beginTransaction();try {$content = json_decode($email->content, true);// Validación básica de los datos del emailif (json_last_error() !== JSON_ERROR_NONE) {throw new \RuntimeException('Error al decodificar el contenido JSON del email: ' . json_last_error_msg());}if (!isset($content['to_address'], $content['subject'], $content['message'])) {throw new \RuntimeException('Contenido del email JSON incompleto: faltan datos esenciales.');}$to_address = $content['to_address'];$to_name = $content['to_name'] ?? ''; // Valor por defecto$cc = $content['cc'] ?? []; // Valor por defecto$bcc = $content['bcc'] ?? []; // Valor por defecto$subject = $content['subject'];$message = $content['message'];// Normalización de codificación$subject = $this->normalizeEncoding($subject);$message = $this->normalizeEncoding($message);$phpMailer = new PHPMailer(true); // true para habilitar excepciones$phpMailer->isSMTP();$phpMailer->SMTPDebug = 0; // 0 para no depurar, 2 para mensajes detallados en desarrollo$phpMailer->Host = $host;$phpMailer->SMTPAuth = true;$phpMailer->SMTPSecure = 'tls'; // o 'ssl' si el puerto es 465$phpMailer->Username = $username;$phpMailer->Password = $password;$phpMailer->Port = $port;$phpMailer->CharSet = 'UTF-8';$phpMailer->setFrom($from_address, $from_name);$phpMailer->addAddress($to_address, $to_name);foreach ($cc as $address => $name) {$phpMailer->addCC($address, $name);}foreach ($bcc as $address => $name) {$phpMailer->addBCC($address, $name);}$phpMailer->IsHTML(true);$phpMailer->Subject = $subject;$phpMailer->Body = $message;$phpMailer->AltBody = strip_tags($message); // Versión de texto plano$phpMailer->send();// Si el envío es exitoso$emailCompleted++;$email->status = Email::STATUS_COMPLETED;$email->tried = $email->tried + 1; // Aunque exitoso, es bueno registrar el intento$this->logger->info('Email enviado correctamente: ' . $email->id);$output->writeln(" <info>✔ Email #{$email->id} enviado a {$to_address}</info>");// Actualizar el estado del email en la BD y confirmar la transacción$emailMapper->update($email);$this->adapter->getDriver()->getConnection()->commit();} catch (\Exception $e) {$error_message = 'Error de PHPMailer al enviar email ' . $email->id . ': ' . $e->getMessage() . ' Debug: ' . $phpMailer->ErrorInfo;$this->logger->error($error_message);$output->writeln(" <error>✗ Error al enviar email #{$email->id}: {$e->getMessage()}</error>");$email->tried = $email->tried + 1;if ($email->tried >= $this->config['leaderslinked.email.max_retries']) { // Usar una configuración para el número máximo de reintentos$emailError++;$email->status = Email::STATUS_ERROR;$this->logger->info('Email descartado después de ' . $email->tried . ' intentos fallidos: ' . $email->id);$output->writeln(" <error>Email #{$email->id} descartado (máx. reintentos alcanzado).</error>");} else {$emailRetried++;$email->status = Email::STATUS_PENDING; // Mantener como pendiente para reintentar$this->logger->warning('Email ' . $email->id . ' falló. Intentos: ' . $email->tried . '. Reintentando más tarde.');}// Actualizar el estado del email en la BD y revertir si algo sale mal con la actualización (raro en este punto)try {$emailMapper->update($email);$this->adapter->getDriver()->getConnection()->commit(); // Confirmar la transacción} catch (\Exception $dbE) {$this->adapter->getDriver()->getConnection()->rollback();$this->logger->critical('Error CRÍTICO al actualizar el estado del email #'.$email->id.' en la BD después de fallo de envío: ' . $dbE->getMessage());$output->writeln("<error> ERROR CRÍTICO: No se pudo actualizar el estado del email #{$email->id} en la BD.</error>");}} catch (\RuntimeException $e) {// Errores de lógica de negocio o validación (ej. JSON malformado)$error_message = 'Error de lógica/validación para email ' . $email->id . ': ' . $e->getMessage();$this->logger->error($error_message);$output->writeln(" <error>✗ Error de procesamiento interno para email #{$email->id}: {$e->getMessage()}</error>");$emailError++; // Generalmente estos errores son irrecuperables por reintentos$email->status = Email::STATUS_ERROR;$email->tried = $email->tried + 1; // Registrar el intento fallidotry {$emailMapper->update($email);$this->adapter->getDriver()->getConnection()->commit(); // Confirmar la transacción} catch (\Exception $dbE) {$this->adapter->getDriver()->getConnection()->rollback();$this->logger->critical('Error CRÍTICO al actualizar el estado del email #'.$email->id.' en la BD después de error de procesamiento: ' . $dbE->getMessage());$output->writeln("<error> ERROR CRÍTICO: No se pudo actualizar el estado del email #{$email->id} en la BD.</error>");}}catch (\Exception $e) {// Cualquier otra excepción no capturada$error_message = 'Error desconocido al procesar email ' . $email->id . ': ' . $e->getMessage();$this->logger->critical($error_message);$output->writeln(" <error>✗ Error inesperado al procesar email #{$email->id}: {$e->getMessage()}</error>");$emailError++;$email->status = Email::STATUS_ERROR; // Marcar como error irrecuperable$email->tried = $email->tried + 1;try {$emailMapper->update($email);$this->adapter->getDriver()->getConnection()->commit(); // Confirmar la transacción} catch (\Exception $dbE) {$this->adapter->getDriver()->getConnection()->rollback();$this->logger->critical('Error CRÍTICO al actualizar el estado del email #'.$email->id.' en la BD después de error desconocido: ' . $dbE->getMessage());$output->writeln("<error> ERROR CRÍTICO: No se pudo actualizar el estado del email #{$email->id} en la BD.</error>");}}}$this->logger->info('Email con Errores descartados: ' . $emailError);$this->logger->info('Email pendientes para reintento: ' . $emailRetried);$this->logger->info('Email enviados correctamente: ' . $emailCompleted);$this->logger->info('Fin del proceso de la cola de Email');$output->writeln('');$output->writeln("<info>Resumen de la ejecución:</info>");$output->writeln(" - Correos enviados: <info>{$emailCompleted}</info>");$output->writeln(" - Correos con error y descartados: <error>{$emailError}</error>");$output->writeln(" - Correos a reintentar: <comment>{$emailRetried}</comment>");$output->writeln('<info>Proceso de la cola de emails finalizado.</info>');return Command::SUCCESS; // Usar constantes de Symfony para el código de salida}/*** Normaliza la codificación de una cadena a UTF-8.** @param string $string* @return string*/private function normalizeEncoding(string $string): string{$encoding = mb_detect_encoding($string, mb_detect_order(), true);if ($encoding && $encoding != 'UTF-8') {return mb_convert_encoding($string, 'UTF-8', $encoding);}return $string;}}