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

Knp Last Tweets Bundle Laravel Package

knplabs/knp-last-tweets-bundle

KNPLabs LastTweetsBundle is a Symfony bundle for fetching and displaying a user's latest tweets. It offers simple configuration and Twig integration so you can embed recent tweet lists in templates with minimal setup, making it easy to add Twitter feeds to Symfony applications.

View on GitHub
Deep Wiki
Context7

Getting Started

Read this before running composer require: you won't. The package is a Symfony2 Bundle — its value lives in a Bundle class, a DependencyInjection extension, service YAML, and a Twig extension. Composer will also refuse: it pins Symfony ^2.3 and PHP ~5.3|~7.0, which conflicts with any Laravel 10/11/12 install on PHP 8.1+. And its reason to exist — an unauthenticated call to the Twitter REST v1.1 user-timeline endpoint — returns 410/404 today; X moved to v2 with mandatory OAuth 2.0 bearer tokens and a paid free tier. So the "getting started" path for a Laravel developer is: read it as a reference spec, then build ~200–400 LOC of first-party Laravel code. Treat the bundle as a shape, not a dependency.

Where to look first. Clone to a throwaway directory (never into your app) and read three files, in this order:

  1. The service that does the fetch — the small class behind the knp_last_tweets service id. This is the entire "contract": how it takes a username and count, calls the HTTP client, and hands back a list.
  2. The Twig extension / helper method that renders the widget. One call site; note how it passes the list to the partial and how it escapes.
  3. The template partial (.twig). Note what is escaped ({{ }}) versus raw ({{ x|raw }}) — this is your XSS reference, and the part you must not copy blindly.

Also open DependencyInjection/Configuration.php (or the equivalent YAML node definition) — that's where username, count, and cache lifetime are declared, and it's the direct analogue of what you'll put in config/last-tweets.php.

First use case — the smallest viable thing. A single Blade component rendering the last N tweets on a marketing page, backed by the official X embed, not the API. Concretely, in half a day:

// config/last-tweets.php
return [
    'handle' => env('LAST_TWEETS_HANDLE', 'laravel'),
    'count'  => 5,
    'ttl'    => 600,          // seconds; mirrors the bundle's cache lifetime knob
];
{{-- resources/views/components/last-tweets.blade.php --}}
@props(['handle' => config('last-tweets.handle'), 'count' => config('last-tweets.count')])

<a class="twitter-timeline"
   data-tweet-limit="{{ $count }}"
   href="https://twitter.com/{{ $handle }}?ref_src=twsrc%5Etfw">
   Tweets by {{ $handle }}
</a>

@once
    <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
@endonce

