Skip to content

Commit 65883e2

Browse files
authored
Add BC-safe enhancements: ProviderCollection, cache, oEmbed (#83)
* Add ProviderCollection, cache support, and oEmbed discovery - Add getProviders() method returning ProviderCollection for type-safe iteration - Add CacheInterface with ArrayCache implementation - Add optional PSR-16 compatible cache to UrlMatcher for persistent index - Add OEmbedDiscovery class for auto-discovering oEmbed endpoints - Add OEmbedResponse DTO for parsed oEmbed data - Add comprehensive tests for all new features * Add documentation for ProviderCollection, caching, and oEmbed
1 parent 9ef4cfa commit 65883e2

11 files changed

Lines changed: 1003 additions & 1 deletion

File tree

‎docs/README.md‎

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,145 @@ $loader = new ArrayLoader([
347347
$MediaEmbed->loadProvidersFromLoader($loader, reset: true);
348348
```
349349

350+
### Provider Collection
351+
352+
Get all providers as a filterable collection:
353+
354+
```php
355+
use MediaEmbed\Provider\ProviderCollection;
356+
357+
$MediaEmbed = new MediaEmbed();
358+
359+
// Get all providers
360+
$providers = $MediaEmbed->getProviders();
361+
echo count($providers); // e.g., 137
362+
363+
// Filter to specific providers
364+
$subset = $MediaEmbed->getProviders(['youtube', 'vimeo', 'dailymotion']);
365+
366+
// Filter by capabilities
367+
$withIframe = $providers->withIframeSupport();
368+
$withThumbnails = $providers->withThumbnailSupport();
369+
370+
// Iterate over providers
371+
foreach ($providers as $slug => $config) {
372+
echo $config->name . ' (' . $slug . ')' . PHP_EOL;
373+
}
374+
375+
// Chain filters
376+
$filtered = $providers
377+
->withIframeSupport()
378+
->whitelist(['youtube', 'vimeo', 'twitch']);
379+
```
380+
381+
### Caching
382+
383+
For better performance on repeated requests, use the cache support:
384+
385+
```php
386+
use MediaEmbed\Cache\ArrayCache;
387+
use MediaEmbed\Cache\CacheInterface;
388+
389+
// Using the built-in ArrayCache (in-memory, single request)
390+
$cache = new ArrayCache();
391+
$MediaEmbed = new MediaEmbed();
392+
$matcher = $MediaEmbed->getUrlMatcher();
393+
$matcher->setCache($cache);
394+
395+
// The domain index will be cached after first match
396+
$MediaEmbed->parseUrl('https://youtube.com/watch?v=abc');
397+
```
398+
399+
For persistent caching, implement `CacheInterface` with your preferred cache backend (Redis, Memcached, filesystem, etc.):
400+
401+
```php
402+
use MediaEmbed\Cache\CacheInterface;
403+
use Psr\SimpleCache\CacheInterface as Psr16Cache;
404+
405+
class Psr16Adapter implements CacheInterface {
406+
public function __construct(private Psr16Cache $cache) {}
407+
408+
public function get(string $key, mixed $default = null): mixed {
409+
return $this->cache->get($key, $default);
410+
}
411+
412+
public function set(string $key, mixed $value, ?int $ttl = null): bool {
413+
return $this->cache->set($key, $value, $ttl);
414+
}
415+
416+
public function delete(string $key): bool {
417+
return $this->cache->delete($key);
418+
}
419+
420+
public function has(string $key): bool {
421+
return $this->cache->has($key);
422+
}
423+
}
424+
425+
// Use with any PSR-16 cache
426+
$cache = new Psr16Adapter($yourPsr16Cache);
427+
$MediaEmbed->getUrlMatcher()->setCache($cache, ttl: 3600);
428+
```
429+
430+
### oEmbed Discovery
431+
432+
For URLs not covered by built-in providers, use oEmbed auto-discovery:
433+
434+
```php
435+
use MediaEmbed\OEmbed\OEmbedDiscovery;
436+
437+
$discovery = new OEmbedDiscovery();
438+
439+
// Auto-discover and fetch oEmbed data
440+
$response = $discovery->discover('https://example.com/video/123');
441+
442+
if ($response !== null) {
443+
echo $response->title;
444+
echo $response->providerName;
445+
446+
if ($response->hasHtml()) {
447+
echo $response->html; // Ready-to-use embed code
448+
}
449+
450+
if ($response->hasThumbnail()) {
451+
echo $response->thumbnailUrl;
452+
}
453+
}
454+
455+
// With size constraints
456+
$response = $discovery->discover($url, maxWidth: 640, maxHeight: 480);
457+
458+
// Or fetch directly from a known endpoint
459+
$response = $discovery->fetch('https://example.com/oembed?url=...');
460+
```
461+
462+
The `OEmbedResponse` provides typed access to all oEmbed fields:
463+
464+
```php
465+
$response->type; // 'video', 'photo', 'link', or 'rich'
466+
$response->version; // oEmbed version (usually '1.0')
467+
$response->title; // Content title
468+
$response->authorName; // Author/creator name
469+
$response->authorUrl; // Author URL
470+
$response->providerName; // Provider name (e.g., 'YouTube')
471+
$response->providerUrl; // Provider homepage
472+
$response->thumbnailUrl; // Thumbnail image URL
473+
$response->thumbnailWidth; // Thumbnail width
474+
$response->thumbnailHeight;// Thumbnail height
475+
$response->html; // Embed HTML (for video/rich types)
476+
$response->width; // Embed width
477+
$response->height; // Embed height
478+
$response->url; // Source URL (for photo type)
479+
$response->cacheAge; // Suggested cache duration in seconds
480+
481+
// Type checks
482+
$response->isVideo();
483+
$response->isPhoto();
484+
$response->isRich();
485+
$response->hasHtml();
486+
$response->hasThumbnail();
487+
```
488+
350489
### Example with BBCode
351490

352491
#### Parse video content upon save (db input)

‎src/Cache/ArrayCache.php‎

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MediaEmbed\Cache;
6+
7+
/**
8+
* Simple in-memory array cache.
9+
*
10+
* This cache stores values in memory for the duration of the request.
11+
* Use this when you don't need persistent caching.
12+
*/
13+
final class ArrayCache implements CacheInterface {
14+
15+
/**
16+
* @var array<string, mixed>
17+
*/
18+
private array $cache = [];
19+
20+
/**
21+
* @inheritDoc
22+
*/
23+
public function get(string $key, mixed $default = null): mixed {
24+
return $this->cache[$key] ?? $default;
25+
}
26+
27+
/**
28+
* @inheritDoc
29+
*/
30+
public function set(string $key, mixed $value, ?int $ttl = null): bool {
31+
$this->cache[$key] = $value;
32+
33+
return true;
34+
}
35+
36+
/**
37+
* @inheritDoc
38+
*/
39+
public function delete(string $key): bool {
40+
unset($this->cache[$key]);
41+
42+
return true;
43+
}
44+
45+
/**
46+
* @inheritDoc
47+
*/
48+
public function has(string $key): bool {
49+
return isset($this->cache[$key]);
50+
}
51+
52+
/**
53+
* Clear all cached values.
54+
*
55+
* @return bool
56+
*/
57+
public function clear(): bool {
58+
$this->cache = [];
59+
60+
return true;
61+
}
62+
63+
}

‎src/Cache/CacheInterface.php‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MediaEmbed\Cache;
6+
7+
/**
8+
* Simple cache interface compatible with PSR-16 SimpleCache.
9+
*
10+
* Implementations of this interface can wrap any PSR-16 compatible cache
11+
* or provide custom caching logic.
12+
*/
13+
interface CacheInterface {
14+
15+
/**
16+
* Fetches a value from the cache.
17+
*
18+
* @param string $key The unique key of this item in the cache.
19+
* @param mixed $default Default value to return if the key does not exist.
20+
* @return mixed The value of the item from the cache, or $default if not found.
21+
*/
22+
public function get(string $key, mixed $default = null): mixed;
23+
24+
/**
25+
* Persists data in the cache.
26+
*
27+
* @param string $key The key of the item to store.
28+
* @param mixed $value The value of the item to store.
29+
* @param int|null $ttl Optional TTL in seconds.
30+
* @return bool True on success, false on failure.
31+
*/
32+
public function set(string $key, mixed $value, ?int $ttl = null): bool;
33+
34+
/**
35+
* Delete an item from the cache by its unique key.
36+
*
37+
* @param string $key The unique cache key.
38+
* @return bool True if the item was successfully removed, false otherwise.
39+
*/
40+
public function delete(string $key): bool;
41+
42+
/**
43+
* Determines whether an item is present in the cache.
44+
*
45+
* @param string $key The cache item key.
46+
* @return bool
47+
*/
48+
public function has(string $key): bool;
49+
50+
}

‎src/Matcher/UrlMatcher.php‎

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
namespace MediaEmbed\Matcher;
66

7+
use MediaEmbed\Cache\CacheInterface;
8+
79
/**
810
* URL matcher with optional domain-based caching for faster lookups.
911
*
@@ -12,6 +14,12 @@
1214
*/
1315
final class UrlMatcher {
1416

17+
/**
18+
* Cache key for the domain index.
19+
* @var string
20+
*/
21+
private const CACHE_KEY = 'media_embed_domain_index';
22+
1523
/**
1624
* Domain-to-providers index for fast path matching.
1725
*
@@ -31,11 +39,25 @@ final class UrlMatcher {
3139
*/
3240
private bool $indexBuilt = false;
3341

42+
/**
43+
* Optional cache for persisting the domain index.
44+
*/
45+
private ?CacheInterface $cache = null;
46+
47+
/**
48+
* Cache TTL in seconds (default: 1 hour).
49+
*/
50+
private int $cacheTtl = 3600;
51+
3452
/**
3553
* @param array<string, array<string, mixed>> $providers Providers keyed by slug.
54+
* @param \MediaEmbed\Cache\CacheInterface|null $cache Optional cache for persisting domain index.
55+
* @param int $cacheTtl Cache TTL in seconds.
3656
*/
37-
public function __construct(array $providers = []) {
57+
public function __construct(array $providers = [], ?CacheInterface $cache = null, int $cacheTtl = 3600) {
3858
$this->providers = $providers;
59+
$this->cache = $cache;
60+
$this->cacheTtl = $cacheTtl;
3961
}
4062

4163
/**
@@ -49,6 +71,25 @@ public function setProviders(array $providers) {
4971
$this->indexBuilt = false;
5072
$this->domainIndex = [];
5173

74+
// Clear cached index when providers change
75+
if ($this->cache !== null) {
76+
$this->cache->delete(static::CACHE_KEY);
77+
}
78+
79+
return $this;
80+
}
81+
82+
/**
83+
* Set the cache implementation.
84+
*
85+
* @param \MediaEmbed\Cache\CacheInterface|null $cache Cache implementation.
86+
* @param int $ttl Cache TTL in seconds.
87+
* @return $this
88+
*/
89+
public function setCache(?CacheInterface $cache, int $ttl = 3600) {
90+
$this->cache = $cache;
91+
$this->cacheTtl = $ttl;
92+
5293
return $this;
5394
}
5495

@@ -118,6 +159,17 @@ private function buildIndexIfNeeded(): void {
118159
return;
119160
}
120161

162+
// Try to load from cache first
163+
if ($this->cache !== null) {
164+
$cached = $this->cache->get(static::CACHE_KEY);
165+
if (is_array($cached)) {
166+
$this->domainIndex = $cached;
167+
$this->indexBuilt = true;
168+
169+
return;
170+
}
171+
}
172+
121173
$this->domainIndex = [];
122174

123175
foreach ($this->providers as $slug => $provider) {
@@ -136,6 +188,11 @@ private function buildIndexIfNeeded(): void {
136188
}
137189
}
138190

191+
// Store in cache
192+
if ($this->cache !== null) {
193+
$this->cache->set(static::CACHE_KEY, $this->domainIndex, $this->cacheTtl);
194+
}
195+
139196
$this->indexBuilt = true;
140197
}
141198

‎src/MediaEmbed.php‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use MediaEmbed\Matcher\UrlMatcher;
1111
use MediaEmbed\Object\MediaObject;
1212
use MediaEmbed\Provider\PhpFileLoader;
13+
use MediaEmbed\Provider\ProviderCollection;
1314
use MediaEmbed\Provider\ProviderConfig;
1415
use MediaEmbed\Provider\ProviderLoaderInterface;
1516
use URLify;
@@ -508,6 +509,18 @@ public function getProviderOrFail(string $alias): ProviderConfig {
508509
return ProviderConfig::fromArray($host);
509510
}
510511

512+
/**
513+
* Get all providers as a collection.
514+
*
515+
* @param array<string> $whitelist Optional list of slugs to include.
516+
* @return \MediaEmbed\Provider\ProviderCollection
517+
*/
518+
public function getProviders(array $whitelist = []): ProviderCollection {
519+
$hosts = $this->getHosts($whitelist);
520+
521+
return ProviderCollection::fromArray(array_values($hosts));
522+
}
523+
511524
/**
512525
* Add a provider using ProviderConfig DTO.
513526
*

0 commit comments

Comments
 (0)