jcroll/foursquare-api-client
A PHP client for the Foursquare API that simplifies authentication and requests to endpoints like venues, users, and check-ins. Works in Laravel or standalone PHP projects, helping you query location data without boilerplate.
1. Install with an explicit PSR-7/HTTPlug implementation. The package ships no HTTP transport of its own — it declares php-http/client-implementation and relies on php-http/discovery. On a modern Laravel app you must install an adapter yourself, otherwise you get a discovery exception at runtime:
composer require jcroll/foursquare-api-client
composer require php-http/guzzle7-adapter # or guzzle6-adapter on older apps
2. Configure credentials. Foursquare v2 userless auth needs only a client ID/secret pair plus a mandatory v (version date) parameter:
// config/services.php
'foursquare' => [
'client_id' => env('FOURSQUARE_CLIENT_ID'),
'client_secret' => env('FOURSQUARE_CLIENT_SECRET'),
'version' => env('FOURSQUARE_VERSION', '20180201'), // YYYYMMDD, required
'mode' => 'foursquare', // base URL flavour; 'venues' scopes to /v2/venues/
],
3. Bind it once in a service provider. FoursquareClient::factory() wires up the discovered HTTP client with the package's auth, version and endpoint plugins:
// app/Providers/FoursquareServiceProvider.php
use Jcroll\FoursquareApiClient\Client\FoursquareClient;
public function register(): void
{
$this->app->singleton(FoursquareClient::class, fn () => FoursquareClient::factory(
config('services.foursquare')
));
}
4. First use case — a venue search. This is what 90% of integrations use the package for: venues/search to enrich a location, address, or free-text place query.
use Jcroll\FoursquareApiClient\Client\FoursquareClient;
$response = app(FoursquareClient::class)->get('venues/search', [
'near' => 'Chicago, IL',
'query' => 'coffee',
'limit' => 10,
]);
$payload = json_decode((string) $response->getBody(), true);
$venues = $payload['response']['venues'] ?? [];
Look first at src/Client/FoursquareClient.php (what factory() accepts and returns) and src/Plugin/ (auth, version, endpoint). Both are small and are the entire public surface.
Wrap the client in a domain service — never inject it into controllers. The client returns a PSR-7 response with a Foursquare meta/response envelope; that shape should not leak into your application layer.
final class VenueSearch
{
public function __construct(private FoursquareClient $client) {}
public function nearby(float $lat, float $lng, string $query, int $limit = 10): array
{
$response = $this->client->get('venues/search', [
'll' => "{$lat},{$lng}",
'query' => $query,
'limit' => $limit,
'intent' => 'browse',
]);
return $this->unwrap($response);
}
private function unwrap($response): array
{
$body = json_decode((string) $response->getBody(), true);
if (($body['meta']['code'] ?? 500) !== 200) {
throw new FoursquareException($body['meta']['errorDetail'] ?? 'Unknown error');
}
return $body['response'];
}
}
Cache aggressively. Foursquare quotas are per-hour and per-endpoint; venue data is effectively static on a request timescale. A short TTL turns a quota problem into a non-problem:
$venues = Cache::remember("fsq:search:{$key}", now()->addHours(6), fn () => $search->nearby(...));
Push bulk enrichment onto the queue. Bulk geocoding hundreds of records inline will burn your quota and block the request:
class EnrichVenue implements ShouldQueue
{
use InteractsWithQueue, Queueable;
public int $tries = 3;
public int $backoff = 30;
public function handle(VenueSearch $search): void { /* ... */ }
}
Throttle the queue with Redis::throttle('foursquare')->allow(50)->every(60) rather than relying on retries to absorb 429s.
Use plugins rather than editing vendor code for cross-cutting concerns. The factory composes an HTTPlug PluginClient, so you can append RetryPlugin, LoggerPlugin, or your own Plugin implementation for request signing. If a package version doesn't expose plugin injection, bind your own HttpClient and pass it in, or wrap sendRequest().
Test against a mock client. Bind php-http/mock-client in the test container so FoursquareClient never touches the network:
$mock->addResponse(new Response(200, [], file_get_contents(base_path('tests/fixtures/venues.json'))));
$this->app->instance(ClientInterface::class, $mock);
Keep a response-fixture directory. Foursquare payloads are deep and vary by intent (checkin, browse, global, match); fixtures are the only sane way to write assertions against response.venues[].location.formattedAddress.
php-http v1 constraints, which conflict with newer packages in a Laravel 10/11 app. If Composer fights you, isolate it behind a ClientInterface you own and plan a migration; don't let its types spread across your codebase.meta.code is the real status code — a bad OAuth token, a rate limit, or a deprecated endpoint all come back as HTTP 200 with meta.code 401/429/400. Always check meta.code; the client will not throw for these.v is mandatory. Omitting or malforming the version date (YYYYMMDD) returns a 400 "Invalid version" inside the envelope. Put it in config, never hardcode it in calls.client_id/client_secret to the URL, so they land in access logs, queue payload dumps, and exception traces. Scrub client_secret in logging/exception handlers, or move to a token-based plugin.(string) $response->getBody() consumes the stream. If you need the raw body for logging and parsing, rewind() first or capture the string once.php-http/client-implementation is installed — not that the package is misconfigured.places-api.foursquare.com), bearer service keys, and different paths/payloads. This client will not follow you there; the endpoint plugin is your extension point if you want to buy time by repointing the base URL while keeping the auth/version plumbing.mode changes the base path. 'venues' scopes requests under /v2/venues/, so get('search', ...) becomes /v2/venues/search. Mixing modes and full paths produces double-segmented 404s — pick one convention per codebase.acting as a user) calls require your own HTTPlug plugin that adds oauth_token and strips the client secret.X-RateLimit-Remaining from the response headers and expose it as a metric; a search loop that looks fine in dev can exhaust quota in minutes in production.Plugin (retry, logging, auth), a swapped HttpClient implementation, and the factory config array. Everything else in the package is internal.How can I help you explore Laravel packages today?