alchemyguy/youtube-laravel-api
The package is a thin, API-key-driven wrapper around YouTube Data API v3, exposed through a facade. It is a read client first: video, channel, and playlist lookups plus search. Uploads and anything acting on behalf of a user require OAuth and are largely out of scope.
Install and configure:
composer require alchemyguy/youtube-laravel-api
php artisan vendor:publish --provider="Alchemyguy\YoutubeLaravelApi\YoutubeServiceProvider"
Add your key to .env (the published config reads the API key env var, typically YOUTUBE_API_KEY) and get one from Google Cloud Console with YouTube Data API v3 enabled. Then php artisan config:cache — the config is loaded at boot, so a stale cache is a common "my key isn't being picked up" cause.
First use case — video metadata for an embed block:
use Alchemyguy\YoutubeLaravelApi\Facades\Youtube;
$info = Youtube::getVideoInfo('dQw4w9WgXcQ');
// snippet.title, snippet.thumbnails, contentDetails.duration, statistics.viewCount
This is the right first integration: one call, one video, no pagination, no quota anxiety.
Where to look first:
config/youtube.php — it is the whole surface area of configuration.composer.json constraints — v2.0 requires PHP 8.3+ and Laravel 11+. Verify before you plan the upgrade.Wrap every call in a cache. The Data API gives you 10,000 quota units per day per project. search.list costs 100 units; a videos.list lookup costs 1. Uncached, a modest page view can exhaust the day's quota before lunch. Treat the package as a raw HTTP client and put your own cache in front of it:
$info = Cache::remember("yt:video:{$id}", now()->addHours(6), fn () => Youtube::getVideoInfo($id));
Video metadata is stable enough for hours or days; view counts are the only fast-moving field, so either accept staleness or cache that field separately with a shorter TTL.
List a channel's uploads via the playlist trick, not search. search.list with a channel filter costs 100 units and is imprecise. The uploads playlist is UU + the channel ID minus its UC prefix, and playlistItems.list costs 1 unit per page:
$uploadsPlaylist = 'UU' . substr($channelId, 2);
$items = Youtube::getPlaylistItems($uploadsPlaylist, 50);
This is the single biggest quota and correctness win for any "latest videos from channel X" feature.
Resolve by facade in controllers, inject in services. The facade is fine in a controller or Blade-adjacent helper. For anything you want to test or swap, type-hint the underlying client class (or bind an interface to it in a service provider) so you can substitute a fake without touching Youtube::.
Normalize defensively at the boundary. The API returns a nested payload; a deleted, private, or region-blocked video comes back as an empty items array rather than a clean error. Assume every lookup can yield nothing and centralize the shape:
class VideoLookup
{
public function find(string $id): ?array
{
$items = Cache::remember("yt:video:{$id}", now()->addHours(6),
fn () => Youtube::getVideoInfo($id)['items'] ?? []);
return $items[0] ?? null;
}
}
Push bulk work to queues. Backfilling 500 videos is 500 HTTP round trips. Chunk into jobs of 50 IDs, batch IDs into a single videos.list call where the package method allows comma-separated IDs, and let the queue absorb the latency.
Fake HTTP in tests. Because the package ultimately issues HTTP requests, Http::fake() with a recorded fixture payload gives you fast, quota-free tests. Record one real response per endpoint shape (found, empty, error) and reuse them.
Budget quota per feature, not per app. Keep a short comment or a config constant next to each integration stating its unit cost. When search creeps into three features, you want that visible before the 403s start.
v2.0 is a hard platform bump. PHP 8.3+ and Laravel 11+ are now required. If you are on Laravel 10 or PHP 8.1/8.2, this is a blocking upgrade, not a patch — schedule it with the framework upgrade, not as a standalone composer update. There is no deprecation shim between 1.0.3 and 2.0.0. The upside is that the "full bug-fix sweep" claim means many v1 rough edges around response handling and error surfacing are worth re-testing rather than working around; if you carried defensive hacks for 1.x behaviour, re-check them under 2.0 and delete the ones that no longer apply.
Key restrictions are the classic 403 source. A server-side key restricted by HTTP referrer will fail; restrict by IP, or leave it unrestricted and rely on server-only usage. Read the Google error payload in the response body — keyInvalid, quotaExceeded, and ipRefererBlocked all surface as 401/403 but need completely different fixes.
Search is the expensive escape hatch. Budget 100 units and use it only for genuine free-text queries. Anything channel-, playlist-, or ID-shaped has a 1-unit alternative.
Pagination is manual. Responses carry nextPageToken; loop until it's absent. Cap the loop — an unbounded crawl over a large playlist is a quota incident waiting to happen.
Private and unlisted video access needs OAuth. An API key sees public data only. If a feature "works in testing but not for the client's unlisted videos," that's the cause, and no config change fixes it — it needs a different auth flow than this package provides.
Negative caching matters. A deleted video ID will be queried repeatedly by an embed component. Cache the empty result too, briefly, so misses don't burn quota.
Debug with a raw client, not the facade. When a call misbehaves, Http::get('https://www.googleapis.com/youtube/v3/videos', [...]) reproduces it in one line and shows you the full error body the package may be collapsing into a generic exception.
Extension points are thin, by design. There is no middleware pipeline or response DTO layer to hook into. If you need retries with backoff, circuit breaking, or response transformation, wrap the call site (a small repository or decorator around the client) rather than trying to extend the package. Attempting to subclass the service class tends to be more brittle than a 30-line wrapper you fully control.
Pin the major version deliberately. Given a 1.x → 2.x jump with no intermediate deprecations, add a constraint you've consciously chosen and let Dependabot surface the next major as a PR you review, not an automatic merge.
How can I help you explore Laravel packages today?