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

Meta Catalog Manager Laravel Package

scriptdevelop/meta-catalog-manager

A Laravel package for managing Meta (Facebook) product catalogs: sync products, build and upload catalog feeds, handle batch API requests, and keep item data consistent with Meta Commerce Manager.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish

composer require scriptdevelop/meta-catalog-manager
php artisan vendor:publish --provider="ScriptDevelop\MetaCatalogManager\MetaCatalogManagerServiceProvider"

Because the package has near-zero adoption (2 stars, 0 dependents), treat the README as a starting hypothesis, not documentation. Before you write a line of integration code, spend 15 minutes in vendor/scriptdevelop/meta-catalog-manager/:

  1. src/*ServiceProvider.php — confirms the real config publish tag, whether bindings are singletons or scoped, and which classes are registered as facades/aliases.
  2. config/meta-catalog-manager.php (name may differ) — the authoritative list of env keys: Meta app ID/secret, business ID, access token, API version, default catalog ID, logging channel.
  3. src/ directory tree — identifies the client/manager entry point, the DTOs or arrays returned, and whether there are separate services per concern (accounts, feeds, inventory, offers, diagnostics).
  4. routes/ and src/Console/ — diagnostics endpoints or artisan commands are often the fastest way to confirm credentials work before touching your own code.
  5. composer.json — check the illuminate/* and php constraints against your app. A package this small is unlikely to be tested against Laravel 11/12; if the constraint is loose (e.g. ^9.0|^10.0), verify manually.

Configuration

META_APP_ID=
META_APP_SECRET=
META_BUSINESS_ID=
META_ACCESS_TOKEN=
META_API_VERSION=v21.0
META_CATALOG_ID=

Use a System User token from Meta Business Manager, not a personal token — personal tokens expire in ~60 days and will silently break your scheduled feed jobs.

First use case — prove the pipe end to end

The highest-value first slice is: one catalog, one product feed, visible in Commerce Manager. Do this in a tinker session or a throwaway artisan command before building anything:

$manager = app(\ScriptDevelop\MetaCatalogManager\MetaCatalogManager::class);

// 1. Can I read the account/catalog? (proves token scope + permissions)
$catalog = $manager->catalog($catalogId)->get();

// 2. Can I push a minimal product?
$manager->products($catalogId)->create([
    'retailer_id' => 'SKU-1',
    'name'        => 'Test Product',
    'price'       => '19.99 USD',
    'availability'=> 'in stock',
    'condition'   => 'new',
    'link'        => 'https://example.com/p/1',
    'image_link'  => 'https://example.com/p/1.jpg',
]);

If step 1 fails with an OAuthException, the problem is token scopes — you need catalog_management and business_management at minimum — not the package. Getting that distinction early saves hours.

Implementation Patterns

Wrap the package; do not call it from controllers

Given the adoption level, assume the public API may shift between patch releases. Put a thin CatalogService of your own in front of it so a breaking change is a one-file fix:

class CatalogService
{
    public function __construct(
        private MetaCatalogManager $manager,
        private CatalogAccountResolver $resolver,
    ) {}

    public function pushProduct(Product $product, string $accountKey): array
    {
        $account = $this->resolver->for($accountKey);

        return $this->manager
            ->account($account->meta_business_id, $account->access_token)
            ->products($account->catalog_id)
            ->updateOrCreate($product->sku, $this->toPayload($product));
    }
}

Multi-account: resolve credentials per call, never globally

The package's headline feature is multi-account catalogs. Whatever the global config says, the day-to-day reality is one catalog per storefront/region/tenant. Store business_id, catalog_id, and access_token (encrypted cast) on your own model — e.g. meta_accounts — and pass them explicitly. If the package only reads credentials from config, you'll end up doing:

config(['meta-catalog-manager.access_token' => $account->access_token]);

That pattern is a landmine under Octane, queued jobs, and any concurrent request. If the package doesn't support per-account credentials natively, register it as a scoped binding or instantiate a fresh client per job — and note it as a fork-worthy gap.

Product feeds: build the source of truth in Laravel, not in Meta

Keep products in Eloquent, and treat Meta as a downstream projection. A scheduled command is the cleanest shape:

// app/Console/Commands/SyncCatalogFeed.php
Schedule::command('catalog:sync')->hourly()->withoutOverlapping();

Inside, stream with chunkById(500) and diff against a meta_feed_state table (sku, payload_hash, synced_at, status). Only send rows whose hash changed. Meta catalogs are eventually consistent and re-pushing unchanged products wastes quota and muddies diagnostics — the package's diagnostics output is far more useful when your own diff layer has already removed the noise.

Batch inventory: chunk, queue, throttle

Inventory is the highest-frequency operation and the one most likely to hit rate limits. Pattern:

Product::query()
    ->where('updated_at', '>', $since)
    ->chunkById(200, function ($products) use ($account) {
        UpdateCatalogInventory::dispatch($account->id, $products->pluck('sku'));
    });
class UpdateCatalogInventory implements ShouldQueue, ShouldBeUnique
{
    public $uniqueFor = 3600;

    public function middleware(): array
    {
        return [(new RateLimited('meta-api'))->releaseAfter(30)];
    }
}

Define the meta-api limiter in AppServiceProvider from your observed quota rather than a guess. Meta's batch endpoints have per-request item ceilings; keep your chunk sizes comfortably under them and make the chunk size a config value so you can tune it without a deploy.

Localized catalogs: model the locale explicitly

If you sell in multiple locales, the cleanest model is a localized feed per locale derived from a base product, keyed by retailer_id suffixed or namespaced by locale (SKU-1__de-DE). Keep translations in a product_translations table, generate one payload per locale, and let each locale map to its own catalog or override feed. Do not attempt to mutate a single product's localized fields repeatedly — the write amplification and race conditions are worse than the duplication.

Diagnostics as a first-class job

Wire the package's diagnostics into your own health surface: a daily scheduled command that checks token expiry, per-catalog item counts, and last successful feed timestamp, writing to a meta_health_checks table. Alert on drift (e.g. Laravel item count vs. Meta product_count differing by >2%). This is what makes the package genuinely operational rather than a one-shot uploader — and it's the part most integrations skip.

Testing

Use Http::fake() at the boundary and assert on the outbound payload shape, not on the package's internals. Snapshot-test your toPayload() output — Meta rejects on field-level detail, and a schema regression is the most common production break.

Gotchas and Tips

Adoption risk is real — pin and audit

Two stars and zero dependents means no community bug reports to learn from. Pin an exact version ("scriptdevelop/meta-catalog-manager": "1.2.3"), read the diff before every bump, and check the last commit date. If the repo has been dormant for 12+ months, assume you are the maintainer of anything you rely on, and budget for possibly inlining the ~few-hundred lines you actually use.

Long-lived tokens are the #1 production incident

System User tokens can still be invalidated by app permission changes, business verification lapses, or password/2FA events on the owning admin. Store an expiry, run a daily validity probe, and alert before the feed job starts failing. Never store the token in a config file that gets committed.

Config cache breaks runtime env()

If the package calls env() inside its service provider or classes, php artisan config:cache will freeze those values to whatever existed at cache time — or return null. Check for env( calls in src/. If present, prefer setting everything via the published config file and re-running config:cache after env changes.

API version drift

Pin META_API_VERSION explicitly. Unpinned or defaulted values get you deprecated endpoints with cryptic errors months later. Read the version out of config, never hardcode it in your own code.

Uploads are asynchronous

Product feed and batch uploads return a session/handle, not a completed result. Poll the status, and don't assert success right after the call. Design your synced_at to represent confirmed ingestion, and add a reconciliation job that re-checks anything stuck in pending past a threshold.

Error handling is where packages this size are weakest

Catch at the boundary and normalize. Log Meta's fbtrace_id from the error body on every failure — Meta support will not help you without it, and it's usually the only way to correlate your request with their logs. Distinguish clearly between:

  • Auth/scope errors (OAuthException, code 200/190) — fix credentials, do not retry.
  • Rate limits (throttling codes) — retry with backoff, use job middleware.
  • Validation errors (invalid field, missing required attribute like price or availability) — dead-letter, alert, do not retry; retrying a payload Meta considers invalid just burns quota.

Localization fields are stricter than they look

price must include the currency code; availability and condition are enumerated strings; image_link must be a publicly reachable absolute URL (Meta fetches it — localhost and signed/expiring URLs will fail). These fail on Meta's side, not the package's, so validate them in a FormRequest or a toPayload() guard before dispatch.

Extension points to check for

Look for a bindable interface (e.g. an HTTP client contract) in the service provider. If one exists, bind your own implementation in a service provider so you can inject retry logic, logging, and Http::preventStrayRequests() in tests without forking. If none exists, that's your single best fork candidate — a small, isolated change with high operational payoff.

Debugging checklist when nothing works, in order

  1. Direct curl against graph.facebook.com with the same token → isolates package vs. credentials.
  2. Token debugger in Meta's Graph API Explorer → confirms scopes and expiry.
  3. Business/catalog asset permissions → the token's user must be assigned to the catalog in Business Manager.
  4. Package diagnostics command/endpoint → if it exists, it will report the raw Meta error; read error_user_title/error_user_msg, which are far clearer than message.
  5. Only then start reading package source.
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.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
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