<?php
declare(strict_types=1); namespace App\Domain; class UrlFetcher { private const DEFAULT_MAX_BYTES = 51200; private const DEFAULT_EXTENDED_HOSTS = [ 'youtube.com' => 1572864, 'youtu.be' => 1572864, ]; private HttpClientInterface $httpClient; private array $extendedHosts; public function __construct(HttpClientInterface $httpClient, array $extendedHosts = []) { $this->httpClient = $httpClient; $this->extendedHosts = $extendedHosts !== [] ? $extendedHosts : self::DEFAULT_EXTENDED_HOSTS; } public function fetchMetadata(string $url): array { try { $html = $this->httpClient->get($url, 3, $this->resolveMaxBytes($url)); } catch (\RuntimeException $e) { return [ 'title' => '', 'description' => '', 'keywords' => '', ]; } $keywords = $this->extractMeta($html, 'keywords'); $ogTags = $this->extractOpenGraphTags($html); $combinedKeywords = implode(',', array_filter([$keywords, $ogTags])); return [ 'title' => $this->extractTag($html, 'title'), 'description' => $this->extractMeta($html, 'description'), 'keywords' => $combinedKeywords, ]; } private function resolveMaxBytes(string $url): int { $host = strtolower((string)parse_url($url, PHP_URL_HOST)); foreach ($this->extendedHosts as $targetHost => $maxBytes) { $targetHost = strtolower($targetHost); if ($host === $targetHost || str_ends_with($host, '.' . $targetHost)) { return (int)$maxBytes; } } return self::DEFAULT_MAX_BYTES; } private function extractTag(string $html, string $tag): string { $pattern = "/<{$tag}[^>]*>(.*?)<\/{$tag}>/is"; if (preg_match($pattern, $html, $matches)) { return $this->sanitize($matches[1]); } return ''; } private function extractMeta(string $html, string $name): string { $pattern1 = "/<meta[^>]+name=[\"']" . preg_quote($name, '/') . "[\"'][^>]+content=[\"'](.*?)[\"'][^>]*>/is"; if (preg_match($pattern1, $html, $matches)) { return $this->sanitize($matches[1]); } $pattern2 = "/<meta[^>]+content=[\"'](.*?)[\"'][^>]+name=[\"']" . preg_quote($name, '/') . "[\"'][^>]*>/is"; if (preg_match($pattern2, $html, $matches)) { return $this->sanitize($matches[1]); } return ''; } private function extractOpenGraphTags(string $html): string { $pattern = "/<meta[^>]+property=[\"']article:tag[\"'][^>]+content=[\"'](.*?)[\"'][^>]*>/is"; if (preg_match_all($pattern, $html, $matches)) { $tags = array_map(function($tag) { return $this->sanitize($tag); }, $matches[1]); return implode(',', array_filter($tags)); } return ''; } private function sanitize(string $text): string { $text = preg_replace('/\s+/', ' ', $text); $text = strip_tags(trim($text)); return html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8'); } } 