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

Packagist Api Laravel Package

knplabs/packagist-api

A simple object-oriented PHP wrapper for the Packagist API, offering package search, full package details, and efficient Composer metadata retrieval. Easily list results, limit pages, and access names, descriptions, and versions.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a thin, dependency-light wrapper around Packagist's public JSON endpoints — no service provider, no facade, no config file. You get one class (Packagist\Api\Client) and a set of hydrated result objects. Treat it as an HTTP SDK, not a Laravel package.

composer require knplabs/packagist-api

Requires PHP ^8.0 (on 7.4 you're stuck on ~2.1.0; on PHP 8.4 pin at least ^2.1.1, which fixed a nullable-declaration deprecation).

First use case — "what does this package actually do, and is it alive?":

$client = new \Packagist\Api\Client();

$package = $client->get('sylius/sylius');
printf('%s — %s', $package->getName(), $package->getDescription());

foreach ($client->search('sylius') as $result) {
    echo $result->getName(), PHP_EOL;
}

Where to look first, in this order:

  1. src/Packagist/Api/Client.php — the full surface area. The README documents search(), get(), getComposer(), all(), and setPackagistUrl(), but releases added getComposerReleases(), getComposerBranches(), and a security-advisories endpoint that the README never mentions. Read the class, not the docs.
  2. src/Packagist/Api/Result/ — Package, Version, Maintainer, SearchResult, and the Factory that hydrates them. These classes are the real "API contract" you'll code against.
  3. src/Packagist/Api/Exception/ — PackageNotFoundException and friends.

In a Laravel app the very first integration is almost always a small service class wrapping the client so you can inject it, cache it, and fake it in tests. Don't let new Client() leak into controllers.

Implementation Patterns

Bind it once, resolve everywhere. There's no auto-discovery, so register a singleton in AppServiceProvider:

$this->app->singleton(\Packagist\Api\Client::class, function () {
    $client = new \Packagist\Api\Client();
    // Only if you run a mirror or a caching proxy in front of Packagist:
    // $client->setPackagistUrl(config('services.packagist.url'));
    return $client;
});

Then type-hint Client in your own PackagistService, not in controllers or jobs directly. That extra layer is what makes the package swappable (see Gotchas).

Always cache — Packagist is a third-party HTTP call in your request path. Package metadata barely changes minute to minute:

class PackageInfo
{
    public function __construct(private \Packagist\Api\Client $client) {}

    public function find(string $name): ?\Packagist\Api\Result\Package
    {
        return Cache::remember("packagist:pkg:{$name}", now()->addHours(12), function () use ($name) {
            try {
                return $this->client->get($name);
            } catch (\Packagist\Api\PackageNotFoundException) {
                return null;
            }
        });
    }
}

Cache the miss too (store a sentinel or null explicitly), otherwise a typo'd package name hammers the API on every render.

Prefer getComposer() over get() for anything you call often. The README says this explicitly: get() builds full metadata dynamically, while getComposer() reads Packagist's statically cached Composer files and is far cheaper. The trade-off is a different return shape:

$packages = $client->getComposer('sylius/sylius');   // array, keyed by package name
$package  = $packages['sylius/sylius'];              // select manually
$versions = $package->getVersions();
echo $versions[0]->getDescription();

If you only need version constraints, branches, or release tags, getComposerReleases() / getComposerBranches() are even narrower.

Use search() with an explicit cap. The third argument is a page count, and there's also a limit option (added in 1.7.0) — they are different knobs and easy to confuse:

$results = $client->search('laravel payment', ['type' => 'library'], 2); // first 2 pages

Cap it. Unbounded search on a broad term will pull a lot of pages and slow your endpoint to a crawl.

Push all() off the request path, permanently. $client->all() walks the entire Packagist index — hundreds of thousands of entries streamed one at a time. This belongs in a queued job or an artisan command, never a controller:

// app/Console/Commands/SyncVendorPackages.php
foreach ($this->client->all(['vendor' => 'sylius']) as $package) {
    // filter by vendor/type server-side, then persist what you need
}

Note all() returns package names (string-castable result objects), so it's a discovery/audit tool, not a metadata dump — you'll need a follow-up getComposer() per name.

Fan-out audit pattern. The most valuable day-to-day use is dependency health: take your composer.lock package list, resolve each via getComposer(), and check isAbandoned(), getReplacementPackage(), and the security-advisories endpoint. Run it as a scheduled command, cache results for a day, and post a diff to Slack.

Normalise result objects immediately. The result classes are plain hydrated objects with getters, not Eloquent models and not Arrayable. If you're returning data from an endpoint or storing it, convert to an array/DTO at the boundary of your service — don't hand Package objects to Blade and call a dozen getters in the template.

Handle the two exception types distinctly:

try {
    $package = $client->get($name);
} catch (\Packagist\Api\PackageNotFoundException $e) {
    // 404 — genuinely no such package; safe to cache a negative result
} catch (\InvalidArgumentException $e) {
    // response body wasn't parseable — upstream broke or a proxy returned HTML
}

A parse failure is an infrastructure signal; don't cache it as a miss.

Gotchas and Tips

  • No Laravel integration at all. No config publish, no facade, no container binding, no HTTP-client abstraction. Every one of those is yours to write. Budget an hour, not a minute.
  • getComposer() returns an array, not a package. It's keyed by name so Packagist can batch. Indexing [0] or assuming a single object is the most common first bug.
  • get() vs getComposer() metadata differs. getComposer() deliberately lacks some fields present in get(). If a getter returns unexpectedly empty, you're probably on the wrong method — check the Packagist API docs for which endpoint supplies what.
  • Nullable fields collide with non-nullable type declarations. History is littered with fixes here: null descriptions, null download counts, null dist reference/shasum, null version sources. Assume any string getter can blow up or return null and defend with ?? '' rather than trusting the signature.
  • Unknown properties are silently dropped since 2.0.3 (AbstractResult no longer sets arbitrary keys). If Packagist adds a field, you won't see it until the package's Factory/result class is updated. Don't build features on undocumented fields.
  • No retries, no backoff, no rate-limit handling. Packagist will 429/503 under load and the library surfaces whatever Guzzle throws. Wrap calls in retry() (Laravel's helper works fine around a closure) and set a sane timeout.
  • Set a User-Agent. Packagist asks API consumers to identify themselves. The Client constructor accepts an optional Guzzle client in the current major line — verify the exact signature in src/Packagist/Api/Client.php, then inject one with a UA header, timeouts, and (optionally) a caching middleware. That's also your cleanest extension point: cache at the Guzzle layer and skip the wrapper entirely.
  • setPackagistUrl() must be called before any request and applies to the whole client. Pointing at a local mirror/proxy is the best fix for both latency and rate limits in a busy app.
  • Abandoned packages need explicit handling. Check isAbandoned() and getReplacementPackage() before you surface a version recommendation — otherwise your "upgrade advisor" cheerfully recommends dead packages.
  • search() on no matches returns an empty iterable, not an exception. Only get()-style lookups throw PackageNotFoundException. Don't write one catch for both behaviours.
  • Testing: Client is a concrete class, which makes it awkward to mock. Either (a) inject a Guzzle client with a MockHandler if the constructor allows, or (b) — better — define your own PackagistService interface and bind the real implementation behind it. Option (b) also solves the next problem.
  • Maintenance risk is real. KNPLabs is publicly looking for maintainers, and the package is a ~500-line wrapper over public JSON endpoints. Encapsulate it behind your own interface so that if it stalls on a future PHP version you can swap to Http::get('https://repo.packagist.org/p2/{name}.json') in one class, not fifty. Pin ^2.1 and read the changelog before bumping — 2.x has had several narrow but real hydration bugs.
  • Debugging tip: when hydration fails with InvalidArgumentException, dump the raw body first. Nine times out of ten it's an HTML error page from a proxy or a 200 with an unexpectedly minified metadata shape (minified responses caused a genuine bug fixed in 2.0.0), not your code.
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.
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource