Proyectos de Subversion LeadersLinked - Services

Rev

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

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
345 www 2
declare(strict_types = 1);
1 efrain 3
namespace LeadersLinked\Command;
4
 
5
use Symfony\Component\Console\Command\Command;
6
use Symfony\Component\Console\Input\InputInterface;
7
use Symfony\Component\Console\Output\OutputInterface;
8
use Laminas\Db\Adapter\AdapterInterface;
9
use Laminas\Log\LoggerInterface;
10
use LeadersLinked\Mapper\EmailMapper;
11
use PHPMailer\PHPMailer\PHPMailer;
12
use LeadersLinked\Model\Email;
13
use Laminas\Mvc\I18n\Translator;
14
use LeadersLinked\Cache\CacheInterface;
613 stevensc 15
use Laminas\Db\Adapter\Exception\InvalidQueryException; // Importar para manejar excepciones de BD
1 efrain 16
 
17
class ProcessQueueEmailCommand extends Command
18
{
19
    /**
20
     *
21
     * @var \Laminas\Db\Adapter\AdapterInterface
22
     */
23
    private $adapter;
345 www 24
 
1 efrain 25
    /**
26
     *
27
     * @var \LeadersLinked\Cache\CacheInterface
28
     */
29
    private $cache;
345 www 30
 
1 efrain 31
    /**
32
     *
33
     * @var \Laminas\Log\LoggerInterface
34
     */
35
    private $logger;
345 www 36
 
1 efrain 37
    /**
38
     *
39
     * @var array
40
     */
41
    private $config;
345 www 42
 
1 efrain 43
    /**
44
     *
45
     * @var \Laminas\Mvc\I18n\Translator
46
     */
47
    private $translator;
345 www 48
 
1 efrain 49
    /**
50
     *
51
     * @param \Laminas\Db\Adapter\AdapterInterface $adapter
52
     * @param \LeadersLinked\Cache\CacheInterface $cache
613 stevensc 53
     * @param \Laminas\Log\LoggerInterface $logger
1 efrain 54
     * @param array $config
55
     * @param \Laminas\Mvc\I18n\Translator $translator
56
     */
57
    public function __construct($adapter, $cache, $logger, $config, $translator)
58
    {
345 www 59
        $this->adapter = $adapter;
60
        $this->cache = $cache;
61
        $this->logger = $logger;
62
        $this->config = $config;
63
        $this->translator = $translator;
64
 
1 efrain 65
        parent::__construct();
66
    }
67
 
613 stevensc 68
    protected function execute($input, $output)
1 efrain 69
    {
609 stevensc 70
        $sandbox = $this->config['leaderslinked.runmode.sandbox'];
71
        if ($sandbox) {
72
            $batch_size = $this->config['leaderslinked.email.sandbox_batch_size'];
73
            $from_address = $this->config['leaderslinked.email.sandbox_from_address'];
74
            $from_name = $this->config['leaderslinked.email.sandbox_from_name'];
75
            $host = $this->config['leaderslinked.email.sandbox_host'];
76
            $port = $this->config['leaderslinked.email.sandbox_port'];
77
            $username = $this->config['leaderslinked.email.sandbox_username'];
78
            $password = $this->config['leaderslinked.email.sandbox_password'];
79
        } else {
80
            $batch_size = $this->config['leaderslinked.email.production_batch_size'];
81
            $from_address = $this->config['leaderslinked.email.production_from_address'];
82
            $from_name = $this->config['leaderslinked.email.production_from_name'];
83
            $host = $this->config['leaderslinked.email.production_host'];
84
            $port = $this->config['leaderslinked.email.production_port'];
85
            $username = $this->config['leaderslinked.email.production_username'];
86
            $password = $this->config['leaderslinked.email.production_password'];
87
        }
1 efrain 88
 
609 stevensc 89
        $this->logger->info('Inicio del proceso de la cola de Email');
613 stevensc 90
        $output->writeln('<info>Iniciando el procesamiento de la cola de emails...</info>');
1 efrain 91
 
609 stevensc 92
        $emailCompleted = 0;
613 stevensc 93
        $emailError = 0; // Para correos con errores que se descartan
94
        $emailRetried = 0; // Para correos que se reintentan
345 www 95
 
609 stevensc 96
        $emailMapper = EmailMapper::getInstance($this->adapter);
97
        $emails = $emailMapper->fetchBatch($batch_size);
1 efrain 98
 
613 stevensc 99
        if (!$emails) {
100
            $this->logger->info('No hay emails en la cola');
101
            $output->writeln('<comment>No hay emails en la cola para procesar.</comment>');
102
            return Command::SUCCESS;
103
        }
104
 
105
        foreach ($emails as $email) {
106
            // Iniciar transacción para cada email para asegurar atomicidad
107
            $this->adapter->getDriver()->getConnection()->beginTransaction();
108
            try {
609 stevensc 109
                $content = json_decode($email->content, true);
345 www 110
 
613 stevensc 111
                // Validación básica de los datos del email
112
                if (json_last_error() !== JSON_ERROR_NONE) {
113
                    throw new \RuntimeException('Error al decodificar el contenido JSON del email: ' . json_last_error_msg());
114
                }
115
                if (!isset($content['to_address'], $content['subject'], $content['message'])) {
116
                    throw new \RuntimeException('Contenido del email JSON incompleto: faltan datos esenciales.');
117
                }
118
 
609 stevensc 119
                $to_address = $content['to_address'];
613 stevensc 120
                $to_name = $content['to_name'] ?? ''; // Valor por defecto
121
                $cc = $content['cc'] ?? []; // Valor por defecto
122
                $bcc = $content['bcc'] ?? []; // Valor por defecto
609 stevensc 123
                $subject = $content['subject'];
124
                $message = $content['message'];
345 www 125
 
613 stevensc 126
                // Normalización de codificación
127
                $subject = $this->normalizeEncoding($subject);
128
                $message = $this->normalizeEncoding($message);
345 www 129
 
613 stevensc 130
                $phpMailer = new PHPMailer(true); // true para habilitar excepciones
609 stevensc 131
                $phpMailer->isSMTP();
613 stevensc 132
                $phpMailer->SMTPDebug = 0; // 0 para no depurar, 2 para mensajes detallados en desarrollo
133
                $phpMailer->Host = $host;
134
                $phpMailer->SMTPAuth = true;
135
                $phpMailer->SMTPSecure = 'tls'; // o 'ssl' si el puerto es 465
136
                $phpMailer->Username = $username;
137
                $phpMailer->Password = $password;
138
                $phpMailer->Port = $port;
139
                $phpMailer->CharSet = 'UTF-8';
345 www 140
 
613 stevensc 141
                $phpMailer->setFrom($from_address, $from_name);
609 stevensc 142
                $phpMailer->addAddress($to_address, $to_name);
613 stevensc 143
 
144
                foreach ($cc as $address => $name) {
145
                    $phpMailer->addCC($address, $name);
609 stevensc 146
                }
613 stevensc 147
                foreach ($bcc as $address => $name) {
148
                    $phpMailer->addBCC($address, $name);
609 stevensc 149
                }
345 www 150
 
609 stevensc 151
                $phpMailer->IsHTML(true);
152
                $phpMailer->Subject = $subject;
153
                $phpMailer->Body = $message;
613 stevensc 154
                $phpMailer->AltBody = strip_tags($message); // Versión de texto plano
606 stevensc 155
 
613 stevensc 156
                $phpMailer->send();
345 www 157
 
613 stevensc 158
                // Si el envío es exitoso
159
                $emailCompleted++;
160
                $email->status = Email::STATUS_COMPLETED;
161
                $email->tried = $email->tried + 1; // Aunque exitoso, es bueno registrar el intento
162
                $this->logger->info('Email enviado correctamente: ' . $email->id);
163
                $output->writeln("  <info>✔ Email #{$email->id} enviado a {$to_address}</info>");
164
 
165
                // Actualizar el estado del email en la BD y confirmar la transacción
166
                $emailMapper->update($email);
167
                $this->adapter->getDriver()->getConnection()->commit();
168
 
169
            } catch (\Exception $e) {
170
                $error_message = 'Error de PHPMailer al enviar email ' . $email->id . ': ' . $e->getMessage() . ' Debug: ' . $phpMailer->ErrorInfo;
171
                $this->logger->error($error_message);
172
                $output->writeln("  <error>✗ Error al enviar email #{$email->id}: {$e->getMessage()}</error>");
173
 
174
                $email->tried = $email->tried + 1;
175
 
176
                if ($email->tried >= $this->config['leaderslinked.email.max_retries']) { // Usar una configuración para el número máximo de reintentos
177
                    $emailError++;
178
                    $email->status = Email::STATUS_ERROR;
179
                    $this->logger->info('Email descartado después de ' . $email->tried . ' intentos fallidos: ' . $email->id);
180
                    $output->writeln("    <error>Email #{$email->id} descartado (máx. reintentos alcanzado).</error>");
609 stevensc 181
                } else {
613 stevensc 182
                    $emailRetried++;
183
                    $email->status = Email::STATUS_PENDING; // Mantener como pendiente para reintentar
184
                    $this->logger->warning('Email ' . $email->id . ' falló. Intentos: ' . $email->tried . '. Reintentando más tarde.');
609 stevensc 185
                }
613 stevensc 186
                // Actualizar el estado del email en la BD y revertir si algo sale mal con la actualización (raro en este punto)
187
                try {
188
                    $emailMapper->update($email);
189
                    $this->adapter->getDriver()->getConnection()->commit(); // Confirmar la transacción
190
                } catch (\Exception $dbE) {
191
                    $this->adapter->getDriver()->getConnection()->rollback();
192
                    $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());
193
                    $output->writeln("<error>    ERROR CRÍTICO: No se pudo actualizar el estado del email #{$email->id} en la BD.</error>");
194
                }
611 stevensc 195
 
613 stevensc 196
            } catch (\RuntimeException $e) {
197
                // Errores de lógica de negocio o validación (ej. JSON malformado)
198
                $error_message = 'Error de lógica/validación para email ' . $email->id . ': ' . $e->getMessage();
199
                $this->logger->error($error_message);
200
                $output->writeln("  <error>✗ Error de procesamiento interno para email #{$email->id}: {$e->getMessage()}</error>");
201
 
202
                $emailError++; // Generalmente estos errores son irrecuperables por reintentos
203
                $email->status = Email::STATUS_ERROR;
204
                $email->tried = $email->tried + 1; // Registrar el intento fallido
205
 
206
                try {
207
                    $emailMapper->update($email);
208
                    $this->adapter->getDriver()->getConnection()->commit(); // Confirmar la transacción
209
                } catch (\Exception $dbE) {
210
                    $this->adapter->getDriver()->getConnection()->rollback();
211
                    $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());
212
                    $output->writeln("<error>    ERROR CRÍTICO: No se pudo actualizar el estado del email #{$email->id} en la BD.</error>");
213
                }
606 stevensc 214
            }
613 stevensc 215
             catch (\Exception $e) {
216
                // Cualquier otra excepción no capturada
217
                $error_message = 'Error desconocido al procesar email ' . $email->id . ': ' . $e->getMessage();
218
                $this->logger->critical($error_message);
219
                $output->writeln("  <error>✗ Error inesperado al procesar email #{$email->id}: {$e->getMessage()}</error>");
220
 
221
                $emailError++;
222
                $email->status = Email::STATUS_ERROR; // Marcar como error irrecuperable
223
                $email->tried = $email->tried + 1;
224
 
225
                try {
226
                    $emailMapper->update($email);
227
                    $this->adapter->getDriver()->getConnection()->commit(); // Confirmar la transacción
228
                } catch (\Exception $dbE) {
229
                    $this->adapter->getDriver()->getConnection()->rollback();
230
                    $this->logger->critical('Error CRÍTICO al actualizar el estado del email #'.$email->id.' en la BD después de error desconocido: ' . $dbE->getMessage());
231
                    $output->writeln("<error>    ERROR CRÍTICO: No se pudo actualizar el estado del email #{$email->id} en la BD.</error>");
232
                }
233
            }
606 stevensc 234
        }
