webproject-xyz/php-symfony-tibber-api-bundle
A Symfony bundle for integrating the Tibber API into PHP applications, offering ready-to-use services and configuration to query electricity prices, consumption data, and other Tibber resources with minimal setup and clean dependency injection.
Before you composer require anything, understand what this is: webproject-xyz/php-symfony-tibber-api-bundle is a Symfony bundle, not a Laravel package. It ships a *Bundle class, DependencyInjection/ extension + compiler passes, and expects Symfony's container, Configuration tree, and autoconfiguration. Laravel will not discover or boot it automagically — config/app.php providers list is irrelevant to it.
That said, 1.0.0 makes it usable from Laravel in two realistic ways:
Option A — use it as a plain library (recommended for most Laravel apps). Install it, ignore the bundle wiring, and resolve the underlying client/service classes yourself in a service provider:
composer require webproject-xyz/php-symfony-tibber-api-bundle:^1.0
// app/Providers/TibberServiceProvider.php
public function register(): void
{
$this->app->singleton(TibberClient::class, fn () => new TibberClient(
token: config('services.tibber.token'),
));
// one named binding per Tibber account/home
foreach (config('services.tibber.accounts', []) as $name => $cfg) {
$this->app->bind("tibber.{$name}", fn () => new TibberClient($cfg['token']));
}
}
Option B — borrow the config shape only. Read DependencyInjection/Configuration.php in the package to learn the supported keys (accounts, cache TTLs, HTTP options), then mirror them into config/services.php. Do not copy the XML/YAML service definitions; they reference Symfony-only services (cache.app, http_client, event_dispatcher).
Where to look first: the DependencyInjection/ folder (account schema + compiler passes — it's the de-facto API documentation for multi-account), then the cache layer to see how keys are derived, then the console commands (they reveal the CLI error-boundary behaviour fixed in 1.0.0).
First use case: an hourly price widget. Pull today's + tomorrow's prices for the configured home, cache them, render a blade chart, and refresh on a scheduler tick. That exercises auth, multi-account resolution, and the cache invalidation path — the three things most likely to bite you.
⚠️ Platform check first: Symfony 8 requires PHP 8.4+. Verify php -v before anything else; a Laravel 10/11 app on PHP 8.2 cannot install this at all.
Multi-account as first-class, not an afterthought. 1.0.0's headline feature is multiple Tibber accounts/homes in one process. In Laravel terms, think of it as multi-tenant config:
// config/services.php
'tibber' => [
'default' => env('TIBBER_ACCOUNT', 'home'),
'accounts' => [
'home' => ['token' => env('TIBBER_TOKEN_HOME')],
'cabin' => ['token' => env('TIBBER_TOKEN_CABIN')],
'rental' => ['token' => env('TIBBER_TOKEN_RENTAL')],
],
],
Resolve per-request or per-job with a small resolver rather than reaching into globals — this is the Laravel equivalent of the bundle's compiler-pass tagging:
$client = app(TibberClientResolver::class)->for($user->tibber_account);
Cache outside the bundle's cache layer. The bundle's "smart caching" assumes a PSR-6/PSR-16 pool. In Laravel, either inject a Symfony\Component\Cache\Adapter\Psr16Adapter wrapping Laravel's Cache, or — usually simpler and more observable — disable the bundle cache and wrap calls in Cache::remember yourself:
$prices = Cache::remember(
"tibber:{$account}:prices:".now()->format('Y-m-d-H'),
now()->addMinutes(30),
fn () => $client->getPrices(),
);
Reasons to prefer your own layer: you get Laravel's tags, cache()->forget(), Horizon visibility, and a key format you control. The 1.0.0 fix list ("key lengths, invalidation") tells you the internal key derivation is subtle enough that you don't want to debug it blind.
Queue the fetch, cache the result. Tibber's API is rate-limited and your prices only change on the hour. The clean pattern:
// app/Jobs/RefreshTibberPrices.php — scheduled hourly, slightly after the hour
Schedule::job(new RefreshTibberPrices)->hourlyAt(2);
public function handle(): void
{
foreach (config('services.tibber.accounts') as $name => $cfg) {
try {
Cache::put(
"tibber:{$name}:prices:today",
app(TibberClientResolver::class)->for($name)->getPrices(),
now()->endOfDay(),
);
} catch (Throwable $e) {
report($e); // never let one account kill the loop
$this->fail($e); // or continue, and fail the batch at the end
}
}
}
Timezones are the load-bearing detail. Tibber prices are hourly in the home's local timezone, and DST transitions create 23- and 25-hour days. Always store timestamps UTC and convert at the view layer; never build daily buckets by now()->startOfDay() unless you know the home's tz. The 1.0.0 "caching timezone" fix is the same class of bug — assume it can recur in your own wrapper.
Console commands are worth borrowing, not running. The bundle's CLI commands are Symfony Console commands with a documented error boundary (partial failures → non-zero exit, per-account messages). If you need scheduled syncs, model your Artisan command on that contract: per-account try/catch, aggregate exit code, structured output. Don't try to register Symfony commands into Artisan — write Artisan equivalents.
composer.json before planning; a failed install here usually means a too-old PHP, not a bad package.Symfony\Component\HttpKernel\Bundle\Bundle subclasses are inert in Laravel. Don't add it to any provider list and don't expect tibber.* container aliases to exist — construct services manually.config/packages/tibber.yaml is YAML you must translate by hand. Treat the Symfony config tree as the spec, not as something that works.hash('xxh128', $account.'|'.$range). The 1.0.0 fix addressed this — your own wrapper can reintroduce it.now()->addHour()->startOfHour()), never a flat addHour().handle(). Use ShouldBeUnique per account to prevent thundering-herd fetches when several scheduled jobs overlap.^1.0 and add a contract test that asserts today's price array has the shape your Blade views expect, so a 1.0.x patch can't silently break rendering.try/catch (Throwable) and decide policy explicitly: stale-cache-serve (Cache::get fallback), retry with backoff, or fail the job. Tibber auth tokens expire rarely, but per-home token misconfiguration is the common failure and should be reported with the account name attached.Http::buildClient() (or a Guzzle instance with HandlerStack middleware) to get logging, retries, and X-Request-Id propagation for free — far more valuable than fighting the container.Http::withToken()->post() is a smaller dependency than a Symfony bundle bridged into Laravel — and avoids the whole PHP 8.4 / container-wiring conversation.How can I help you explore Laravel packages today?