Proyectos de Subversion LeadersLinked - Services

Rev

Rev 612 | Rev 614 | Ir a la última revisión | Autoría | Comparar con el anterior | Ultima modificación | Ver Log |

<?php
declare(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 BD

class 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 email
                if (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 fallido

                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 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;
    }
}