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

Instagram User Feed Laravel Package

pgrimaud/instagram-user-feed

Fetch public Instagram user feeds in PHP without the official API or access tokens. Retrieve profile info, posts, photos, videos, and stories via a simple, dependency-light crawler with pagination and caching. Works standalone or in Laravel.

Deep Wiki
Context7

Getting Started

Install

composer require pgrimaud/instagram-user-feed
composer require symfony/cache   # if not already pulled in

Minimum viable setup. The client is stateful: it keeps device IDs and cookies in a PSR-6 cache pool you must provide. That pool is not optional — it's how the package survives between requests without logging in again.

use Instagram\Api;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;

$cache = new FilesystemAdapter('instagram', 0, storage_path('app/instagram'));
$api   = new Api($cache);

$profile = $api->getProfile('nasa');
$medias  = $profile->getMedias();      // first page is embedded in the profile call

Where to look first

  1. Instagram\Api — the single entry point; read its public methods before anything else (getProfile, getFeed, getMedias, getMediaByCode, getComments, like, follow, getUserInfoByName).
  2. src/Model/ — Profile, Media, Comment, User. These are the objects you'll actually type-hint and serialize. Getters are mostly nullable.
  3. src/Exception/ — InstagramException and subclasses (InstagramAuthException, InstagramFetchException, InstagramNotFoundException) drive your retry/alert logic.
  4. The README + tests/ fixtures — the fastest way to confirm a method signature before Instagram changes something.

First use case. A "latest posts" block on a marketing site where the account is a client's or competitor's public profile and the official Graph API is off the table (no Business account link, no app review). One call, cache the result for 15–30 minutes, render. Do not attempt this in the page request on a busy site — see below.

Implementation Patterns

Bind it as a singleton behind a config file.

// AppServiceProvider
$this->app->singleton(Api::class, fn () => tap(
    new Api(new FilesystemAdapter('instagram', 0, storage_path('app/instagram'))),
    function (Api $api) {
        $api->setUserAgent(config('instagram.user_agent'));
        if ($proxy = config('instagram.proxy')) {
            $api->setProxy($proxy);
        }
    }
));

Configure the proxy and user agent before the first request — they get baked into the session the cache pool persists.

Wrap it in your own interface. Do not let Instagram\Api leak into controllers or Blade. Define InstagramFeedRepository with profile(string $handle): FeedProfile returning your own DTOs. When the scraper inevitably breaks you swap one adapter instead of hunting call sites — and you can fake the interface in tests instead of hitting Instagram from CI.

Separate "fetch" from "render." The reliable pattern is a scheduled command that pulls data into your own tables (or a cache key), and views that read only from your store.

// app/Console/Commands/SyncInstagramFeeds.php — hourly
foreach (InstagramAccount::where('active', true)->get() as $account) {
    SyncInstagramAccount::dispatch($account);
}

The job calls the API, maps Media models into an instagram_media table (id, shortcode, caption, thumbnail, posted_at, like_count), and upserts on shortcode. Your front end becomes a plain Eloquent query — it never blocks on a third-party scrape.

Layer your own cache with a TTL. The package's cache pool stores session state, not results. Add a second layer:

$profile = Cache::remember("ig:profile:{$handle}", now()->addMinutes(30), fn () => $api->getProfile($handle));

Stale-but-served beats a 429 at render time.

Anonymous vs. logged-in is a real architectural choice.

  • Public profiles, public posts, comments: anonymous works and is the lowest-risk mode.
  • like, follow, getFollowers, getFollowing, private accounts: requires $api->login($user, $pass) with a dedicated throwaway account, never the client's real one.

Run authenticated interactions from a distinct queue with a different proxy/IP than your read-scraping, and throttle them (e.g. one action every 30–90s with jitter). Treat it as an outbound-integration worker, not a loop over an array.

Pagination for backfill. getMedias($userId, $limit, $maxId) returns a page plus a cursor; walk it slowly for a one-time import, then switch to incremental sync by shortcode. Backfilling a 2000-post account in a tight loop is the fastest way to get an IP or account flagged.

Shortcodes, not URLs. When a user pastes instagram.com/p/CxYz.../, strip it and use getMediaByCode($code) rather than reconstructing IDs.

Mirror media you intend to keep. getThumbnailUrl() / getVideoUrl() point at Instagram's CDN with signed, expiring URLs. If the images are part of your product (not a transient widget), copy them to S3 at sync time and store your own path.

Handle exceptions by class, not by string:

try {
    $profile = $api->getProfile($handle);
} catch (InstagramNotFoundException $e) {
    $account->update(['status' => 'gone']);
} catch (InstagramAuthException $e) {
    Cache::forget('ig:session');        // force a clean re-login next run
    report($e);
} catch (InstagramFetchException $e) {
    $this->release(300);                // rate limited / blocked — back off hard
}

Gotchas and Tips

This is a scraper, and it will break. There is no contract, no versioning on Instagram's side, and the underlying private endpoints change without notice — sometimes within days. Last release is May 2024. Pin the version, watch the issue tracker, and design for the package being unavailable: an interface you can reimplement, a cache that can serve stale content for hours, and a fallback (official Graph API for accounts you own, or the public embed endpoint) for the worst case.

The cache directory is the #1 source of "it works locally" bugs. If storage/app/instagram is missing, read-only, or wiped on deploy, the session silently resets every request — you'll see intermittent auth failures and checkpoints. Make it a persistent volume, and add it to your deploy's writable paths.

Stateful sessions break on multi-server setups. With a FilesystemAdapter on three app servers you effectively have three identities, each re-logging in. Use a shared Redis pool (Symfony\Component\Cache\Adapter\RedisAdapter) the moment you run more than one node.

Login triggers Instagram checkpoints. A brand-new account logging in from a datacenter IP will be asked for an email or SMS code that this package handles poorly. Warm the account on a residential IP via the normal app first, and never log in "just in time" per request — log in once, persist the pool, and reuse.

Rate limits are per-IP and per-account, and they're opaque. You will not get a documented Retry-After. Treat InstagramFetchException and silent empty responses as "back off exponentially, then alert." Add jitter; synchronized cron jobs across tenants look exactly like a bot.

Keep one user agent per session. Changing setUserAgent() after a session exists creates a fingerprint mismatch that reliably returns 403. Store the UA alongside the cache and keep them paired.

Model getters lie by being null. getLocation() is null on the vast majority of posts, getCaption() is null on some media types, video posts may not expose every image field. Nullable-check everything before persisting, or your import job dies on record 400 of 5000.

Timezone discipline. getDate() gives you a DateTime derived from Instagram's timestamp — normalize to UTC on write and convert at render, otherwise your "posted 2 hours ago" is wrong for half your audience.

Don't scrape and interact from the same identity. Reading is cheap to recover from; a burned login account means the client's scheduled posts and DMs are gone. Two accounts, two proxies, two queues.

Debugging blocked requests. Log the full exception message and the account handle together, and keep a known-good handle you can hit manually to distinguish "this profile is gone" from "we are blocked." If a manual call with the same cache pool succeeds, the problem is your throttle; if it fails too, the package or the endpoint changed — time to check for a release.

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.
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle