Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Twitter Laravel Package

thujohn/twitter

thujohn/twitter is a Laravel wrapper for the Twitter API, offering a simple facade, configuration, and methods for timelines, tweets, users, and OAuth. It helps Laravel/PHP developers integrate Twitter features quickly with minimal setup.

Deep Wiki
Context7

Getting Started

1. Install and register

composer require thujohn/twitter

If your Laravel version doesn't auto-discover the provider, add it to config/app.php:

'providers' => [
    Thujohn\Twitter\TwitterServiceProvider::class,
],
'aliases' => [
    'Twitter' => Thujohn\Twitter\Facades\Twitter::class,
],

2. Publish and fill the config

php artisan vendor:publish --provider="Thujohn\Twitter\TwitterServiceProvider" --tag=config

config/twitter.php expects four values from the X/Twitter developer portal: consumer_key, consumer_secret, access_token, access_token_secret (plus API_VERSION, API_URL, upload_url, timeout, format). Put the credentials in .env and reference them from the config file — never call env() at runtime, it returns null once config is cached.

Important: the app must have Read + Write permission in the developer portal before you generate the access token pair. Tokens minted under Read-only silently fail with 403 on every write call, and the fix is regenerating the tokens, not changing code.

3. Where to look first

There is effectively no maintained API reference for this package. The authoritative surface is the source of Thujohn\Twitter\Twitter — every public method is a thin wrapper whose name is derived mechanically from the endpoint path:

  • statuses/home_timeline → getHomeTimeline()
  • statuses/user_timeline → getUserTimeline()
  • statuses/update → postTweet()
  • statuses/show/:id → getTweet($id)
  • users/lookup → getUsersLookup()
  • search/tweets → getSearchTweets()
  • media/upload → uploadMedia()

So get/post + StudlyCase of the path. Once you internalise that, you can guess almost any v1.1 endpoint without reading docs.

4. First use case — read-only timeline

use Thujohn\Twitter\Facades\Twitter;

$tweets = Twitter::getUserTimeline([
    'screen_name'     => 'laravelphp',
    'count'           => 10,
    'tweet_mode'      => 'extended',
    'format'          => 'array', // critical — see Gotchas
]);

foreach ($tweets as $tweet) {
    echo $tweet['full_text'];
}

Start with a read endpoint to validate credentials end-to-end before touching posting. A cheap sanity check is Twitter::getUsersLookup(['screen_name' => 'twitter']) — if that throws, the problem is auth, not your endpoint logic.

Implementation Patterns

Always pass format per call

The config default is a JSON string. Passing 'format' => 'array' on each call is the single most common fix for "why is $tweet['text'] failing". Use 'array' for anything you iterate, 'object' if you prefer ->, and leave 'json' only when you're logging raw payloads verbatim.

Wrap the facade in your own repository

Don't sprinkle Twitter:: through controllers. One class, injected:

class TwitterGateway
{
    public function __construct(private \Thujohn\Twitter\Twitter $twitter) {}

    public function recentTweets(string $handle, int $count = 20): array
    {
        return Cache::remember("tw:timeline:{$handle}", 300, fn () => $this->twitter
            ->getUserTimeline([
                'screen_name' => $handle,
                'count'       => $count,
                'tweet_mode'  => 'extended',
                'format'      => 'array',
            ]));
    }
}

This gives you a swap point when you inevitably migrate to API v2, and makes tests trivial (bind a fake).

Cache aggressively — rate limits are per-endpoint, per-15-minutes

Read endpoints are 15 requests / 15 min per user context (higher app-only). A timeline widget rendered on every page load will exhaust this in minutes. Cache with a TTL matched to freshness needs (300s for timelines, longer for profile lookups) and key by all parameters.

Post from a queued job, and make it idempotent

class PublishTweet implements ShouldQueue
{
    public int $tries = 3;
    public array $backoff = [30, 120, 600];