Use <x-last-tweets /> in the view. No credentials, no rate limits, no maintenance burden — X owns the auth, the API, and the rendering. This is the day-to-day answer for ~90% of "we want a Twitter feed" requests. Only walk away from this path if a stakeholder can articulate a concrete reason server-side rendering is required (e.g., SEO-crawlable tweet text, custom markup that the embed won't produce, or content that must survive ad-blockers).

Implementation Patterns

If a requirement genuinely forces server-side rendering, port the bundle's shape into a first-party Laravel package rather than resurrecting it. The bundle's design is small and sound; the wiring is what's dead.

Service + cache + partial — the ported architecture. The bundle decomposes into three concerns; map them straight onto Laravel:

Bundle concept Laravel equivalent
KnpLastTweetsBundle service definition ServiceProvider + singleton binding
DI config node (username, count, cache_ttl) config/last-tweets.php + .env
Cache-backed fetch service TweetRepository with Cache::remember
Twig extension + partial Blade component (or @include)
Bundle Configuration validation config + a small DTO / validation at boot

Repository with stale-on-error fallback. The bundle's cache layer is the one piece of engineering worth preserving — do it properly:

final class TweetRepository
{
    public function __construct(private TwitterClient $client) {}

    /** @return array<int, array{text:string,id:string,created_at:string}> */
    public function latest(string $handle, int $count): array
    {
        $key = "last-tweets:{$handle}:{$count}";
        $ttl = config('last-tweets.ttl');

        $cached = Cache::get($key);

        try {
            $fresh = $this->client->fetch($handle, $count);   // hard timeout inside
            Cache::put($key, $fresh, $ttl);

            return $fresh;
        } catch (\Throwable $e) {
            report($e);                        // alert, never surface to the page
            return $cached ?? [];              // stale-on-error, or empty
        }
    }
}

Bind it in a ServiceProvider, publish the config, and keep credentials in config/services.php + .env — never in a published config file that lands in git. This mirrors the bundle's multi-environment YAML setup without the Symfony plumbing.

Single-flight the refresh. The bundle's cache was adequate for a 2011 traffic profile; modern pages are spikier. Wrap the miss path in a short lock so N concurrent cold-cache requests produce one API call, not N:

return Cache::lock("last-tweets:{$handle}:refresh", 10)->block(3, function () use (...) {
    // check cache again inside the lock, then fetch
});

Render defensively. Blade, not Twig, but the discipline transfers:

@if(count($tweets))
    <ul class="tweets">
        @foreach($tweets as $t)
            <li>{{ $t['text'] }} <time>{{ $t['created_at'] }}</time></li>
        @endforeach
    </ul>
@endif
{{-- nothing rendered on empty/failed fetch — no broken box --}}

Note {{ }}, never {!! !!}. Tweet text, display names, and media URLs are untrusted input. Validate media URLs against expected hosts before linking.

Test with fakes, not the network. The bundle had no test story you can reuse; write your own:

Http::fake(['api.x.com/*' => Http::response($fixture, 200)]);
$this->get('/')->assertSee('expected tweet text');

Add cases for 401 (bad creds → stale cache), 429 (rate limit → stale cache + no in-request retry), and empty payload (renders nothing).

Feature-flag the rollout. Ship behind a flag, keep the old markup for one release, then cut over — the same sequencing the bundle would have needed, just owned by you.

Gotchas and Tips

The API is the first blocker, not the code. X's v1.1 timeline endpoint is retired. Before writing a single line of repository code, spend a timeboxed half-day confirming you have credentials at a tier that works from your infrastructure today, and that the endpoint and auth scheme you intend to use actually respond. This step invalidates the whole plan most often; sequence it first with a hard stop.

composer require will fail — by design. Symfony ^2.3 / PHP ~5.3|~7.0 constraints versus PHP 8.1+. Don't spend time trying to "make it install"; it's not a tuning problem, it's a hard mismatch. If a fork exists, assume the same.

PHP 8 deprecations even in a ported copy. The bundle predates PSR-12 and dynamic-property rules. If you lift any code, expect issues with dynamic properties, null-to-scalar coercion, and ${var} interpolation (removed in PHP 8.2). Rewrite rather than copy.

Escape on output, always — the bundle's template is not a safe template. The original partial may echo raw tweet text. That's an XSS vector the moment you rehost it. In Blade, {{ }} and a reviewed output path; audit any {!! !!} you find and never carry one over.

Cache TTL is a product decision, not a default. The bundle exposed it as config; keep it explicit. 5–15 minutes is sensible. Too long displays deleted/edited tweets (brand and legal exposure); too short approaches a live-API call per view. Provide a php artisan last-tweets:flush <handle> cache-bust command for the "retracted tweet is still showing" ticket — you will get it.

Never block a page render on a live API call. With a 5–15 minute cache the hit rate is trivially high and API volume is negligible regardless of traffic. Without a cache, every page view is an API call subject to rate limits and 429s. Put the cached fragment behind the app cache and, ideally, the CDN/edge so misses don't stack under a spike.

Design the failure modes explicitly — this is the part the bundle never solved well and the part that actually matters for page reliability:

  • Auth failure / rotated credentials → serve stale cache, log, alert; never leak stack traces or creds.
  • 429 rate limit → stale cache + backoff; do not retry in the request path.
  • Endpoint removal / API deprecation → treat as a scheduled failure; it already happened to this package once. Budget for it.
  • Deleted/edited tweets → short TTL + manual bust command.
  • Protected/empty account → render nothing, not an empty box or exception.
  • Cold cache under load → single-flight the refresh (see lock above).
  • Untrusted content → escape on output, validate media hosts.

Credentials live in services.php + .env. Never in a published config checked into git. The bundle's YAML-in-repo habit is exactly the pattern to unlearn.

The scaling answer is "don't." With the official embed, scaling is X's problem and your surface is one <script> tag. With a server-side build, scaling means cache hit rate and single-flight — solvable, but a permanent few-hours-per-quarter tax plus a reactive fix whenever X changes something. Decide embed-vs-build first; it's the difference between ~0.5 dev-days and ~4–6 dev-days plus recurring cost.

No upstream to escalate to. Archived, 32 stars, 0 dependents, no security advisories, no issue triage. If you adopt or fork, name an internal owner in the runbook and document the two tickets this feature actually generates: "widget is blank" and "widget shows old tweets."

Treat the score as metadata arithmetic. A composite around 0.81 and opportunity around 0.66 shouldn't override dispositive signals: archived, zero dependents, dead framework target, dead API. Use the bundle as a reference for the shape — fetch, cache, render in a template — and nothing else. Default to the official client-side embed; build a thin first-party Laravel package only when a real requirement forces server-side rendering, and never resurrect the bundle itself.

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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource