jeffersongoncalves/laravel-wistia
Laravel Wistia is a lightweight package that integrates Wistia video hosting with your Laravel app, offering a simple API wrapper for uploading, managing, and embedding videos. It helps you add Wistia media features to your projects with minimal configuration.
Initial 1.0.0 covers the core Wistia surface: account, projects, medias, stats, and captions. Minimal setup:
composer require <vendor>/laravel-wistia
php artisan vendor:publish --tag=wistia-config
.env:
WISTIA_API_KEY=your-token
WISTIA_TIMEOUT=10
If your app already centralizes third-party tokens, you can skip the env vars entirely and set services.wistia.token — the package falls back to it.$account = WistiaClient::account();
if ($account === null) {
// bad token, wrong config cache, or network egress blocked
}
Where to look first: config/wistia.php (timeout and token resolution), then the client class to confirm exact method signatures and parameter names — the release notes list the endpoints but not the argument order, so read the source once before wiring up calls.
Best first use case: a read-only dashboard widget or an artisan command that pulls WistiaClient::accountStats() and WistiaClient::projects() to prove credentials and connectivity work in every environment (local, staging, queue workers) before you build write paths.
Never call the client directly from a controller. Wrap it in a small service or repository so the null-safe contract is handled in one place:
class WistiaMediaRepository
{
public function find(string $hashedId): ?array
{
return WistiaClient::media($hashedId);
}
public function update(string $hashedId, array $attributes): bool
{
$result = WistiaClient::updateMedia($hashedId, $attributes);
if ($result === null || $result === false) {
Log::warning('Wistia media update failed', ['media' => $hashedId]);
}
return (bool) $result;
}
}
Mirror metadata locally, don't proxy live. Media and project lists change slowly. Sync them into your own tables via a scheduled command, then join against local records for search, filters, and admin listings. This is the single biggest day-to-day win: you avoid paginating the API on every page load.
// app/Console/Commands/SyncWistiaMedias.php
$medias = WistiaClient::medias(['project_id' => $projectId, 'per_page' => 100]);
if ($medias === null) {
$this->error('Wistia unavailable, keeping existing rows.');
return self::FAILURE; // let the scheduler/alerting see it
}
foreach ($medias as $media) {
WistiaMedia::updateOrCreate(
['hashed_id' => $media['hashed_id']],
['name' => $media['name'], 'duration' => $media['duration'] ?? null, 'synced_at' => now()]
);
}
Cache stats aggressively. accountStats(), project stats, media stats, and the date/engagement/visitor/event endpoints are the most expensive calls and the least time-sensitive. Cache per resource with a short TTL and refresh out-of-band:
$stats = Cache::remember("wistia:media:{$id}:stats", now()->addMinutes(30), fn () =>
WistiaClient::mediaStats($id)
);
Use Cache::remember only where a null result is acceptable to cache — see the gotchas below; for stats dashboards you generally want to avoid caching failures, so prefer an explicit get/put.
Queue anything that writes. Create, update, delete, copy, and caption creation belong in jobs, not in the request cycle. This lets you add retry/backoff. But because the client swallows errors, a naive job will "succeed" on failure — make the job throw explicitly so retries actually fire:
public function handle(): void
{
if (WistiaClient::copyMedia($this->hashedId, $this->destinationProjectId) === null) {
throw new RuntimeException("Wistia copy failed for {$this->hashedId}");
}
}
Captions are worth wiring into your upload pipeline: after a media is created or synced, push English captions with createCaption so accessibility isn't a manual afterthought.
Multi-tenant / per-user tokens. If you serve multiple Wistia accounts, don't rely on the singleton resolving the env token once. Set the token immediately before the call and forget the resolved instance:
config(['wistia.api_key' => $tenant->wistia_token]);
app()->forgetInstance(WistiaClient::class);
$projects = WistiaClient::projects();
Testing. Bind a fake client in the container (or use Http::fake() if it's built on the HTTP client) and assert your repository converts null into the domain outcome you expect. Test the failure branch, not just the happy path — that's where this package's design forces you to write code.
null is overloaded. A null return means "network failure or non-2xx" — it never means "empty result." Confirm what an empty collection looks like (likely []) so your if (! $medias) checks don't confuse "no videos in this project" with "API is down." Treat only === null as failure.false on writes is not "not found." A false from delete/update conflates 404 with 500 with DNS failure. If you need to distinguish, you'll have to log or introspect the underlying response — check whether the package exposes it; if not, keep your own logging wrapper.account() check) and alert on null. Also alert on the rate of nulls, not just a single miss.WISTIA_TIMEOUT at 5–10s is fine for account() but the date-range and engagement endpoints can be slower. Raise it for those calls if the package allows per-request overrides; otherwise set it high enough for your slowest endpoint and accept the slower failure path.WISTIA_API_KEY or services.wistia.token in production, run php artisan config:cache — an old cached config is the most common "it works locally" cause. Remember the fallback order: env key first, then services.wistia.token.queue:restart). Relevant if you swap tokens per tenant.project_id, per_page, page, and whether media/project identifiers are numeric IDs or hashed IDs per method. Guessing here produces 404s that surface as null and look like outages.(array) casts or a small DTO before handing data to views.How can I help you explore Laravel packages today?