addArgument('action', InputArgument::REQUIRED, 'One of: register, remove, deactivate') ->addArgument('callback_id', InputArgument::OPTIONAL, 'Callback ID required for remove/deactivate') ->addOption('url', null, InputOption::VALUE_REQUIRED, 'Override the webhook URL to register before Basic Auth credentials are added'); } protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $action = strtolower(trim((string) $input->getArgument('action'))); try { return match ($action) { self::ACTION_REGISTER => $this->register($io, $input->getOption('url')), self::ACTION_REMOVE, self::ACTION_DEACTIVATE => $this->remove($io, $input->getArgument('callback_id')), default => $this->invalidAction($io, $action), }; } catch (NewsletterProviderException|\InvalidArgumentException $exception) { $this->logger->error('Mailjet newsletter webhook command failed', [ 'action' => $action, 'error' => $exception->getMessage(), ]); $io->error($exception->getMessage()); return Command::FAILURE; } } private function register(SymfonyStyle $io, mixed $overrideUrl): int { $webhookUrl = $this->resolveWebhookUrl($overrideUrl); $authenticatedWebhookUrl = $this->prependBasicAuth($webhookUrl); $payload = $this->mailjetApiClient->createEventCallbackUrl(self::EVENT_TYPE_UNSUBSCRIBE, $authenticatedWebhookUrl, self::WEBHOOK_VERSION); $io->success(sprintf('Created Mailjet newsletter webhook at %s.', $authenticatedWebhookUrl)); $io->writeln($this->formatPayload($payload)); return Command::SUCCESS; } private function remove(SymfonyStyle $io, mixed $callbackId): int { $resolvedCallbackId = $this->resolveCallbackId($callbackId); $this->mailjetApiClient->deleteEventCallbackUrl($resolvedCallbackId); $io->success(sprintf('Deleted Mailjet newsletter webhook callback ID %d.', $resolvedCallbackId)); return Command::SUCCESS; } private function invalidAction(SymfonyStyle $io, string $action): int { $io->error(sprintf('Unknown action "%s". Use register, remove, or deactivate.', $action)); return Command::FAILURE; } private function resolveWebhookUrl(mixed $override): string { if (true === is_string($override) && '' !== trim($override)) { $url = trim($override); if (false === filter_var($url, FILTER_VALIDATE_URL)) { throw new \InvalidArgumentException('The provided webhook URL is not valid.'); } return rtrim($url, '/'); } if ('' === trim($this->defaultBaseUrl)) { throw new \InvalidArgumentException('APP_BASE_URL is not configured.'); } return rtrim(trim($this->defaultBaseUrl), '/').self::WEBHOOK_PATH; } private function prependBasicAuth(string $url): string { if ('' === trim($this->basicAuthPassword)) { throw new \InvalidArgumentException('MAILJET_WEBHOOK_BASIC_PASSWORD is not configured.'); } $parts = parse_url($url); if (false === is_array($parts) || false === isset($parts['scheme'], $parts['host'])) { throw new \InvalidArgumentException('The webhook URL is not valid.'); } $host = $parts['host']; if (false === str_starts_with($host, '[') && true === str_contains($host, ':')) { $host = sprintf('[%s]', $host); } $authenticatedUrl = sprintf( '%s://%s:%s@%s', $parts['scheme'], rawurlencode(self::WEBHOOK_BASIC_AUTH_USER), rawurlencode($this->basicAuthPassword), $host, ); if (isset($parts['port'])) { $authenticatedUrl .= sprintf(':%d', $parts['port']); } $authenticatedUrl .= $parts['path'] ?? ''; if (isset($parts['query'])) { $authenticatedUrl .= '?'.$parts['query']; } if (isset($parts['fragment'])) { $authenticatedUrl .= '#'.$parts['fragment']; } return $authenticatedUrl; } private function resolveCallbackId(mixed $callbackId): int { if (true === is_int($callbackId) && $callbackId > 0) { return $callbackId; } if (true === is_string($callbackId) && true === ctype_digit(trim($callbackId))) { $resolvedCallbackId = (int) trim($callbackId); if ($resolvedCallbackId > 0) { return $resolvedCallbackId; } } throw new \InvalidArgumentException('A numeric callback ID is required for remove/deactivate.'); } /** * @param array $payload */ private function formatPayload(array $payload): string { $encoded = json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); if (false !== $encoded) { return $encoded; } return var_export($payload, true); } }