dejwcake/yahoo-finance
dejwcake/yahoo-finance is a Laravel/PHP package for integrating Yahoo Finance market data into your applications. Use it to retrieve stock quotes and financial information through a simple API, making it easier to add market data features to your projects.
Install and configure (5 minutes).
composer require dejwcake/yahoo-finance
php artisan vendor:publish --provider="DejwCake\YahooFinance\YahooFinanceServiceProvider"
Set the key in .env and cache config:
YAHOO_FINANCE_API_KEY=your-yfapi-net-key
php artisan config:cache
Important framing before you write a line of code: this is not a scraper or a client for Yahoo's own endpoints. It talks to yfapi.net, a commercial third-party API proxy that requires a subscription key. Budget for that key and its quota as part of adopting the package — the apiKey, apiUrl, and timeout values all live in config/yahoo-finance.php.
Where to look first, in this order:
README.md — consumer-facing usage; the only consumer-oriented doc.config/yahoo-finance.php — three knobs: apiUrl, timeout, apiKey.src/YahooFinanceClient.php — the entire public surface. Two methods: getStockQuote() and getStockHistory(), plus the FETCH_OBJECT / FETCH_RESPONSE constants. Treat this file as the authoritative signature reference; it is small.src/Models/Enums/ — Range, Interval, Region, Lang, each backed and shipped with EnumAsArrayTrait / EnumForSelectTrait (handy for building a <select> in a chart UI).src/EndPoints/ — only if you need to know exactly which query parameters each call accepts.First use case: a watchlist quote.
There is no facade. Inject the client — it's bound as a singleton by the service provider, already carrying the X-API-KEY header on its Guzzle client.
declare(strict_types=1);
namespace App\Services;
use DejwCake\YahooFinance\YahooFinanceClient;
final readonly class MarketDataService
{
public function __construct(private YahooFinanceClient $yahooFinanceClient)
{
}
public function quotes(array $symbols): mixed
{
return $this->yahooFinanceClient->getStockQuote($symbols);
}
}
By default the client fetches FETCH_OBJECT — hydrated response models parsed from JSON by the factories in src/Models/ResponseModels/Factories/ — so you get objects, not arrays or raw bodies.
Wrap the client in your own gateway. Bind a thin MarketDataService (as above) and inject that everywhere. Two reasons: the package's models are final readonly, so you cannot extend them cleanly; and a single wrapper gives you one place to add caching, retries, and DTO mapping, and one place to swap the package if the GPL/licensing or the yfapi.net dependency becomes a problem.
Quotes — cache hard, refresh on a schedule. Quote calls are the highest-volume, lowest-value-per-call operation. Cache per symbol for 30–60 seconds and refresh the hot set from a scheduled job rather than calling inline on page render:
$quotes = Cache::remember(
sprintf('quotes:%s', md5(implode(',', $symbols))),
now()->addSeconds(60),
fn () => $this->marketDataService->quotes($symbols),
);
History — persist it, don't re-fetch it. getStockHistory() is the call that maps most naturally onto a database table. Take a Range + Interval enum pair, fetch once, store the rows, and read from your own table afterwards. A nightly backfill job that requests Range::OneYear at Interval::OneDay per tracked symbol is a good default shape and keeps your yfapi.net quota predictable.
Use the enums for user input. Range::from($request->string('range')) and Interval::tryFrom(...) give you validation for free, and EnumAsArrayTrait / EnumForSelectTrait exist specifically so you can feed the options straight into a form select without hand-maintaining a list.
Reach for FETCH_RESPONSE when you need the wire. Passing YahooFinanceClient::FETCH_RESPONSE returns the raw PSR-7 response instead of hydrated models. Use it to debug a parse failure, to forward a payload verbatim to a front end, or to parse into your own DTO when the package's models don't carry a field you need. FETCH_OBJECT is the default; prefer it for production.
Push bulk work onto the queue. Chunk your symbol list, dispatch one job per chunk, and let queue concurrency absorb the latency. Never call these methods in a loop inside a web request — with a single global timeout, N sequential calls means N × timeout worst-case latency.
Standalone PHP (no Laravel). The only constructor is YahooFinanceClient::create($httpClient, $logger), which resolves PSR-17 factories via php-http/discovery. Pass any PSR-18 client (a Guzzle instance with the X-API-KEY header) and a PSR-3 logger, and you get the same object minus the service provider.
Testing. Bind a mock of your MarketDataService in the container for feature tests, or — for a true integration test — construct the client with a mocked PSR-18 client returning a recorded JSON body, exactly as the package's own tests/Functional/YahooFinanceClientTest.php does. Keep fixtures as real, trimmed API responses rather than invented ones; that's how the upstream suite does it and it's the right call.
The GPL-3.0 license is the biggest thing on this page. This package is GPL-3.0, not MIT. For a network-delivered SaaS this is usually defensible, but for anything you ship to a customer (an installed app, a bundled binary, a distributed plugin) it warrants a legal opinion before you build a feature on it. Decide this before you write the integration, not after.
You need a yfapi.net subscription, and quotas are real. Quota exhaustion and 429s are operational incidents, not bugs — handle them where you handle HTTP failures and surface "data unavailable" in the UI rather than an exception page. Never proxy the API key to a browser: all calls are server-side for a reason, and the key is a billing credential.
Http::fake() will not intercept this. The package uses its own Guzzle client (with php-http/discovery PSR-17 factories), not Laravel's HTTP client, so the usual test double does nothing and your suite will hit the real API. Mock your own gateway, or inject a fake PSR-18 client into YahooFinanceClient::create().
Timeouts are global, not per-call. There is one timeout in config feeding one singleton Guzzle client. If you need a longer timeout for a heavy history backfill and a short one for web requests, bind a second client built via create() with different options — don't mutate the singleton.
No caching, no retry, no backoff in the package. Every one of those is your job. Add a retry with jitter for transient 5xx/429s at the gateway layer, and always cache history. This is the single largest source of "why are we getting rate-limited" tickets.
Exceptions are status-mapped, so catch granularly. The BaseEndpoint translates HTTP status into specific exception types under src/ApiClient/Exceptions/. Catch those, not \Throwable, so you can distinguish "unknown symbol" (retry is pointless) from "rate limited" (retry with backoff is right). Note that invalid tickers may come back as a successful response with an empty result set — check for emptiness, not just for exceptions.
Symbols are stringly-typed. Exchange suffixes (.L, .DE, .TO) and index carets (^GSPC) pass through untouched. Normalize and validate symbols at your boundary; garbage in produces confusing empty results out rather than a clean 400.
env() only works inside config/. Standard Laravel rule, but easy to trip over here: if you add config keys, read them via config('yahoo-finance.*') everywhere else, or config:cache will silently break them.
Debugging: switch to FETCH_RESPONSE, log $response->getStatusCode() and the request URI, then dump the body. Nine times out of ten the problem is quota, key, or symbol format — not the package.
Extension point — add an endpoint without forking. A new API call is a mechanical four-piece change: request model, endpoint class (URI, method, query params, transformResponseBody()), response model + factory, and a client method. Keep that code in your own app and compose around the client; forking takes you straight back into the GPL conversation and away from upstream fixes.
Maintenance posture: pin and audit. Single maintainer, near-zero stars, GPL, and an AGENTS.md implying AI-assisted contributions. Pin ^2.0, review the lockfile diff on every bump, and add a smoke test against a recorded fixture so an upstream patch regression surfaces in your CI rather than in production. Supported runtimes are PHP 8.2–8.4 (code is written to run on 8.2 even where 8.5 appears in dev tooling) and Laravel 10–12 — don't infer 8.5 runtime support from the CI matrix.
How can I help you explore Laravel packages today?