Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Laravel Package

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.

Deep Wiki
Context7

Getting Started

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

  1. config/tmdb.php — what's configurable.
  2. The facade methods are dynamic; 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.
  3. vendor/php-tmdb/laravel/src/ — it's small; read it once and you know exactly what's wired up.

Implementation Patterns

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.

Gotchas and Tips

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:

  1. Depend on 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.
  2. Fork the wrapper and widen the constraints (the code is trivial; usually nothing else breaks).
  3. Evaluate a maintained alternative before committing to this for new work.

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.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment