scheb/yahoo-finance-api
A PHP client for the Yahoo Finance API that lets you fetch stock quotes, historical prices, dividends, splits, and other market data through a simple, expressive interface. Framework-agnostic, no API key required, and easy to drop into any PHP project.
Install via Composer and use the built-in factory to construct a client:
composer require scheb/yahoo-finance-api
use Scheb\YahooFinanceApi\ApiClientFactory;
$client = ApiClientFactory::createApiClient();
$quote = $client->getQuote('AAPL');
First place to look is the ApiClient class — it exposes getQuote, getQuotes, getHistoricalData, getExchangeRate, search, and getScreener. In Laravel, bind the client as a singleton in a service provider so you don't recreate the HTTP client per request:
// AppServiceProvider::register()
$this->app->singleton(ApiClient::class, fn () => ApiClientFactory::createApiClient());
Your first realistic use case is a "quote lookup" endpoint: take a user-supplied symbol, call getQuote(), cache the result, and return a small DTO. Don't reach for the screener or historical endpoints until you've validated symbol normalization (lowercase aapl vs AAPL, BRK.B vs BRK-B).
Note the v5.2.0 change: search() now accepts a region argument. If you're building a symbol autocomplete, pass a region so results are biased toward the right listings:
$results = $client->search('apple', region: 'US');
Wrap the client, don't inject it everywhere. ApiClient is a concrete class with no interface. Create a thin FinanceService that composes it so you can mock, cache, and normalize in one place. Injecting ApiClient directly into ten controllers makes testing painful.
Cache aggressively and defensively. Yahoo's endpoints are rate-limited and occasionally return stale/odd data. Cache quotes for 60–300 seconds with Cache::remember, and cache historical data for hours. Use distinct cache keys per symbol+range+interval; the library does no caching of its own.
Normalize symbol casing upfront. Use a FormRequest prepareForValidation() to uppercase and map common aliases (e.g. BRK.B → BRK-B). Fail fast with a 422 if the symbol doesn't match your accepted pattern rather than letting Yahoo return a confusing empty result.
Batch quotes carefully. getQuotes(['AAPL','MSFT','GOOG']) is one HTTP round-trip — prefer it over looping getQuote(). Chunk large lists (a few dozen symbols) and handle partial failures, since one bad symbol can blank a whole batch response.
Treat search() as autocomplete-only. With v5.2.0's region parameter you can localize results, but the returned payload is loosely structured (quotes, news, options mixed in). Filter by $result->getQuotes() before rendering — don't assume every hit is a tradable symbol.
Use a queued job for heavy pulls. Historical data over multi-year ranges is slow. Push it to a job, store in a price_history table, and let the UI read from the database. This isolates you from upstream latency and rate limits.
Wrap calls in try/catch at the service boundary. The library throws on transport errors. Translate to domain exceptions or a Result type so controllers stay simple. Log the underlying exception; users see "symbol not found" or "data temporarily unavailable".
No Laravel service provider ships with the package. You get a factory, not a facade. Binding it as a singleton avoids rebuilding the Symfony HTTP client on every resolution — meaningful if you call it many times per request.
The client is stateful-ish per instance but the underlying transport isn't pooled by Laravel. If you're firing many sequential calls, reuse the singleton client rather than calling ApiClientFactory::createApiClient() repeatedly.
region in v5.2.0 is a new optional argument. Check your composer.lock — if you're on 5.1.x, passing region will either error or be ignored depending on how you call it. Pin to ^5.2 to be safe. There's no deprecation here, but the method signature changed, so any mock/fake that redefines search() should be updated to match.
Mocks and final classes. ApiClient isn't easy to subclass for fakes. Mock via Mockery against the concrete class (it works) or, better, wrap it in your own interface and mock that. This is the single biggest testability gotcha.
Symbol edge cases bite. Class-share symbols, exchange suffixes (7203.T, VOD.L), and OTC tickers behave differently than US large-caps. Build a small fixture set and assert on real responses before shipping — Yahoo occasionally returns a valid-looking record with null prices.
Historical intervals and ranges are validated loosely. Passing an unsupported interval doesn't always throw — it can return an empty array. Validate interval (1d, 1wk, 1mo, …) in your own code and surface a clear error.
HTTP client configuration is limited. ApiClientFactory::createApiClient() accepts an optional HttpClientInterface for advanced tuning (timeouts, proxies). Set at least a timeout and retry policy there — default timeouts may be too generous for web requests.
Time zones. Returned datetimes are UTC-based; cast and convert explicitly for display. Don't rely on implicit server-timezone conversion — Laravel apps default to UTC, but views/users may not.
Extension points are minimal. There are no events, no custom parsers, no pluggable serializers. Your flexibility lives in the wrapper layer (transforming DTOs, caching, adding retry logic). Plan for that rather than expecting to hook into the library's internals.
How can I help you explore Laravel packages today?