mrgoodbytes8667/twitch-response-bundle
A Laravel/PHP package for integrating Twitch API responses into your application. It provides structured response handling to help standardize Twitch API data and simplify building Twitch-powered features.
Read this first: it is a Symfony bundle, not a Laravel package. In a Laravel app you will use it as a plain library — Composer autoloads the classes fine, you simply never register the bundle in config/app.php and never get the Symfony DI wiring. If a class needs a Symfony service, you instantiate it yourself or bind it in a service provider.
Minimal steps
composer require mrgoodbytes8667/twitch-response-bundle — current release is v0.7.6. Still 0.7.x, still pre-1.0, so pin it exactly (see gotchas) rather than floating on ^0.7.vendor/mrgoodbytes8667/twitch-response-bundle/composer.json and note the autoload.psr-4 root (typically MrGoodBytes8667\…) plus the require block. Confirm what it drags in — symfony/* components especially, and whether symfony/serializer is a hard or dev requirement. The normalizers only work if the Serializer is actually installed.ls vendor/mrgoodbytes8667/twitch-response-bundle/src/ — this package is small enough to read end to end in one sitting, and you should. You are looking for four groups now, not three: response/DTO classes, backed enums, the signature-verification class/service, and a Normalizer/ directory (added to getSupportedTypes() in v0.7.6).app/ layer (see below) before writing anything — don't let framework-adjacent code call the vendor namespace directly from controllers.First use case: EventSub webhook receiver. This is where the package earns its place, because the signature verification is fiddly and easy to get subtly wrong. A minimal route plus a verification middleware is the whole day-one win:
// routes/api.php
Route::post('/webhooks/twitch', TwitchWebhookController::class)
->middleware('twitch.signature');
// app/Http/Middleware/VerifyTwitchSignature.php
public function handle(Request $request, Closure $next)
{
$id = $request->header('Twitch-Eventsub-Message-Id');
$timestamp = $request->header('Twitch-Eventsub-Message-Timestamp');
$signature = $request->header('Twitch-Eventsub-Message-Signature'); // "sha256=..."
$body = $request->getContent(); // raw, unparsed
$expected = 'sha256=' . hash_hmac('sha256', $id . $timestamp . $body, config('services.twitch.webhook_secret'));
abort_unless(hash_equals($expected, (string) $signature), 403);
// also reject stale timestamps (Twitch recommends ~10 min tolerance)
return $next($request);
}
If the package already exposes a verifier object, wrap it in one of your own classes so the rest of your app depends on your interface, not the vendor's.
Second thing to try, now that v0.7.6 landed: hydrate one Helix response through the package's normalizers by hand (php artisan tinker is fine). If that round-trips cleanly, the DTOs become genuinely usable in Laravel; if it doesn't, you've learned in ten minutes that the typed objects are decoration and you should stay on arrays. See the Serializer pattern below.
Adapter layer over the DTOs. Do not sprinkle vendor DTOs through your app. Create app/Twitch/ with a thin mapping layer (TwitchUser, TwitchStream, your own enums) and convert at the boundary. This costs an hour now and is the difference between swapping this package out in an afternoon versus a week. It matters more than usual here: zero stars, zero dependents, single maintainer — actively patched (v0.7.6 is the seventh patch in the 0.7 line) but with no ecosystem safety net.
Standalone Serializer for Helix responses. The v0.7.6 change is the tip-off: the package ships Symfony Serializer normalizers, not hand-written fromArray() factories. That means in Laravel you don't need the bundle — you need a Serializer with those normalizers registered, built once in a service provider and bound as a singleton:
// AppServiceProvider::register()
$this->app->singleton('twitch.serializer', function () {
$normalizer = new \MrGoodBytes8667\TwitchResponseBundle\Normalizer\TwitchUserNormalizer();
return new \Symfony\Component\Serializer\Serializer(
[$normalizer], // normalizers (object -> array)
[$normalizer], // denormalizers (array -> object)
);
});
$user = app('twitch.serializer')->denormalize(
$response->json('data.0'),
\MrGoodBytes8667\TwitchResponseBundle\Response\TwitchUser::class,
'json',
);
Check the normalizer constructors before you write this — if any take arguments, wire them here, not in the controller. Keep this behind your own TwitchResponseHydrator interface so the app never type-hints the vendor class.
EventSub: verify → ack → queue. Twitch retries aggressively. Verify the signature, dedupe on Twitch-Eventsub-Message-Id via Cache::add($id, true, now()->addHours(24)), return 204 immediately, and push the payload to a job. Never do Twitch API calls or DB writes inside the webhook request. Handle the webhook_callback_verification message type by returning the raw challenge field after signature verification.
Typed payload handling. Decode the verified JSON into whatever event/response classes the package provides, then switch on the enum for the subscription type:
$type = TwitchEventType::tryFrom($payload['subscription']['type']);
match ($type) {
TwitchEventType::STREAM_ONLINE => StreamWentOnline::dispatch($payload),
TwitchEventType::STREAM_OFFLINE => StreamWentOffline::dispatch($payload),
default => Log::info('unhandled eventsub type', ['type' => $payload['subscription']['type']]),
};
Always use tryFrom() with a default arm. Twitch adds subscription types and enum values without warning, and a hard from() will throw in production on a payload you've never seen.
Enum as validation source. Backed enums are the useful half of this package. Rule::enum(TwitchUserType::class) (Laravel 9+) and $enum->value at the serialization boundary give you free type safety when building filters/forms over Twitch concepts like broadcaster type, subscription status, or sort order.
Symfony shops. If the same codebase is Symfony, register the bundle in bundles.php, let the extension wire the verifier and normalizers, and inject them. Note that the normalizers' getSupportedTypes() support means they're ready for Symfony 6.3+/7's normalizer contract, so the bundle path is the better-supported one — if you're dual-framework, that's where the package is actually maintained. The Laravel and Symfony usages should share the adapter layer, not the raw vendor classes.
Testing. Store a handful of real, scrubbed webhook bodies as fixtures under tests/Fixtures/twitch/. Round-trip them through your verifier (including a bad-signature case and a stale-timestamp case) and through your DTO mapping. That fixture set is also your regression net when you eventually replace the package — and it's the only thing that will catch a normalizer change on a patch bump.
$request->all() and calling json_encode() on it, will break the HMAC. Verify in middleware before the route, and never touch getContent() afterwards in a way that assumes it's still available.sha256=. Use hash_equals, not ===. This is the single most common source of "why does verification fail" — it's almost never the secret, it's the order or a stray newline in the secret from .env.now() with a tolerance window and reject outside it.getSupportedTypes() needs a recent Serializer. The v0.7.6 addition is required by Symfony 6.3+/7's NormalizerInterface/DenormalizerInterface. On older Symfony it's simply an ignored extra method (harmless); on Symfony 7 the previous releases would have been the problem, so 0.7.6 is effectively your minimum, not a nice-to-have. If you're pinning to a Symfony LTS, pin both together and test the upgrade as a pair.getSupportedTypes() returns determines whether Symfony caches "this normalizer supports class X". A catch-all entry (['*' => true]) disables that cache — irrelevant at webhook volumes, relevant if you start hydrating large Helix pages in a loop. Read the return value in src/Normalizer/ before you assume hydration is cheap.allow_extra_attributes at its default; do not set it to false to "catch drift", or a field Twitch adds next Tuesday turns into a 500.total, newer endpoint fields), you won't see it. Keep the raw array alongside the typed object until you've confirmed coverage.tryFrom() returns null, which silently becomes "unknown" in your match. Log the raw value whenever you hit a default arm so you notice new Twitch values instead of quietly dropping events.null for things like game_id/game_name on offline channels and for optional timestamps. The DTOs may declare these non-nullable or you may assume so — and with normalizers, a null hitting a non-nullable typed property is a TypeError, not a soft failure. Type-test against an offline channel and a brand-new account before trusting them.symfony/http-client, construct them manually in AppServiceProvider and bind to interfaces. The normalizers are the easiest piece to use standalone — start there.NOASSERTION license metadata (read the actual LICENSE file before shipping to a client). The upside versus the last review: the package is being patched — v0.7.6 lands a real forward-compatibility fix — so it isn't abandoned. That also means patch bumps can touch the normalizers you now depend on. Pin the exact version ("mrgoodbytes8667/twitch-response-bundle": "0.7.6", no caret), run composer audit, and treat every bump as needing your fixture round-trip test. Your mitigation is still the adapter layer: when it dies, you delete one directory and rewrite ~200 lines.Http::withToken() plus your own 429/Ratelimit-Reset backoff.composer show mrgoodbytes8667/twitch-response-bundle for the resolved version and deps, grep -rn "function verify" vendor/mrgoodbytes8667/twitch-response-bundle/src to find the verifier, ls src/Normalizer/ to see what's actually hydratable, and php artisan tinker to probe enums (TwitchUserType::tryFrom('affiliate')) before wiring them into validation. Log the raw payload before decoding, once, on every event type you handle — it's the only way to see what the DTO silently dropped.app/ layer, or stop using it for that piece and rely on the raw array. The verification helper and the enums are the parts worth keeping even if you drop the rest.How can I help you explore Laravel packages today?