hownowstephen/php-foursquare
A lightweight PHP client for the Foursquare API, offering simple methods for authentication and accessing endpoints such as venues, users, checkins, and photos. Easily integrate Foursquare data into Laravel or plain PHP applications.
composer require hownowstephen/php-foursquare
If Packagist resolution fails (the project predates modern PSR conventions and its metadata has drifted), add it as a VCS repository and use "autoload": { "classmap": ["vendor/hownowstephen/php-foursquare"] } in composer.json, then composer dump-autoload.
First places to look
FoursquareAPI.php (or the single class file in the repo root) — the entire API surface is there: SetRedirectUrl, GetAuthCode, GetAccessToken, SetAccessToken, Get, Post. Read it once; it's small.Configuration
Add credentials to config/services.php so they are cacheable and not read from env() at runtime:
// config/services.php
'foursquare' => [
'client_id' => env('FOURSQUARE_CLIENT_ID'),
'client_secret' => env('FOURSQUARE_CLIENT_SECRET'),
'redirect' => env('FOURSQUARE_REDIRECT_URI'), // must match console exactly
],
First use case: OAuth handshake → venue search
// app/Services/Foursquare.php
namespace App\Services;
class Foursquare
{
protected \FoursquareAPI $api;
public function __construct()
{
$this->api = new \FoursquareAPI(
config('services.foursquare.client_id'),
config('services.foursquare.client_secret')
);
$this->api->SetRedirectUrl(config('services.foursquare.redirect'));
}
public function authUrl(): string
{
return $this->api->GetAuthCode();
}
public function exchange(string $code): string
{
return $this->api->GetAccessToken($code); // access token string
}
public function searchVenues(string $token, string $near, string $query): array
{
$this->api->SetAccessToken($token);
$raw = $this->api->Get('venues/search', [
'near' => $near,
'query' => $query,
'limit' => 10,
]);
return json_decode($raw, true)['response']['venues'] ?? [];
}
}
Bind it as a singleton in a service provider ($this->app->singleton(Foursquare::class)) so you instantiate the cURL-backed client once per request, not per call.
OAuth flow in Laravel
// routes/web.php
Route::get('/connect/foursquare', fn () => redirect(app(Foursquare::class)->authUrl()));
Route::get('/callback/foursquare', function (Request $request, Foursquare $fs) {
abort_unless($request->filled('code'), 400);
$token = $fs->exchange($request->query('code'));
$request->user()->forceFill([
'foursquare_token' => encrypt($token), // tokens are bearer credentials
])->save();
return redirect('/dashboard');
});
Store the token encrypted in a social_accounts table with a provider column if you support multiple integrations; the token is long-lived and does not need a refresh flow.
Keep the raw client behind your own adapter. The library is a concrete class with PascalCase setters and a mutable token on the instance. In tests you cannot Http::fake() it, and you cannot mock it cleanly. Wrap the five or six endpoints you actually use in App\Services\Foursquare behind an interface, bind a fake in TestCase, and let the third-party class stay at the edge.
Endpoint strings are positional, parameters are associative
$fs->api->Get('users/self/checkins', ['limit' => 20, 'beforeTimestamp' => $ts]);
$fs->api->Post('checkins/add', ['venueId' => $id, 'shout' => 'First!']);
Anything not in the core set (venues/search, venues/VENUE_ID, users/self, checkins/add, checkins/recent) is still reachable through Get/Post — you rarely need the helper methods, and the library adds little beyond URL assembly plus the oauth_token parameter.
Cache aggressively. Venue metadata and category trees change slowly and the API is rate limited per token:
$venues = Cache::remember("fs:venue:{$id}", now()->addHours(12), fn () =>
$fs->get('venues/'.$id, ['v' => config('services.foursquare.version', '20240101')])
);
Route writes through the queue. Check-in posting and venue likes are non-critical; dispatch a job so a Foursquare timeout never blocks a user request. Include the token in the job's payload or reload the user in handle().
Handle the response envelope yourself. Every v2 response is {"meta":{"code":200,...},"response":{...}}. Decode once in your adapter and translate meta.code !== 200 into a typed exception (FoursquareException) so controllers never see raw JSON.
Pagination is limit/offset (or beforeTimestamp for check-in feeds). Do not rely on the library for this; loop in your adapter and cap at a sane page count.
create_function, and curly-brace string offsets will emit deprecations or hard errors on 8.1+. Laravel 10/11 requires 8.1+, so budget time for a fork or a shim, or plan the migration below.false or an empty body on transport failure, and non-2xx responses still return a body. Check $raw === false || $raw === '' before json_decode, and set CURLOPT_TIMEOUT/CURLOPT_CONNECTTIMEOUT yourself if you fork — otherwise a hung Foursquare endpoint hangs a queue worker.SetAccessToken mutates the instance, so a singleton shared across a multi-tenant request will leak one user's token into another's call. Either never share the instance or clone it before setting the token.v query parameter (date-versioned). The library does not inject one; add it to every call or your responses can change shape without warning.X-RateLimit-Remaining / X-Ratelimit-Reset headers, which this library exposes only if you dig into the raw response. If you need them, subclass the request method and stash headers.Http::withToken() covers the same surface with retries, fakes, and timeouts for free.App\Contracts, the real adapter bound in AppServiceProvider, and an in-memory fake bound in tests. Never let a feature test hit the network.oauth_token=... in plaintext; scrub oauth_token and client_secret from any Log::debug of request parameters.How can I help you explore Laravel packages today?