diff --git a/src/Embed/Embed.php b/src/Embed/Embed.php index 0396ac8..d4dd125 100644 --- a/src/Embed/Embed.php +++ b/src/Embed/Embed.php @@ -2,39 +2,42 @@ namespace Hyvor\Unfold\Embed; -use Hyvor\Unfold\Embed\Exception\ParserException; -use Hyvor\Unfold\Embed\Exception\UnableToResolveEmbedException; -use Hyvor\Unfold\Embed\Platforms\Reddit; -use Hyvor\Unfold\Embed\Platforms\Tiktok; -use Hyvor\Unfold\Embed\Platforms\Twitter; -use Hyvor\Unfold\Embed\Platforms\Youtube; -use Hyvor\Unfold\UnfoldConfigObject; +//use Hyvor\Unfold\Embed\Platforms\GithubGist; +//use Hyvor\Unfold\Embed\Platforms\Reddit; +//use Hyvor\Unfold\Embed\Platforms\Tiktok; +//use Hyvor\Unfold\Embed\Platforms\Twitter; +//use Hyvor\Unfold\Embed\Platforms\Youtube; +use Hyvor\Unfold\Exception\EmbedUnableToResolveException; +use Hyvor\Unfold\Exception\EmbedParserException; +use Hyvor\Unfold\Exception\UnfoldException; +use Hyvor\Unfold\UnfoldCallContext; +use Hyvor\Unfold\UnfoldConfig; use Hyvor\Unfold\Unfolded\Unfolded; -use Hyvor\Unfold\UnfoldException; use Hyvor\Unfold\UnfoldMethod; -use Hyvor\Unfold\UnfoldCallContext; class Embed { + /** - * @var EmbedParserAbstract[] + * @return string[] */ - public const PARSERS = [ - Youtube::class, - Reddit::class, - Tiktok::class, - Twitter::class, - Reddit::class, - ]; + public static function getParsers(): array + { + $namespace = __NAMESPACE__ . '\\Platforms\\'; + return array_map( + fn($file) => $namespace . pathinfo($file, PATHINFO_FILENAME), + glob(__DIR__ . '/Platforms/*.php') + ); + } /** - * @throws ParserException + * @throws EmbedParserException */ public static function parse( string $url, - ?UnfoldConfigObject $config = null, + ?UnfoldConfig $config = null, ): ?EmbedResponseObject { - foreach (self::PARSERS as $parserClass) { + foreach (self::getParsers() as $parserClass) { $parser = new $parserClass($url, $config); if ($parser->match()) { return $parser->parse(); @@ -55,7 +58,7 @@ public static function getUnfoldedObject( if ($oembed === null) { if ($context->method === UnfoldMethod::EMBED) { - throw new UnableToResolveEmbedException(); + throw new EmbedUnableToResolveException(); } else { return null; } diff --git a/src/Embed/EmbedParserAbstract.php b/src/Embed/EmbedParserAbstract.php index b1a65b8..994b86e 100644 --- a/src/Embed/EmbedParserAbstract.php +++ b/src/Embed/EmbedParserAbstract.php @@ -4,8 +4,9 @@ use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Uri; -use Hyvor\Unfold\Embed\Exception\ParserException; -use Hyvor\Unfold\UnfoldConfigObject; +use Hyvor\Unfold\Exception\EmbedParserException; +use Hyvor\Unfold\Exception\UnfoldException; +use Hyvor\Unfold\UnfoldConfig; use Psr\Http\Client\ClientExceptionInterface; use Psr\Http\Message\RequestInterface; @@ -15,13 +16,13 @@ */ abstract class EmbedParserAbstract { - protected UnfoldConfigObject $config; + protected UnfoldConfig $config; public function __construct( protected string $url, - ?UnfoldConfigObject $config = null, + ?UnfoldConfig $config = null, ) { - $this->config = $config ?? new UnfoldConfigObject(); + $this->config = $config ?? new UnfoldConfig(); } @@ -65,14 +66,25 @@ public function match(): bool return false; } - public function parse(): ?EmbedResponseObject + /** + * @throws UnfoldException + */ + public function parse(): EmbedResponseObject { - $oEmbedUrl = $this->oEmbedUrl(); - - if (!$oEmbedUrl) { - // TODO: Check config option for fallback - return null; + if ($this instanceof EmbedParserOEmbedInterface) { + return $this->parseOEmbed(); + } elseif ($this instanceof EmbedParserCustomInterface) { + return $this->parseCustom(); + } else { + throw new \Exception( + 'EmbedParserAbstract should be implemented with either EmbedParserOEmbedInterface or EmbedParserCustomInterface' + ); // @codeCoverageIgnore } + } + + public function parseOEmbed(): ?EmbedResponseObject + { + $oEmbedUrl = $this->oEmbedUrl(); $uri = Uri::withQueryValues( new Uri($oEmbedUrl), @@ -97,7 +109,7 @@ public function parse(): ?EmbedResponseObject try { $response = $client->sendRequest($request); } catch (ClientExceptionInterface $e) { - throw new ParserException( + throw new EmbedParserException( "Failed to fetch oEmbed data from the endpoint", previous: $e ); @@ -107,7 +119,7 @@ public function parse(): ?EmbedResponseObject $content = $response->getBody()->getContents(); if ($status !== 200) { - throw new ParserException( + throw new EmbedParserException( "Failed to fetch oEmbed data from the endpoint. Status: $status. Response: $content" ); } @@ -115,10 +127,20 @@ public function parse(): ?EmbedResponseObject $parsed = json_decode($content, true); if (!is_array($parsed)) { - throw new ParserException("Failed to parse JSON response from oEmbed endpoint"); + throw new EmbedParserException("Failed to parse JSON response from oEmbed endpoint"); } return EmbedResponseObject::fromArray($parsed); } + private function parseCustom() + { + $html = $this->getEmbedHtml(); + + return EmbedResponseObject::fromArray([ + 'type' => 'embed', + 'html' => $html + ]); + } + } diff --git a/src/Embed/EmbedParserOEmbedInterface.php b/src/Embed/EmbedParserOEmbedInterface.php index 874f7fb..e67c4d0 100644 --- a/src/Embed/EmbedParserOEmbedInterface.php +++ b/src/Embed/EmbedParserOEmbedInterface.php @@ -4,6 +4,6 @@ interface EmbedParserOEmbedInterface { - public function oEmbedUrl(): ?string; + public function oEmbedUrl(): string; } diff --git a/src/Embed/Exception/ParserException.php b/src/Embed/Exception/ParserException.php deleted file mode 100644 index 4e49b12..0000000 --- a/src/Embed/Exception/ParserException.php +++ /dev/null @@ -1,9 +0,0 @@ -config->facebookAccessToken; if (!$facebookAccessToken) { - throw new ParserException('Facebook Access Token is required for Instagram embeds'); + throw new EmbedParserException('Facebook Access Token is required for Instagram embeds'); } $uri = Uri::withQueryValue($uri, 'access_token', $facebookAccessToken); @@ -57,7 +57,7 @@ public function regex() ]; } - public function oEmbedUrl(): ?string + public function oEmbedUrl(): string { return 'https://graph.facebook.com/v16.0/instagram_oembed'; } diff --git a/src/Embed/Platforms/Reddit.php b/src/Embed/Platforms/Reddit.php index e681e68..c1bb944 100644 --- a/src/Embed/Platforms/Reddit.php +++ b/src/Embed/Platforms/Reddit.php @@ -19,7 +19,7 @@ public function regex() ]; } - public function oEmbedUrl(): ?string + public function oEmbedUrl(): string { return 'https://www.reddit.com/oembed'; } diff --git a/src/Embed/Platforms/Tiktok.php b/src/Embed/Platforms/Tiktok.php index 80ead39..5511548 100644 --- a/src/Embed/Platforms/Tiktok.php +++ b/src/Embed/Platforms/Tiktok.php @@ -3,8 +3,9 @@ namespace Hyvor\Unfold\Embed\Platforms; use Hyvor\Unfold\Embed\EmbedParserAbstract; +use Hyvor\Unfold\Embed\EmbedParserOEmbedInterface; -class Tiktok extends EmbedParserAbstract +class Tiktok extends EmbedParserAbstract implements EmbedParserOEmbedInterface { public function regex() { @@ -14,7 +15,7 @@ public function regex() ]; } - public function oEmbedUrl(): ?string + public function oEmbedUrl(): string { return 'https://www.tiktok.com/oembed'; } diff --git a/src/Embed/Platforms/Twitter.php b/src/Embed/Platforms/Twitter.php index e92ce2f..08048e8 100644 --- a/src/Embed/Platforms/Twitter.php +++ b/src/Embed/Platforms/Twitter.php @@ -19,7 +19,7 @@ public function regex() ]; } - public function oEmbedUrl(): ?string + public function oEmbedUrl(): string { return 'https://publish.twitter.com/oembed'; } diff --git a/src/Embed/Platforms/Youtube.php b/src/Embed/Platforms/Youtube.php index 2236510..b65af59 100644 --- a/src/Embed/Platforms/Youtube.php +++ b/src/Embed/Platforms/Youtube.php @@ -23,7 +23,7 @@ public function regex() ]; } - public function oEmbedUrl(): ?string + public function oEmbedUrl(): string { return 'https://www.youtube.com/oembed'; } diff --git a/src/Exception/EmbedParserException.php b/src/Exception/EmbedParserException.php new file mode 100644 index 0000000..5098a97 --- /dev/null +++ b/src/Exception/EmbedParserException.php @@ -0,0 +1,7 @@ +config->httpClient->sendRequest($request); } catch (ClientExceptionInterface $e) { - // + throw new LinkScrapeException($e->getMessage()); } $status = $response->getStatusCode(); - $content = $response->getBody()->getContents(); - // TODO: - return $response->getBody(); + if ($status < 200 || $status >= 300) { + throw new LinkScrapeException("Unable to scrape link. HTTP status code: $status"); + } + + return $response->getBody()->getContents(); } diff --git a/src/Link/Metadata/MetadataKeyType.php b/src/Link/Metadata/MetadataKeyType.php index 2341301..7016877 100644 --- a/src/Link/Metadata/MetadataKeyType.php +++ b/src/Link/Metadata/MetadataKeyType.php @@ -2,6 +2,9 @@ namespace Hyvor\Unfold\Link\Metadata; +use Hyvor\Unfold\Unfolded\UnfoldedAuthor; +use Hyvor\Unfold\Unfolded\UnfoldedTag; + enum MetadataKeyType { case TITLE; @@ -48,4 +51,40 @@ enum MetadataKeyType case TWITTER_DESCRIPTION; case TWITTER_TITLE; case TWITTER_IMAGE; + + /** + * Gets the value of the metadata from a given content string + * ex: article:published_time is converted to DateTimeInterface + */ + public function getValue(string $content) + { + if ( + $this === MetadataKeyType::OG_ARTICLE_PUBLISHED_TIME || + $this === MetadataKeyType::OG_ARTICLE_MODIFIED_TIME + ) { + return MetadataParser::getDateTimeFromString($content); + } + + if ( + $this === MetadataKeyType::OG_ARTICLE_AUTHOR || + $this === MetadataKeyType::TWITTER_CREATOR + ) { + return $this->getAuthorFromString($content); + } + + if ($this === MetadataKeyType::OG_ARTICLE_TAG) { + return new UnfoldedTag($content); + } + + return $content; + } + + private function getAuthorFromString(string $value): UnfoldedAuthor + { + if (str_contains($value, 'http://') || str_contains($value, 'https://')) { + return new UnfoldedAuthor(null, $value); + } else { + return new UnfoldedAuthor($value, null); + } + } } diff --git a/src/Link/Metadata/MetadataParser.php b/src/Link/Metadata/MetadataParser.php index e94c893..96a847d 100644 --- a/src/Link/Metadata/MetadataParser.php +++ b/src/Link/Metadata/MetadataParser.php @@ -90,28 +90,7 @@ public function addMetadataFromMetaTags(array $keys): void return; } - if ( - $keyType === MetadataKeyType::OG_ARTICLE_PUBLISHED_TIME || - $keyType === MetadataKeyType::OG_ARTICLE_MODIFIED_TIME - ) { - $content = self::getDateTimeFromString($content); - } - - if ( - $keyType === MetadataKeyType::OG_ARTICLE_AUTHOR || - $keyType === MetadataKeyType::TWITTER_CREATOR - ) { - if (str_contains($content, 'http://') || str_contains($content, 'https://')) { - $content = new UnfoldedAuthor(null, $content); - } else { - $content = new UnfoldedAuthor($content, null); - } - } - - if ($keyType === MetadataKeyType::OG_ARTICLE_TAG) { - $content = new UnfoldedTag($content); - } - + $content = $keyType->getValue($content); $metadata[] = new MetadataObject($keyType, $content); }); diff --git a/src/Link/Metadata/MetadataPriority.php b/src/Link/Metadata/MetadataPriority.php index 853b702..9edbeca 100644 --- a/src/Link/Metadata/MetadataPriority.php +++ b/src/Link/Metadata/MetadataPriority.php @@ -11,7 +11,6 @@ */ class MetadataPriority { - /** * @param MetadataObject[] $metadata */ @@ -57,7 +56,7 @@ private function prioritizedAll(array $keys) * 'OG_TITLE' => [MetadataObject], * ] */ - $keysNames = array_map(fn($key) => $key->name, $keys); + $keysNames = array_map(fn ($key) => $key->name, $keys); uksort($keyedMetadata, function ($a, $b) use ($keysNames) { return array_search($a, $keysNames) - array_search($b, $keysNames); }); @@ -65,7 +64,7 @@ private function prioritizedAll(array $keys) // index by 0,1,2 $keyedMetadata = array_values($keyedMetadata); // return the values - return array_map(fn($metadata) => $metadata->value, $keyedMetadata[0] ?? []); + return array_map(fn ($metadata) => $metadata->value, $keyedMetadata[0] ?? []); } /** @@ -129,18 +128,31 @@ public function siteName(): ?string } - public function siteUrl(): ?string + public function siteUrl(string $url): ?string { - return $this->prioritized([ + $currentUrl = $this->prioritized([ + MetadataKeyType::CANONICAL_URL, MetadataKeyType::OG_URL ]); + $currentUrl = $currentUrl ?? $url; + + // get origin from url + $parsedUrl = parse_url($currentUrl); + if ($parsedUrl !== false) { + $scheme = $parsedUrl['scheme'] ?? 'http'; + $host = $parsedUrl['host'] ?? ''; + return $host ? $scheme . '://' . $host : null; + } + + return null; } public function canonicalUrl(): ?string { return $this->prioritized([ - MetadataKeyType::CANONICAL_URL + MetadataKeyType::CANONICAL_URL, + MetadataKeyType::OG_URL ]); } @@ -190,4 +202,4 @@ public function locale(): ?string ]); } -} \ No newline at end of file +} diff --git a/src/Link/Metadata/Parsers/HtmlLangParser.php b/src/Link/Metadata/Parsers/HtmlLangParser.php index b9301f6..5d11a0d 100644 --- a/src/Link/Metadata/Parsers/HtmlLangParser.php +++ b/src/Link/Metadata/Parsers/HtmlLangParser.php @@ -12,7 +12,7 @@ public function add(): void $htmlNode = $this->parser->crawler->filterXPath('//html'); if ($htmlNode->count() === 0) { - return; + return; // @codeCoverageIgnore } $lang = $htmlNode->attr('lang'); diff --git a/src/Link/Metadata/Parsers/JsonLdParser.php b/src/Link/Metadata/Parsers/JsonLdParser.php index 757767c..0dc9c4a 100644 --- a/src/Link/Metadata/Parsers/JsonLdParser.php +++ b/src/Link/Metadata/Parsers/JsonLdParser.php @@ -20,17 +20,15 @@ public function add(): void if (isset($json['datePublished'])) { $date = MetadataParser::getDateTimeFromString($json['datePublished']); - if (!$date) { - return; + if ($date) { + $this->parser->addMetadata(new MetadataObject(MetadataKeyType::RICH_SCHEMA_PUBLISHED_TIME, $date)); } - $this->parser->addMetadata(new MetadataObject(MetadataKeyType::RICH_SCHEMA_PUBLISHED_TIME, $date)); } if (isset($json['dateModified'])) { $date = MetadataParser::getDateTimeFromString($json['dateModified']); - if (!$date) { - return; + if ($date) { + $this->parser->addMetadata(new MetadataObject(MetadataKeyType::RICH_SCHEMA_MODIFIED_TIME, $date)); } - $this->parser->addMetadata(new MetadataObject(MetadataKeyType::RICH_SCHEMA_MODIFIED_TIME, $date)); } if (isset($json['author'])) { foreach ($json['author'] as $author) { diff --git a/src/Unfold.php b/src/Unfold.php index 0029d7c..7e6efda 100644 --- a/src/Unfold.php +++ b/src/Unfold.php @@ -3,6 +3,7 @@ namespace Hyvor\Unfold; use Hyvor\Unfold\Embed\Embed; +use Hyvor\Unfold\Exception\UnfoldException; use Hyvor\Unfold\Link\Link; use Hyvor\Unfold\Unfolded\Unfolded; @@ -14,9 +15,9 @@ class Unfold public static function unfold( string $url, UnfoldMethod $method = UnfoldMethod::LINK, - UnfoldConfigObject $config = null, + UnfoldConfig $config = null, ): Unfolded { - $config ??= new UnfoldConfigObject(); + $config ??= new UnfoldConfig(); $context = new UnfoldCallContext( $method, $config, diff --git a/src/UnfoldCallContext.php b/src/UnfoldCallContext.php index dcda22b..88ac4d0 100644 --- a/src/UnfoldCallContext.php +++ b/src/UnfoldCallContext.php @@ -8,7 +8,7 @@ class UnfoldCallContext public function __construct( public UnfoldMethod $method, - public UnfoldConfigObject $config, + public UnfoldConfig $config, ) { $this->startTime = microtime(true); } diff --git a/src/UnfoldConfigObject.php b/src/UnfoldConfig.php similarity index 99% rename from src/UnfoldConfigObject.php rename to src/UnfoldConfig.php index 144a43c..0b92ad6 100644 --- a/src/UnfoldConfigObject.php +++ b/src/UnfoldConfig.php @@ -7,7 +7,7 @@ use Http\Discovery\Psr18ClientDiscovery; use Psr\Http\Client\ClientInterface; -class UnfoldConfigObject +class UnfoldConfig { /** * A PSR-18 HTTP Client for sending oembed and other requests @@ -81,7 +81,8 @@ public function __construct( public ?string $facebookAccessToken = null, // CACHE - ) { + ) + { $this->setHttpClient($httpClient); } diff --git a/src/Unfolded/Unfolded.php b/src/Unfolded/Unfolded.php index 1e7f263..36ff3fd 100644 --- a/src/Unfolded/Unfolded.php +++ b/src/Unfolded/Unfolded.php @@ -39,190 +39,6 @@ public function __construct( $this->version = '1.0'; } - /** - * @param MetadataObject[] $metadata - */ - public static function title(array $metadata): ?string - { - return self::getMetadataFromKeys($metadata, [ - MetadataKeyType::TITLE, - MetadataKeyType::OG_TITLE, - MetadataKeyType::TWITTER_TITLE - ]); - } - - /** - * @param MetadataObject[] $metadata - */ - public static function description(array $metadata): ?string - { - return self::getMetadataFromKeys($metadata, [ - MetadataKeyType::DESCRIPTION, - MetadataKeyType::OG_DESCRIPTION, - MetadataKeyType::TWITTER_DESCRIPTION - ]); - } - - /** - * @param MetadataObject[] $metadata - * @return UnfoldedAuthor[] - */ - public static function authors(array $metadata): array - { - return self::getMetadataFromKeys($metadata, [ - MetadataKeyType::RICH_SCHEMA_AUTHOR, - MetadataKeyType::OG_ARTICLE_AUTHOR, - MetadataKeyType::TWITTER_CREATOR - ], true); - } - - /** - * @param MetadataObject[] $metadata - * @return UnfoldedTag[] - */ - public static function tags(array $metadata): array - { - return self::getMetadataFromKeys($metadata, [ - MetadataKeyType::OG_ARTICLE_TAG - ], true); - } - - /** - * @param MetadataObject[] $metadata - */ - public static function siteName(array $metadata): ?string - { - return self::getMetadataFromKeys($metadata, [ - MetadataKeyType::OG_SITE_NAME, - MetadataKeyType::TWITTER_SITE - ]); - } - - /** - * @param MetadataObject[] $metadata - */ - public static function siteUrl(array $metadata): ?string - { - return self::getMetadataFromKeys($metadata, [ - MetadataKeyType::OG_URL - ]); - } - - /** - * @param MetadataObject[] $metadata - */ - public static function canonicalUrl(array $metadata): ?string - { - return self::getMetadataFromKeys($metadata, [ - MetadataKeyType::CANONICAL_URL - ]); - } - - /** - * @param MetadataObject[] $metadata - */ - public static function publishedTime(array $metadata): ?DateTimeInterface - { - return self::getMetadataFromKeys($metadata, [ - MetadataKeyType::RICH_SCHEMA_PUBLISHED_TIME, - MetadataKeyType::OG_ARTICLE_PUBLISHED_TIME - ]); - } - - /** - * @param MetadataObject[] $metadata - */ - public static function modifiedTime(array $metadata): ?DateTimeInterface - { - return self::getMetadataFromKeys($metadata, [ - MetadataKeyType::RICH_SCHEMA_MODIFIED_TIME, - MetadataKeyType::OG_ARTICLE_MODIFIED_TIME - ]); - } - - /** - * @param MetadataObject[] $metadata - */ - public static function thumbnailUrl(array $metadata): ?string - { - return self::getMetadataFromKeys($metadata, [ - MetadataKeyType::OG_IMAGE, - MetadataKeyType::OG_IMAGE_URL, - MetadataKeyType::OG_IMAGE_SECURE_URL, - MetadataKeyType::TWITTER_IMAGE - ]); - } - - /** - * @param MetadataObject[] $metadata - */ - public static function iconUrl(array $metadata): ?string - { - return self::getMetadataFromKeys($metadata, [ - MetadataKeyType::FAVICON_URL - ]); - } - - /** - * @param MetadataObject[] $metadata - */ - public static function locale(array $metadata): ?string - { - return self::getMetadataFromKeys($metadata, [ - MetadataKeyType::LOCALE, - MetadataKeyType::OG_LOCALE - ]); - } - - - // Helpers - - /** - * @param MetadataObject[] $metadata - * @param MetadataKeyType[] $keys - * @return string|DateTimeInterface|UnfoldedAuthor[]|UnfoldedTag[]|null - */ - public static function getMetadataFromKeys( - array $metadata, - array $keys, - bool $isMultiple = false - ): string|DateTimeInterface|array|null { - $value = []; - /** - * keyIndex is used track the most priority key found in the metadata - */ - $keyIndex = count($keys) + 1; - - foreach ($metadata as $meta) { - if (in_array($meta->key, $keys)) { - $isNewPriority = $keyIndex > array_search($meta->key, $keys); // new key with higher priority found - if (count($value) === 0) { // if value array is empty add the value - $value[] = $meta->value; - $keyIndex = array_search($meta->key, $keys); // set the new key index - } elseif ($isNewPriority) { // if a new key with higher priority found add the value to an empty value array - $value = []; - $value[] = $meta->value; - $keyIndex = array_search($meta->key, $keys); // set the new key index - } elseif ($isMultiple && ($keyIndex === array_search( - $meta->key, - $keys - ))) { // if multiple values are allowed and same priority key found - $value[] = $meta->value; - } - } - if (!$isMultiple && $keyIndex === 0) { - break; - } - } - return $isMultiple ? - $value : // return the array of values - ( - count($value) !== 0 ? - $value[0] : // return the first value - null // return null if no value found - ); - } - /** * @param MetadataObject[] $metadata */ @@ -242,7 +58,7 @@ public static function fromMetadata( $metadataPriority->authors(), $metadataPriority->tags(), $metadataPriority->siteName(), - $metadataPriority->siteUrl(), + $metadataPriority->siteUrl($url), $metadataPriority->canonicalUrl(), $metadataPriority->publishedTime(), $metadataPriority->modifiedTime(), diff --git a/tests/Feature/UnfoldEmbedTest.php b/tests/Feature/UnfoldEmbedTest.php new file mode 100644 index 0000000..06bc3e4 --- /dev/null +++ b/tests/Feature/UnfoldEmbedTest.php @@ -0,0 +1,64 @@ + '1.0', + 'type' => 'video', + 'provider_name' => 'YouTube', + 'provider_url' => 'https://youtube.com/', + 'width' => 425, + 'height' => 344, + 'title' => 'Amazing Nintendo Facts', + 'author_name' => 'ZackScott', + 'author_url' => 'https://www.youtube.com/user/ZackScott', + 'html' => '', + ])) + ]); + + $stack = HandlerStack::create($mock); + $stack->push($historyMiddleware); + $client = new Client(['handler' => $stack]); + $response = Unfold::unfold( + 'https://www.youtube.com/watch?v=123', + method: UnfoldMethod::EMBED, + config: new UnfoldConfig( + httpClient: $client + ) + ); + + expect($response->version)->toBe('1.0'); + expect($response->url)->toBe('https://www.youtube.com/watch?v=123'); + expect($response->embed)->toBe(''); + expect($response->title)->toBe('Amazing Nintendo Facts'); + + expect($response->authors)->toHaveCount(1); + expect($response->authors[0]->name)->toBe('ZackScott'); + expect($response->authors[0]->url)->toBe('https://www.youtube.com/user/ZackScott'); + + $request = $history[0]['request']; + expect($request->getMethod())->toBe('GET'); + expect((string)$request->getUri()) + ->toBe('https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v%3D123&format=json'); +}); + +it('fetches custom', function () { + $response = Unfold::unfold( + 'https://gist.github.com/123', + method: UnfoldMethod::EMBED, + ); + expect($response->embed)->toBe(''); +}); \ No newline at end of file diff --git a/tests/Feature/UnfoldLinkTest.php b/tests/Feature/UnfoldLinkTest.php new file mode 100644 index 0000000..cd370b1 --- /dev/null +++ b/tests/Feature/UnfoldLinkTest.php @@ -0,0 +1,109 @@ + + + HYVOR + + + + + + + + + + + + + + + +HTML + ) + ]); + $client = new Client(['handler' => $mock]); + + $response = Unfold::unfold( + 'https://hyvor.com', + config: new UnfoldConfig( + httpClient: $client + ) + ); + + expect($response->title)->toBe('HYVOR'); + expect($response->description)->toBe('We craft privacy-first, user-friendly tools for websites.'); + expect($response->publishedTime->format('Y-m-d'))->toBe('2021-09-01'); + expect($response->modifiedTime->format('Y-m-d'))->toBe('2021-09-02'); + expect($response->siteName)->toBe('HYVOR WEBSITE'); + expect($response->siteUrl)->toBe('https://hyvor.com'); + expect($response->canonicalUrl)->toBe('https://hyvor.com'); + + expect($response->authors)->toHaveCount(1); + expect($response->authors[0]->name)->toBe('John Doe'); + expect($response->authors[0]->url)->toBe('https://johndoe.com'); + + expect($response->tags)->toHaveCount(1); + expect($response->tags[0]->name)->toBe('php'); + + expect($response->thumbnailUrl)->toBe('https://hyvor.com/image.jpg'); + expect($response->iconUrl)->toBe('https://hyvor.com/favicon.ico'); + expect($response->locale)->toBe('fr'); +}); + +it('on 404', function () { + $mock = new MockHandler([ + new Response(404) + ]); + $client = new Client(['handler' => $mock]); + + expect(fn() => Unfold::unfold( + 'https://hyvor.com', + config: new UnfoldConfig( + httpClient: $client + ) + ))->toThrow( + LinkScrapeException::class, + 'Unable to scrape link. HTTP status code: 404' + ); +}); + +it('request exception', function () { + $mock = new MockHandler([ + new RequestException( + 'Error Communicating with Server', + new Request('GET', 'test') + ) + ]); + $client = new Client(['handler' => $mock]); + + expect(fn() => Unfold::unfold( + 'https://hyvor.com', + config: new UnfoldConfig( + httpClient: $client + ) + ))->toThrow( + LinkScrapeException::class, + 'Error Communicating with Server' + ); +}); \ No newline at end of file diff --git a/tests/Feature/UnfoldTest.php b/tests/Feature/UnfoldTest.php deleted file mode 100644 index 1662609..0000000 --- a/tests/Feature/UnfoldTest.php +++ /dev/null @@ -1,7 +0,0 @@ -match())->toBeFalse(); +}); + it('valid response', function () { $history = []; $historyMiddleware = Middleware::history($history); @@ -56,7 +61,7 @@ public function oEmbedUrl(): ?string $platform = new OEmbedTestPlatform( 'https://hyvor.com/123', - new UnfoldConfigObject( + new UnfoldConfig( httpClient: $client ) ); @@ -111,7 +116,7 @@ public function oEmbedUrl(): ?string $platform = new OEmbedTestPlatform( 'https://hyvor.com/123', - new UnfoldConfigObject( + new UnfoldConfig( httpClient: $client ) ); @@ -137,7 +142,7 @@ public function oEmbedUrl(): ?string $platform = new OEmbedTestPlatform( 'https://hyvor.com/123', - new UnfoldConfigObject( + new UnfoldConfig( httpClient: $client ) ); @@ -149,7 +154,7 @@ public function oEmbedUrl(): ?string $exception = $e; } - expect($exception)->toBeInstanceOf(ParserException::class); + expect($exception)->toBeInstanceOf(EmbedParserException::class); expect($exception->getMessage())->toBe('Failed to fetch oEmbed data from the endpoint'); }); @@ -163,7 +168,7 @@ public function oEmbedUrl(): ?string $platform = new OEmbedTestPlatform( 'https://hyvor.com/123', - new UnfoldConfigObject( + new UnfoldConfig( httpClient: $client ) ); @@ -175,7 +180,7 @@ public function oEmbedUrl(): ?string $exception = $e; } - expect($exception)->toBeInstanceOf(ParserException::class); + expect($exception)->toBeInstanceOf(EmbedParserException::class); expect($exception->getMessage())->toBe('Failed to fetch oEmbed data from the endpoint. Status: 404. Response: '); }); @@ -189,7 +194,7 @@ public function oEmbedUrl(): ?string $platform = new OEmbedTestPlatform( 'https://hyvor.com/123', - new UnfoldConfigObject( + new UnfoldConfig( httpClient: $client ) ); @@ -201,6 +206,6 @@ public function oEmbedUrl(): ?string $exception = $e; } - expect($exception)->toBeInstanceOf(ParserException::class); + expect($exception)->toBeInstanceOf(EmbedParserException::class); expect($exception->getMessage())->toBe('Failed to parse JSON response from oEmbed endpoint'); }); diff --git a/tests/Unit/Link/Metadata/MetadataParserTest.php b/tests/Unit/Link/Metadata/MetadataParserTest.php index daa7066..8f5eabe 100644 --- a/tests/Unit/Link/Metadata/MetadataParserTest.php +++ b/tests/Unit/Link/Metadata/MetadataParserTest.php @@ -37,7 +37,7 @@ - + @@ -82,7 +82,7 @@ new DateTimeImmutable('2021-10-10T10:10:10Z') ), new MetadataObject(MetadataKeyType::OG_ARTICLE_AUTHOR, new UnfoldedAuthor('Nadil Karunarathna', null)), - new MetadataObject(MetadataKeyType::OG_ARTICLE_AUTHOR, new UnfoldedAuthor('Supun Wimalasena', null)), + new MetadataObject(MetadataKeyType::OG_ARTICLE_AUTHOR, new UnfoldedAuthor(null, 'https://supun.io')), new MetadataObject(MetadataKeyType::OG_ARTICLE_TAG, new UnfoldedTag('HYVOR')), new MetadataObject(MetadataKeyType::OG_ARTICLE_TAG, new UnfoldedTag('PHP')), new MetadataObject(MetadataKeyType::OG_ARTICLE_TAG, new UnfoldedTag('OEmbed')), @@ -139,6 +139,10 @@ new MetadataObject(MetadataKeyType::TITLE, 'Nadil Karunarathna') ] ], + 'empty title' => [ + '', + [] + ], 'meta description' => [ '', [ @@ -147,7 +151,9 @@ ], 'link tags' => [ ' - ', + + + ', [ new MetadataObject(MetadataKeyType::CANONICAL_URL, 'https://nadil.io'), new MetadataObject(MetadataKeyType::FAVICON_URL, 'https://nadil.io/favicon.ico') @@ -196,6 +202,12 @@ ) ] ], + 'invalid json ld' => [ + '', + [] + ], ]); it('parses metadata', function (string $content, array $metadata) { diff --git a/tests/Unit/Link/Metadata/MetadataPriorityTest.php b/tests/Unit/Link/Metadata/MetadataPriorityTest.php index 10474cc..3b2b134 100644 --- a/tests/Unit/Link/Metadata/MetadataPriorityTest.php +++ b/tests/Unit/Link/Metadata/MetadataPriorityTest.php @@ -5,6 +5,8 @@ use Hyvor\Unfold\Link\Metadata\MetadataKeyType; use Hyvor\Unfold\Link\Metadata\MetadataObject; use Hyvor\Unfold\Link\Metadata\MetadataPriority; +use Hyvor\Unfold\Unfolded\UnfoldedAuthor; +use Hyvor\Unfold\Unfolded\UnfoldedTag; it('prioritizes title', function () { $metadata = [ @@ -46,4 +48,131 @@ $priority = new MetadataPriority($metadata); $authors = $priority->siteName(); expect($authors)->toEqual('OG Site name'); +}); + +it('gets site url', function () { + $metadata = [ + new MetadataObject(MetadataKeyType::CANONICAL_URL, 'https://example.com/php'), + ]; + $priority = new MetadataPriority($metadata); + $siteUrl = $priority->siteUrl('https://hyvor.com/js'); + expect($siteUrl)->toEqual('https://example.com'); + + $priority2 = new MetadataPriority([]); + $siteUrl2 = $priority2->siteUrl('https://hyvor.com/js'); + expect($siteUrl2)->toBe('https://hyvor.com'); + + // empty host + $priority3 = new MetadataPriority([]); + $siteUrl3 = $priority3->siteUrl('invalid url'); + expect($siteUrl3)->toBeNull(); + + // invalid url + $priority4 = new MetadataPriority([]); + $siteUrl4 = $priority4->siteUrl(')@9d8q29cooal'); + expect($siteUrl4)->toBeNull(); + + + // og site url + $metadata = [ + new MetadataObject(MetadataKeyType::OG_URL, 'https://example.com/php'), + ]; + $priority5 = new MetadataPriority($metadata); + $siteUrl5 = $priority5->siteUrl('https://hyvor.com/js'); + expect($siteUrl5)->toEqual('https://example.com'); +}); + + +describe('each one', function () { + it('gets title value', function () { + $metadata = new MetadataObject(MetadataKeyType::TITLE, 'Title'); + $priority = new MetadataPriority([$metadata]); + expect($priority->title())->toBe('Title'); + }); + + it('gets description value', function () { + $metadata = new MetadataObject(MetadataKeyType::OG_DESCRIPTION, 'OG Description'); + $priority = new MetadataPriority([$metadata]); + expect($priority->description())->toBe('OG Description'); + }); + + it('gets authors value', function () { + $metadata = [ + new MetadataObject(MetadataKeyType::OG_ARTICLE_AUTHOR, new UnfoldedAuthor('Author1')), + new MetadataObject(MetadataKeyType::OG_ARTICLE_AUTHOR, new UnfoldedAuthor('Author2')), + new MetadataObject(MetadataKeyType::OG_ARTICLE_AUTHOR, new UnfoldedAuthor(url: 'https://author3.com')), + ]; + $priority = new MetadataPriority($metadata); + expect($priority->authors())->toEqual([ + new UnfoldedAuthor('Author1', null), + new UnfoldedAuthor('Author2', null), + new UnfoldedAuthor(null, 'https://author3.com'), + ]); + }); + + it('gets tags value', function () { + $metadata = [ + new MetadataObject(MetadataKeyType::OG_ARTICLE_TAG, new UnfoldedTag('Tag1')), + new MetadataObject(MetadataKeyType::OG_ARTICLE_TAG, new UnfoldedTag('Tag2')), + ]; + $priority = new MetadataPriority($metadata); + expect($priority->tags())->toEqual([ + new UnfoldedTag('Tag1'), + new UnfoldedTag('Tag2'), + ]); + }); + + it('gets site name value', function () { + $metadata = new MetadataObject(MetadataKeyType::OG_SITE_NAME, 'Site Name'); + $priority = new MetadataPriority([$metadata]); + expect($priority->siteName())->toBe('Site Name'); + }); + + it('gets site url value', function () { + $metadata = new MetadataObject(MetadataKeyType::OG_URL, 'https://example.com'); + $priority = new MetadataPriority([$metadata]); + expect($priority->siteUrl('https://jim.com'))->toBe('https://example.com'); + }); + + it('gets canonical url value', function () { + $metadata = new MetadataObject(MetadataKeyType::CANONICAL_URL, 'https://example.com/canonical'); + $priority = new MetadataPriority([$metadata]); + expect($priority->canonicalUrl())->toBe('https://example.com/canonical'); + }); + + it('gets published time value', function () { + $metadata = new MetadataObject( + MetadataKeyType::OG_ARTICLE_PUBLISHED_TIME, + new \DateTimeImmutable('2024-10-19T16:15:00Z') + ); + $priority = new MetadataPriority([$metadata]); + expect($priority->publishedTime([$metadata])->format('Y-m-d H:i:s'))->toBe('2024-10-19 16:15:00'); + }); + + it('gets modified time value', function () { + $metadata = new MetadataObject( + MetadataKeyType::OG_ARTICLE_MODIFIED_TIME, + new \DateTimeImmutable('2024-10-19T16:15:00Z') + ); + $priority = new MetadataPriority([$metadata]); + expect($priority->modifiedTime([$metadata])->format('Y-m-d H:i:s'))->toBe('2024-10-19 16:15:00'); + }); + + it('gets thumbnail url value', function () { + $metadata = new MetadataObject(MetadataKeyType::OG_IMAGE, 'https://example.com/image.jpg'); + $priority = new MetadataPriority([$metadata]); + expect($priority->thumbnailUrl())->toBe('https://example.com/image.jpg'); + }); + + it('gets icon url value', function () { + $metadata = new MetadataObject(MetadataKeyType::FAVICON_URL, 'https://example.com/favicon.ico'); + $priority = new MetadataPriority([$metadata]); + expect($priority->iconUrl())->toBe('https://example.com/favicon.ico'); + }); + + it('gets locale value', function () { + $metadata = new MetadataObject(MetadataKeyType::OG_LOCALE, 'en'); + $priority = new MetadataPriority([$metadata]); + expect($priority->locale())->toBe('en'); + }); }); \ No newline at end of file diff --git a/tests/Unit/Unfolded/HelperTest.php b/tests/Unit/Unfolded/HelperTest.php deleted file mode 100644 index 77d6261..0000000 --- a/tests/Unit/Unfolded/HelperTest.php +++ /dev/null @@ -1,52 +0,0 @@ -toBe('Title'); -}); - -it('gets metadata from key array 2', function () { - global $metadata; - $metadataValue = Unfolded::getMetadataFromKeys($metadata, [ - MetadataKeyType::OG_TITLE, - MetadataKeyType::TITLE, - MetadataKeyType::TWITTER_TITLE - ]); - expect($metadataValue)->toBe('OG Title'); -}); - -it('returns null when no metadata found', function () { - global $metadata; - $metadataValue = Unfolded::getMetadataFromKeys($metadata, [ - MetadataKeyType::OG_IMAGE, - ]); - expect($metadataValue)->toBeNull(); -}); - -it('returns null when no keys are given', function () { - global $metadata; - $metadataValue = Unfolded::getMetadataFromKeys($metadata, []); - expect($metadataValue)->toBeNull(); -}); diff --git a/tests/Unit/Unfolded/MetadataValueTest.php b/tests/Unit/Unfolded/MetadataValueTest.php deleted file mode 100644 index d0fc78e..0000000 --- a/tests/Unit/Unfolded/MetadataValueTest.php +++ /dev/null @@ -1,90 +0,0 @@ -toBe('Title'); -}); - -it('gets description value', function () { - $metadata = new MetadataObject(MetadataKeyType::OG_DESCRIPTION, 'OG Description'); - expect(Unfolded::description([$metadata]))->toBe('OG Description'); -}); - -it('gets authors value', function () { - $metadata = [ - new MetadataObject(MetadataKeyType::OG_ARTICLE_AUTHOR, new UnfoldedAuthor('Author1')), - new MetadataObject(MetadataKeyType::OG_ARTICLE_AUTHOR, new UnfoldedAuthor('Author2')), - new MetadataObject(MetadataKeyType::OG_ARTICLE_AUTHOR, new UnfoldedAuthor(url: 'https://author3.com')), - ]; - expect(Unfolded::authors($metadata))->toEqual([ - new UnfoldedAuthor('Author1', null), - new UnfoldedAuthor('Author2', null), - new UnfoldedAuthor(null, 'https://author3.com'), - ]); -}); - -it('gets tags value', function () { - $metadata = [ - new MetadataObject(MetadataKeyType::OG_ARTICLE_TAG, new UnfoldedTag('Tag1')), - new MetadataObject(MetadataKeyType::OG_ARTICLE_TAG, new UnfoldedTag('Tag2')), - ]; - expect(Unfolded::tags($metadata))->toEqual([ - new UnfoldedTag('Tag1'), - new UnfoldedTag('Tag2'), - ]); -}); - -it('gets site name value', function () { - $metadata = new MetadataObject(MetadataKeyType::OG_SITE_NAME, 'Site Name'); - expect(Unfolded::siteName([$metadata]))->toBe('Site Name'); -}); - -it('gets site url value', function () { - $metadata = new MetadataObject(MetadataKeyType::OG_URL, 'https://example.com'); - expect(Unfolded::siteUrl([$metadata]))->toBe('https://example.com'); -}); - -it('gets canonical url value', function () { - $metadata = new MetadataObject(MetadataKeyType::CANONICAL_URL, 'https://example.com/canonical'); - expect(Unfolded::canonicalUrl([$metadata]))->toBe('https://example.com/canonical'); -}); - -it('gets published time value', function () { - $metadata = new MetadataObject( - MetadataKeyType::OG_ARTICLE_PUBLISHED_TIME, - new DateTimeImmutable('2024-10-19T16:15:00Z') - ); - expect(Unfolded::publishedTime([$metadata])->format('Y-m-d H:i:s'))->toBe('2024-10-19 16:15:00'); -}); - -it('gets modified time value', function () { - $metadata = new MetadataObject( - MetadataKeyType::OG_ARTICLE_MODIFIED_TIME, - new DateTimeImmutable('2024-10-19T16:15:00Z') - ); - expect(Unfolded::modifiedTime([$metadata])->format('Y-m-d H:i:s'))->toBe('2024-10-19 16:15:00'); -}); - -it('gets thumbnail url value', function () { - $metadata = new MetadataObject(MetadataKeyType::OG_IMAGE, 'https://example.com/image.jpg'); - expect(Unfolded::thumbnailUrl([$metadata]))->toBe('https://example.com/image.jpg'); -}); - -it('gets icon url value', function () { - $metadata = new MetadataObject(MetadataKeyType::FAVICON_URL, 'https://example.com/favicon.ico'); - expect(Unfolded::iconUrl([$metadata]))->toBe('https://example.com/favicon.ico'); -}); - -it('gets locale value', function () { - $metadata = new MetadataObject(MetadataKeyType::OG_LOCALE, 'en'); - expect(Unfolded::locale([$metadata]))->toBe('en'); -});