vkcom/vk-php-sdk
Official VK PHP SDK for building applications on the VK API. It provides OAuth authorization, a client for calling API methods, and helpers for responses and uploads, simplifying VK integration in PHP projects.
The SDK is a thin, schema-generated HTTP client — there is no Laravel service provider, facade, or config file. Everything starts from one class:
composer require vkcom/vk-php-sdk
use VK\Client\VkApiClient;
$vk = new VkApiClient('5.199'); // API version is a required constructor argument
The first useful call is almost always a token + users()->get() smoke test:
$users = $vk->users()->get($accessToken, [
'user_ids' => [1, 6492],
'fields' => ['photo_200', 'city'],
]);
Things to know before you write anything:
VK\Client\VkApiClient is the entry point. Every service is a method on it (users(), wall(), photos(), messages(), groups(), friends(), orders(), etc.). Each service method takes the access token as the first argument, then a params array.VK\Client\OAuth\ — OAuth\Client returns an ImplicitFlow (JS / client-side) or AuthorizationCodeFlow (server-side redirect code exchange). The resulting AccessToken object exposes getAccessToken(), getUserId(), getExpiresIn().VK\Client\Exceptions\ — you will spend most of your debugging time distinguishing VkApiException (VK returned an error JSON) from VkClientException (network/transport).response value directly as an array. You will not see ['response' => ...] when writing $vk->users()->get(...).The fastest end-to-end flow in a fresh Laravel app: put a long-lived community (group) access token or service token in config/services.php, bind VkApiClient as a singleton in a provider, and start calling read-only methods like users()->get(), groups()->getById(), wall()->get() before touching OAuth.
Config:
// config/services.php
'vk' => [
'version' => env('VK_API_VERSION', '5.199'),
'token' => env('VK_SERVICE_TOKEN'),
'oauth' => [
'client_id' => env('VK_CLIENT_ID'),
'client_secret' => env('VK_CLIENT_SECRET'),
'redirect' => env('VK_REDIRECT_URI'),
],
],
Provider:
use VK\Client\VkApiClient;
use VK\Client\Enums\VkLanguage;
$this->app->singleton(VkApiClient::class, fn () => new VkApiClient(
config('services.vk.version'),
null, // default CurlRequest; see gotchas below
VkLanguage::ENGLISH,
));
Inject VkApiClient $vk into controllers, jobs, and services. Never new VkApiClient() inside a controller — the version string and transport should be one decision, made once.
The SDK returns plain arrays with inconsistent key casing (user_id, photo_200, first_name). Do not leak those into your domain:
final class VkUserDirectory
{
public function __construct(private VkApiClient $vk) {}
/** @return array<int, array{id:int,name:string,photo:?string}> */
public function byIds(array $ids, string $token): array
{
$rows = $this->vk->users()->get($token, [
'user_ids' => implode(',', $ids),
'fields' => ['photo_200'],
]);
return array_map(fn ($u) => [
'id' => (int) $u['id'],
'name' => trim(($u['first_name'] ?? '') . ' ' . ($u['last_name'] ?? '')),
'photo' => $u['photo_200'] ?? null,
], $rows);
}
}
AuthorizationCodeFlow, not ImplicitFlowFor a Laravel backend, the flow is: redirect the user to VK, receive ?code=, then exchange server-side.
use VK\Client\OAuth\Client as OAuthClient;
use VK\Client\OAuth\Scope;
$oauth = new OAuthClient();
$flow = $oauth->getAuthorizationCodeFlow();
// Step 1 — redirect
$scope = Scope::create()->withFriends()->withPhotos()->withOffline();
$url = $flow->getAuthorizeUrl(
config('services.vk.oauth.client_id'),
config('services.vk.oauth.redirect'),
$scope,
);
// Step 2 — callback
$token = $flow->getAccessToken(
config('services.vk.oauth.client_id'),
config('services.vk.oauth.client_secret'),
config('services.vk.oauth.redirect'),
$request->query('code'),
);
$token->getAccessToken(); // persist encrypted
$token->getUserId();
$token->getExpiresIn(); // store expiry; the SDK will not refresh for you
Request offline scope if you need long-lived tokens; otherwise schedule a refresh job before expires_in elapses.
use VK\Client\CallbackApi\CallbackApiHandler;
final class VkCallbackHandler extends CallbackApiHandler
{
protected function handleMessageNew(array $event): void
{
// $event['object'] and $event['group_id'] are your payload
ProcessVkMessage::dispatch($event['object']);
}
protected function handleWallPostNew(array $event): void { /* ... */ }
}
The base handler verifies the secret, answers the confirmation event automatically, and dispatches to handle{EventName} methods. Register a single POST route, read php://input, and call ->handle().
VK\Client\LongPoll\LongPoll, events parsed via VK\Client\Events\Events) is a better fit than webhooks when you are behind a firewall or need sub-second event delivery. Get server, key, ts from groups->getLongPollServer(), poll, persist ts.VK\Client\StreamingClient) taps firehose rules (streaming()->getRules(), addRule()) — use it for analytics, not for interactive replies.use VK\Client\Exceptions\VkApiException;
use VK\Client\Exceptions\ApiErrorCodes;
try {
return $vk->wall()->post($token, $params);
} catch (VkApiException $e) {
if (in_array($e->getErrorCode(), [
ApiErrorCodes::TOO_MANY_REQUESTS_PER_SECOND, // 6
ApiErrorCodes::FLOOD_CONTROL, // 9
], true)) {
throw new RateLimitedVkException($e->getErrorMessage(), previous: $e);
}
throw $e;
}
Put this in a single base VkJob/middleware rather than repeating it in every call. For batch-heavy work use the universal execute method (up to 25 calls per request) and queue it — this is the single biggest throughput win available.
3 ("Unknown method passed") if you send 5.131.ImplicitFlow is for browser-side token acquisition only. If you try to exchange a code server-side with ImplicitFlow you will get an invalid-request error. Use AuthorizationCodeFlow.expires_in alongside the token and refresh proactively. A 401 (error code 5) from VK almost always means "expired token", not "wrong scope".14) is unsolvable inside the SDK. You must redirect the user to redirect_uri?captcha_sid=...&captcha_key=.... Detect it in your error mapper and surface a 302, not a 500.ApiRequest is your seam for testing and logging. The interface is small (send(string $url, array $params): string). Swap in a fake that returns canned JSON to unit-test your service layer without hitting VK; decorate it to log every outbound URL and body.CurlRequest requires ext-curl. If your host lacks it, use VK\Client\Request\StreamRequest (based on file_get_contents). This is a one-line constructor change, but it is easy to miss because the failure surfaces as a generic VkClientException.Illuminate\Cache\RateLimiter or a Redis token bucket, especially before you scale queue workers.[] and some return objects. users()->get() with a single valid id returns a 1-element list; with an invalid id it returns an empty list, not a 404. Normalize upstream.execute swallows errors into per-call payloads. A failed inner call comes back as ['response' => [...], 'execute_errors' => [...]]; the SDK treats it as success. Inspect execute_errors explicitly.confirmation must be answered verbatim. The base handler does this, but if you write a custom router you must echo the raw confirmation string, or VK will mark the webhook as failing.message_new → handleMessageNew, wall_wall_post_new → handleWallPostNew. Typos are silent — your handler simply never fires. Log every incoming event during integration.group_id/ts are stateful. Store ts and key in cache/DB between polls; if you lose them, events are lost, not replayed.VkApiClient + ApiRequest), wrapping it behind your own interface lets you swap in a hand-rolled client later without touching call sites.How can I help you explore Laravel packages today?