    public function handle(TwitterGateway $tw): void
    {
        if (TweetLog::where('hash', $this->hash)->exists()) {
            return; // already posted
        }
        $result = $tw->post($this->body);
        TweetLog::create(['hash' => $this->hash, 'tweet_id' => $result['id_str']]);
    }
}

Retries without a dedupe guard are how you get duplicate tweets — a network timeout on a request that actually succeeded is indistinguishable from a failure on the client side.

Media: two steps

Upload first, then attach the returned media_id_string:

$media = Twitter::uploadMedia(['media' => file_get_contents($path)]);
Twitter::postTweet([
    'status'    => 'Ship it',
    'media_ids' => $media['media_id_string'],
]);

Note that chunked upload (INIT/APPEND/FINALIZE) for video isn't exposed as a friendly method. For anything beyond a small image you'll be calling the upload URL directly through the underlying client.

Pagination

Never trust a count alone. Walk timelines backwards with max_id (decrement by 1 to avoid re-fetching the boundary tweet) or forward with since_id for incremental sync. Store the highest id_str you've seen and use since_id on the next poll — this is also how you avoid re-processing the same tweets repeatedly.

Testing

The package sits on abraham/twitteroauth, which uses Guzzle directly, so Http::fake() won't intercept it. Test at your gateway boundary: bind a fake implementation of TwitterGateway in the container and assert against its calls. Reserve real API calls for a single manual smoke test.

Gotchas and Tips

The elephant: v1.1 is largely dead. This package targets the Twitter/X API v1.1, and most of those endpoints no longer work on the free tier — you will hit 403s, 410s, and 429s on endpoints that "used" to work, with no code change on your side. There is no v2 support here. Treat the package as a convenience layer that may only cover a shrinking slice of the surface, and design your gateway interface so the transport can be replaced without touching call sites. Before committing to it, hit one endpoint you actually need and confirm it returns 200.

Exception handling is coarse. Failures throw Thujohn\Twitter\TwitterException; the useful detail is in the code and message, not in typed subclasses. Parse for the well-known codes:

  • 88 — rate limit exceeded (back off, don't retry immediately)
  • 187 — duplicate status (you already posted this exact text)
  • 89 — invalid/expired token (auth problem, not a code problem)
  • 64 / 326 — account suspended/locked

Wrap every write in a try/catch and log the raw message — the exception text is often empty while the body contains the real reason.

Config caching freezes credentials. After editing .env, run php artisan config:clear. In a queue worker, the config was loaded at boot; queue:restart after credential changes or workers keep using old tokens forever.

Single instance, mutable tokens. The class exposes setOauthToken() / setOauthTokenSecret() and friends, which people use for multi-account posting. Because the binding resolves a shared instance, mutating it in a long-running worker leaks credentials across jobs — one tenant's tweet posts from another tenant's account. Either clone the instance per job or construct a fresh one.

Extended text is not in text. Without 'tweet_mode' => 'extended', long tweets are truncated with a … and a truncated: true flag. With it, the body is in full_text, and text may be absent. Also remember URLs always count as 23 characters and get rewritten to t.co, so client-side counting with strlen() will disagree with the API.

Blade output needs escaping. Tweet text is user-generated. {!! $tweet['full_text'] !!} is an XSS vector. The package ships helpers (linkify(), ago()) that auto-link entities — they are convenient but do not make output safe on their own; escape user text first, then linkify.

Timeouts and TLS. timeout, connect_timeout, and ssl_verify in the config exist for a reason. On shared hosting or behind a corporate proxy, TLS verification failures are common; prefer fixing the CA bundle over setting ssl_verify to false (which disables protection on an authenticated request).

Extension points. When you need behaviour the facade doesn't expose, extend Thujohn\Twitter\Twitter, override the request/URL-building method, and rebind your subclass in a service provider. That's the least invasive path to adding logging, retry policy, or raw passthrough calls to endpoints the package doesn't map.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core