j7mbo/twitter-api-php
A lightweight, dependency-free PHP wrapper for the Twitter API. It handles OAuth signing and lets you send GET and POST requests through cURL using a single include file, making it easy to drop into any PHP project for quick Twitter integration.
This is a single-class wrapper (TwitterAPIExchange) around Twitter API v1.1. Installation is usually either composer require j7mbo/twitter-api-php if your Composer setup still resolves it, or simply vendoring TwitterAPIExchange.php into your project. Because the package is archived, vendoring or pinning a fork is often the safer day-to-day choice.
Start by reading the class itself — it is small and all behavior is in buildOauth(), performRequest(), and the setters. Then create a Twitter app in the developer portal, generate an access token/secret with Read and Write permissions, and put all four credentials in .env:
TWITTER_CONSUMER_KEY=
TWITTER_CONSUMER_SECRET=
TWITTER_ACCESS_TOKEN=
TWITTER_ACCESS_TOKEN_SECRET=
Minimal first call — verify credentials:
require 'TwitterAPIExchange.php';
$twitter = new TwitterAPIExchange([
'oauth_access_token' => env('TWITTER_ACCESS_TOKEN'),
'oauth_access_token_secret' => env('TWITTER_ACCESS_TOKEN_SECRET'),
'consumer_key' => env('TWITTER_CONSUMER_KEY'),
'consumer_secret' => env('TWITTER_CONSUMER_SECRET'),
]);
$url = 'https://api.twitter.com/1.1/account/verify_credentials.json';
$response = $twitter->buildOauth($url, 'GET')->performRequest();
$user = json_decode($response, true);
For a GET with parameters, set the query string before signing:
$url = 'https://api.twitter.com/1.1/statuses/user_timeline.json';
$getfield = '?' . http_build_query(['screen_name' => 'twitter', 'count' => 5]);
$response = $twitter
->setGetfield($getfield)
->buildOauth($url, 'GET')
->performRequest();
Your first real use case is almost always reading a timeline, looking up a user, or posting a status. Confirm credentials and app permissions with a standalone script before wiring anything into Laravel.
Wrap it behind a service. Do not scatter new TwitterAPIExchange(...) through controllers and jobs. Create a small client class that owns configuration, request construction, JSON decoding, and error normalization. Bind it in the container and inject it.
final class TwitterClient
{
public function __construct(private array $config) {}
public function get(string $endpoint, array $query = []): array
{
$url = 'https://api.twitter.com/1.1/' . ltrim($endpoint, '/');
$twitter = new TwitterAPIExchange($this->config);
if ($query !== []) {
$twitter->setGetfield('?' . http_build_query($query));
}
return $this->decode(
$twitter->buildOauth($url, 'GET')->performRequest()
);
}
public function post(string $endpoint, array $body = []): array
{
$url = 'https://api.twitter.com/1.1/' . ltrim($endpoint, '/');
$twitter = new TwitterAPIExchange($this->config);
$twitter->setPostfields($body);
return $this->decode(
$twitter->buildOauth($url, 'POST')->performRequest()
);
}
private function decode(string|false $raw): array
{
if ($raw === false || $raw === '') {
throw new RuntimeException('Twitter transport failure');
}
$data = json_decode($raw, true);
if (!is_array($data)) {
throw new RuntimeException('Invalid Twitter response: ' . $raw);
}
if (isset($data['errors'])) {
throw new RuntimeException('Twitter API error: ' . json_encode($data['errors']));
}
return $data;
}
}
Instantiate per request, not as a singleton. The object accumulates request state: getfield, postfields, URL, and method persist between calls. A singleton can silently reuse a previous query string or POST body. Either construct a fresh instance per call or carefully reset all fields.
Set fields before buildOauth(). buildOauth() computes the OAuth signature from the accumulated request state, and performRequest() later appends the GET query string. Calling setGetfield() after buildOauth() produces a signature for a different URL and usually results in a 401.
Use config, not hard-coded credentials. Add a twitter block to config/services.php, read from .env, and pass that array into your wrapper. Never log the access token, secret, or generated Authorization header.
Cache reads aggressively. Twitter rate limits are low and per-user. Wrap timeline/user lookups in Cache::remember() with a short TTL, and queue writes rather than posting inline from web requests.
Posting a tweet:
$client->post('statuses/update.json', [
'status' => 'Deployed via Laravel',
]);
For simple media uploads, base64-encode the file and post to upload.twitter.com/1.1/media/upload.json with media_data, then attach the returned media_id_string to a subsequent statuses/update.json call. Videos and large images require chunked upload (INIT/APPEND/FINALIZE), which this wrapper can theoretically do but has no helpers for.
Testing. The package performs real cURL calls and exposes no injectable HTTP transport. Test against your own TwitterClient interface — bind a fake implementation in the container — rather than trying to mock the package directly.
Consider migration paths. For new work, a maintained OAuth 1.0a client such as abraham/twitteroauth is usually a better long-term dependency. If you need Twitter API v2, note that this package is built around v1.1 conventions; v2 uses different endpoints and JSON body semantics. Keeping your own wrapper makes the eventual replacement much cheaper.
performRequest() returns only the response body. A 401, 429, or 500 looks like ordinary JSON or an empty string. Parse errors from the body, and for rate-limit work you must either fork the class or set CURLOPT_HEADER => true via setCurlOptions() and parse headers out of the response.false with no error message. Always check for false and an empty string before json_decode(). If you need curl_error(), you will have to modify the class, because it closes the handle internally.time(). Loops that fire multiple requests in the same second can produce duplicate nonces and intermittent 401s. Add a small delay, or fork the class to use bin2hex(random_bytes(16)).setGetfield() before buildOauth(), not by appending to the URL manually. Passing a URL with a query string directly to buildOauth() can cause signature mismatches.setPostfields() receives an array, cURL sends multipart form data. Twitter v1.1 generally accepts this, but if you need JSON you must pass a JSON string and set the content type through setCurlOptions(). Nested arrays will not encode usefully.setTimeouts() if available in your version, or CURLOPT_TIMEOUT / CURLOPT_CONNECTTIMEOUT through setCurlOptions().CURLOPT_CAINFO via setCurlOptions() instead of turning verification off.https://api.twitter.com/1.1/... URLs. A stray slash, http, or unexpected query ordering can break the signature.account/verify_credentials.json, dump the raw body, and check server clock skew. OAuth 1.0a signatures are time-sensitive.$url and $requestMethod exist, but most state is private. Subclassing for retries, logging, or header parsing is awkward; forking the single file is often more practical than fighting the API.composer.lock and consider a VCS fork. If Packagist resolution changes or disappears, the archived package can become an install-time liability.How can I help you explore Laravel packages today?