235
 
609 stevensc 236
        $this->logger->info('Email con Errores descartados: ' . $emailError);
613 stevensc 237
        $this->logger->info('Email pendientes para reintento: ' . $emailRetried);
238
        $this->logger->info('Email enviados correctamente: ' . $emailCompleted);
609 stevensc 239
        $this->logger->info('Fin del proceso de la cola de Email');
606 stevensc 240
 
613 stevensc 241
        $output->writeln('');
242
        $output->writeln("<info>Resumen de la ejecución:</info>");
243
        $output->writeln("  - Correos enviados: <info>{$emailCompleted}</info>");
244
        $output->writeln("  - Correos con error y descartados: <error>{$emailError}</error>");
245
        $output->writeln("  - Correos a reintentar: <comment>{$emailRetried}</comment>");
246
        $output->writeln('<info>Proceso de la cola de emails finalizado.</info>');
247
 
248
        return Command::SUCCESS; // Usar constantes de Symfony para el código de salida
1 efrain 249
    }
613 stevensc 250
 
251
    /**
252
     * Normaliza la codificación de una cadena a UTF-8.
253
     *
254
     * @param string $string
255
     * @return string
256
     */
257
    private function normalizeEncoding(string $string): string
258
    {
259
        $encoding = mb_detect_encoding($string, mb_detect_order(), true);
260
        if ($encoding && $encoding != 'UTF-8') {
261
            return mb_convert_encoding($string, 'UTF-8', $encoding);
262
        }
263
        return $string;
264
    }
1 efrain 265
}