php-tmdb/laravel
A Laravel wrapper for The Movie Database (TMDB) API that provides a service provider, facade, and configuration for fetching movies, TV shows, actors, and collections. Easily search TMDB and add media-powered features to Laravel apps with familiar syntax.
Install
composer require php-tmdb/laravel
This pulls in wtfzdotnet/php-tmdb-api as the underlying API client. The Laravel package is a thin wrapper: a service provider that registers a configured Tmdb\Client, plus a facade. Nothing more.
Register (Laravel < 5.5, or if auto-discovery doesn't fire)
// config/app.php
'providers' => [
Tmdb\Laravel\TmdbServiceProvider::class,
],
'aliases' => [
'TMDB' => Tmdb\Laravel\Facades\TMDB::class,
],
Facade namespace changed between releases (Tmdb vs TMDB, Facades\Tmdb vs Facades\TMDB). If the alias above errors, check vendor/php-tmdb/laravel/src/Facades/ for the actual class name. The same applies to the provider class name if TmdbServiceProvider is not found.
Publish config and set your key
php artisan vendor:publish --provider="Tmdb\Laravel\TmdbServiceProvider"
# .env — this must be a TMDB v3 API key from
# https://www.themoviedb.org/settings/api
TMDB_API_KEY=your_v3_key_here
config/tmdb.php is the only config surface. It holds the API key (usually env('TMDB_API_KEY')), language and a few client options passed straight to the underlying library.
First use case
use Tmdb\Laravel\Facades\TMDB;
$movie = TMDB::getMoviesApi()->getMovie(550); // Fight Club
echo $movie->getTitle();
echo $movie->getReleaseDate()->format('Y');
$results = TMDB::getSearchApi()->searchMovies('the matrix');
foreach ($results['results'] as $result) { /* ... */ }
Where to look first
config/tmdb.php — what's configurable.TMDB::getMoviesApi(), getSearchApi(), getTvApi(), getPeopleApi(), getDiscoverApi(), getGenresApi(), getConfigurationApi() all delegate to the underlying Tmdb\Client. The real API reference is the wtfzdotnet/php-tmdb-api docs, not this package's README.vendor/php-tmdb/laravel/src/ — it's small; read it once and you know exactly what's wired up.Wrap it in your own service class
Don't sprinkle TMDB:: calls across controllers. Bind a small service that owns caching, error handling and response mapping:
namespace App\Services;
use Tmdb\Laravel\Facades\TMDB;
class MovieService
{
public function find(int $tmdbId): ?array
{
return cache()->remember("tmdb:movie:{$tmdbId}", now()->addDay(), function () use ($tmdbId) {
try {
$movie = TMDB::getMoviesApi()->getMovie($tmdbId, ['append_to_response' => 'credits,videos']);
} catch (\Tmdb\Exception\TmdbApiException $e) {
if ($e->getCode() === 404) {
return null;
}
throw $e;
}
return [
'id' => $movie->getId(),
'title' => $movie->getTitle(),
'overview' => $movie->getOverview(),
'poster' => $movie->getPosterPath(),
'runtime' => $movie->getRuntime(),
];
});
}
}
Prefer constructor injection for testability
use Tmdb\Client;
class Importer
{
public function __construct(private Client $tmdb) {}
public function import(int $id) {
return $this->tmdb->getMoviesApi()->getMovie($id);
}
}
The container resolves Tmdb\Client because the provider binds it as a singleton. This is easier to fake in tests than the facade.
Caching — use Laravel's cache, not the library's
The underlying library has its own PSR-6 caching story, but it isn't automatically bridged to Laravel's cache driver in this wrapper. In practice, the reliable pattern is Cache::remember() around every call (as above). Key namespaces that make invalidation possible: tmdb:movie:{id}, tmdb:search:{hash}, tmdb:config.
append_to_response is a big win here — one cached call instead of three:
TMDB::getMoviesApi()->getMovie($id, [
'append_to_response' => 'credits,videos,images,recommendations',
]);
Images need a base URL you build yourself
The API returns paths like /abc123.jpg, not full URLs. Pull the base from the configuration endpoint once, cache it, then concatenate:
$base = cache()->rememberForever('tmdb:image_base', fn () =>
TMDB::getConfigurationApi()->getConfiguration()['images']['secure_base_url']
);
$url = $base . 'w500' . $movie->getPosterPath();
Bulk work belongs in queued jobs
Search and import flows are the common integration. Keep the request cycle free of multi-page loops:
class ImportPopularMovies implements ShouldQueue
{
public $timeout = 300;
public function handle(MovieService $movies)
{
foreach (range(1, 5) as $page) {
$popular = TMDB::getMoviesApi()->getPopular(['page' => $page]);
foreach ($popular['results'] as $result) {
$movies->find($result['id']);
usleep(250_000); // be polite to the API
}
}
}
}
Localization
Set the language in config/tmdb.php and cache keys per locale. If you need per-request language switching, resolve a fresh Tmdb\Client with overridden options rather than mutating the shared singleton:
$client = app()->makeWith(\Tmdb\Client::class, ['options' => [
'api_key' => config('tmdb.api_key'),
'lang' => app()->getLocale(),
]]);
Testing
TMDB::shouldReceive('getMoviesApi->getMovie')
->once()
->with(550, Mockery::any())
->andReturn($fakeMovie);
Or bind a hand-rolled fake Tmdb\Client in a test service provider — cleaner if you have more than a couple of endpoints.
Blade / frontend
Resolve data in the controller (or a view composer) and pass arrays, not Tmdb\Model\* objects. The models leak library types into views and make template code verbose.
This package is effectively unmaintained (last release 2018-05-02). Its composer.json will constrain illuminate/support to Laravel 5.x. On Laravel 8+ expect composer require to fail on dependency resolution. Options, in order of preference:
wtfzdotnet/php-tmdb-api directly and register the Tmdb\Client yourself in a service provider — you get the same functionality without the stale Laravel coupling.v3 key vs v4 bearer token. This library predates TMDB's v4 auth. The TMDB_API_KEY must be the v3 API key, not the v4 "Read Access Token". Using the v4 token yields 401s that look like a misconfigured key.
Config caching bites on .env changes. If you use php artisan config:cache, editing TMDB_API_KEY does nothing until you re-run php artisan config:clear / config:cache. Also make sure config/tmdb.php actually reads env('TMDB_API_KEY') — some published versions hardcode an empty string.
Facade method names are dynamic. IDEs and static analysis won't autocomplete TMDB::getMoviesApi(). Add a @method docblock or your own thin facade/interface if you want completion and refactor safety.
Exceptions are your control flow. Wrap calls in try/catch (\Tmdb\Exception\TmdbApiException $e):
404 — legitimate "not found" (bad TMDB id, removed title). Return null, don't 500.401 — bad/missing key.429 — rate limited. Retry with backoff; there is no built-in rate limiter, so usleep() in loops and $job->release() in queues are on you.The client is a singleton. Anything stateful (language, session token for write operations, custom Guzzle handler) affects the whole request. Don't mutate it mid-request; build a scoped instance with makeWith() instead.
Responses are typed models, not arrays. Use getters (getTitle(), getReleaseDate(), getRuntime()). getReleaseDate() returns a DateTime object, which will fatal if you assume it's a string. Some fields are null for unreleased/incomplete entries — guard before calling ->format().
The underlying HTTP client options leak through. If you need to raise timeouts, disable http_errors, or add a Guzzle middleware (mock handler, logging), you generally do it by binding your own Tmdb\Client with the appropriate options rather than through config/tmdb.php. That's the main extension point.
Debugging empty responses. Enable Guzzle debug in a local-only service provider binding (or drop a Log::debug() in your wrapper) to see status codes and raw bodies. A silent empty result is almost always an API key or config-cache problem, not a code problem.
Multi-page API responses. Search, popular, discover, etc. return ['page' => 1, 'total_pages' => N, 'results' => [...]]. Iterating results only gets you page 1 — a very common bug when someone "imports everything popular" and gets 20 records.
Don't persist the API key in the database or expose it to the frontend. All calls go server-side; image URLs are the only thing that should reach the browser.
How can I help you explore Laravel packages today?