jeffersongoncalves/laravel-apollo
Laravel Apollo is a Laravel package that integrates Apollo GraphQL, providing configuration and helpers to connect Apollo with Laravel applications for simpler GraphQL client setup.
Install and wire it up:
composer require <vendor>/laravel-apollo
php artisan vendor:publish --tag="apollo-config"
Add the credentials to .env — you will need an Apollo API key from Settings → Integrations → API in your Apollo account (a master key, not a user-scoped OAuth token):
APOLLO_API_KEY=your-key-here
Look at config/apollo.php first. That file is where the base URL, timeout, retry counts and any default pagination/rate-limit settings live; almost every "why is this slow / why did it 401" question starts there. Then open the service class behind the facade (typically src/Apollo.php or src/Client/…) — this package is thin enough that the method signatures are the documentation, and return types tell you whether you get arrays, collections or DTOs.
Your first use case should be the read-only one, because it costs no credits and validates the whole chain:
use Vendor\Apollo\Facades\Apollo;
$people = Apollo::people()
->search(['q_organization_domains' => 'acme.com', 'person_titles' => ['CTO']])
->get();
Confirm you get results and that Http::fake() in a test intercepts the call. Once search works, the enrichment endpoints (which do consume credits) are the same shape, so the plumbing is already proven.
Search → enrich, not search-and-hope. Apollo's search response is deliberately thin: names, titles, company, and a has_email-style flag — not the email itself. The normal workflow is: search to get person_ids, persist those IDs, then enrich only the records you actually care about. Build this as two steps from day one rather than enriching inside a ->each() over search results, or you will burn credits on duplicates every time a job retries.
Wrap enrichment in queued jobs. Enrichment is network-bound and can take a second or more per call. A job per record (or per small batch) gives you Laravel's retry/backoff for free:
final class EnrichPerson implements ShouldQueue
{
public int $tries = 3;
public array $backoff = [10, 60, 300];
public function __construct(public string $personId) {}
public function handle(Apollo $apollo): void
{
$person = $apollo->people()->enrich($this->personId);
Prospect::where('apollo_id', $this->personId)->update([...]);
}
}
Use Bus::batch() for bulk lists so you can hook then/catch and report partial failures — bulk enrichment of 500 people will rarely be all-or-nothing.
Bulk endpoints take an array; chunk it yourself. Bulk people enrichment accepts a list of identifiers, but the API caps how many per request. Don't pass an unbounded pluck(); wrap it in ->chunk(10) (verify the current cap in the README) and dispatch one job per chunk. This also keeps a single failed chunk from losing the whole batch.
Normalise into your own model immediately. Apollo's payloads are wide and will change. Map into an Eloquent model or a small DTO in the job, and never let raw Apollo arrays leak into Blade/Inertia/API responses. Keep apollo_id, apollo_raw (json column) and enriched_at on the record — you'll want to know when a record was fetched and be able to re-map without re-paying.
Cache aggressively for search, never for enrichment. Search results are cheap to re-fetch but you'll hammer them in UI autocompletes — cache for a few minutes keyed on the filter payload. Enrichment results are point-in-time and cost credits; store them in the DB with a timestamp instead of a TTL cache, so a cache flush doesn't silently trigger re-billing.
Testing. The package should go through Laravel's HTTP client, so Http::fake() works:
Http::fake(['api.apollo.io/*' => Http::response(['person' => [...]])]);
Fake at the HTTP layer rather than mocking the service class — that way the package's own request building and error mapping are still exercised in your suite.
->each() loops, cache misses and dev-environment seeders all spend real money. Set APOLLO_API_KEY to a throwaway key (or Http::fake()) in local and testing, and never let a seeder run against the live key. A Http::preventStrayRequests() in tests/TestCase.php is cheap insurance.page: 600 will fail rather than return empty.q_organization_domains wants a string, not an array. Several search params accept comma- or newline-separated values where you'd intuitively pass an array. Type errors here surface as silent zero-result responses, not exceptions — a great reason to assert count($results) > 0 in a smoke test.Retry-After header, and set ->retry()/backoff in config/apollo.php conservatively — Laravel's default instant retry will make a 429 worse.timeout in the config (and a longer one on bulk calls) so you get a clean timeout error instead of a queued job that dies ambiguously.config/apollo.php or the env var inside a Docker build and nothing changes, it's php artisan config:cache — the classic first-hour debugging trap.{"error": "..."}, as {"errors": [...]}, and sometimes as a 200 with an empty person array. Wrap the client once in a small ApolloException mapping layer so controllers and jobs don't each need to know the difference, and log the raw response body on failure — 4xx reasons are usually self-explanatory once you can see them.HandlerStack bound into the container, not a fork of the package.composer why-not before bumping the package in a legacy app.How can I help you explore Laravel packages today?