suncat/weather-underground
A Laravel/PHP wrapper for the Weather Underground API that lets you fetch current conditions and forecasts for a given location. Configure your API key and query weather data through a clean, simple interface in your applications.
This is a Symfony2 bundle, not a Laravel package, so "getting started" means either spinning it up inside a Symfony 2.x kernel to study it, or reading the source to port the client. Given the repo has zero stars/dependents and the upstream API is defunct, treat it as a reference implementation, not a dependency.
Minimal path to inspect it:
composer require suncat/weather-underground
Register the bundle in app/AppKernel.php:
$bundles = [
// ...
new Suncat\WeatherUndergroundBundle\WeatherUndergroundBundle(),
];
Configure the key in app/config/config.yml (confirm the exact root key in DependencyInjection/Configuration.php):
suncat_weather_underground:
api_key: "%weather_underground_api_key%"
Where to look first:
DependencyInjection/Configuration.php — the real config schema (api_key, base URL, maybe timeout).Resources/config/services.yml — the service ID you actually inject; aliases are often not intuitive.Service/ or Client/ directory — the HTTP wrapper class is the only thing worth porting.First use case: current conditions for a city.
$client = $this->get('suncat_weather_underground.client'); // verify ID in services.yml
$conditions = $client->getConditions('CA', 'San_Francisco');
Before writing that line, verify the service ID — the bundle predates autowiring, so you're pulling from the container by string. grep -r "public: true" Resources/config/ or bin/console debug:container weather in a Symfony 2.x app is faster than guessing.
If you're staying in Laravel: do not install this. Copy the client class, drop the Symfony DI annotations, and rewire it to Illuminate\Support\Facades\Http. See the porting pattern below.
1. Inject, don't resolve from the container. The bundle's value in a real app is that the client is a service. Move the call behind your own interface so you can swap providers later (you will — see Gotchas).
final class WeatherService
{
public function __construct(private WeatherUndergroundClient $client) {}
public function current(string $state, string $city): WeatherSnapshot
{
$raw = $this->client->getConditions($state, $city);
return WeatherSnapshot::fromArray($raw);
}
}
2. Wrap it in a cache decorator. Free-tier WU allowed ~500 calls/day and ~10/min. Even when the API worked, you cached. The decorator also becomes your seam for tests:
class CachedWeatherService implements WeatherProvider
{
public function __construct(
private WeatherProvider $inner,
private CacheItemPoolInterface $cache,
private int $ttl = 600,
) {}
public function current(string $state, string $city): WeatherSnapshot
{
$key = "wu:{$state}:{$city}";
return $this->cache->getItem($key)->get() ?? $this->refresh($key, $state, $city);
}
}
3. Porting the client to Laravel — the pragmatic move for a Laravel dev. The bundle's client is a thin HTTP wrapper; replace its transport with the HTTP facade and keep the response normalization:
class WeatherUndergroundClient
{
public function __construct(
private string $apiKey,
private string $baseUrl = 'http://api.wunderground.com/api',
) {}
public function getConditions(string $state, string $city): array
{
$url = sprintf('%s/%s/conditions/q/%s/%s.json', $this->baseUrl, $this->apiKey, $state, $city);
return Http::timeout(5)
->retry(2, 200)
->get($url)
->throw()
->json('current_observation', []);
}
}
Bind it in a service provider and expose a facade; then you can fake it in tests with Http::fake() instead of mocking a Symfony container.
4. Scheduled ingestion. Forecasts are naturally a cron job, not a request-time call. In Laravel, a Schedule::command('weather:sync')->hourly(); in Symfony2, a console command wired through crontab. Pull the data, persist it, and serve pages from your own table — this decouples your app from provider outages and rate limits.
5. Response normalization at the boundary. The API returns current_observation, forecast, hourly_forecast, etc. — deeply nested and inconsistent across endpoints. Map to your own DTO in one place (WeatherSnapshot::fromArray) so a provider swap is a single-file change.
6. Test seam. Wrap the transport in a WeatherTransport interface with a FakeTransport returning fixture JSON. Do not assert on the live API — the bundle almost certainly has no tests of its own, so yours are the only safety net.
api.wunderground.com no longer return usable data. Any tutorial or example in this bundle's README is historically accurate and operationally dead. Budget for a replacement (Open-Meteo needs no key; OpenWeatherMap, WeatherAPI, or Tomorrow.io if you need forecasts/SLA) regardless of how clean the integration looks.composer.json likely constrains symfony/symfony to ~2.x and PHP to ~5.x, which will conflict with any modern project. Installing it in a current Laravel app is not viable — port the class.Configuration.php and services.yml yourself; they are the docs.config.yml, Symfony2 silently ignores the config and the client gets a null API key. Run bin/console config:dump-reference suncat_weather_underground to see the real schema. In the Laravel port, an empty WU_API_KEY env var is the same trap — fail fast in the constructor.file_get_contents/curl directly rather than injecting a Guzzle client, which makes mocking painful. If that's the case here, the extension point is to decorate the client service with your own interface wrapper rather than subclassing.json_decode($response) on a raw string with no status check. In the port, use ->throw() and map 401/429/5xx to distinct exceptions; 429 (rate limit) should feed your cache TTL backoff.current_observation keys vary by station and by endpoint; defensive reads ($data['current_observation']['temp_c'] ?? null) beat assuming presence. City lookups via q/CA/San_Francisco.json are brittle for non-US addresses — geocode to lat/long upstream and use the lat/long query form.services.yml to substitute a decorated client, and keep your own interface as the injection target in application code. That way the bundle's class name only appears in one service file, and replacing it later is a wiring change, not a refactor.Http::fake()-backed tests.How can I help you explore Laravel packages today?