samsonasik/package-versions
PHP 5.6 backport of ocramius/package-versions offering fast, no-IO access to Composer dependency versions from composer.lock. Call Versions::getVersion(), getShortVersion(), or getMajorVersion() to retrieve installed package versions at runtime.
This package is a PHP 5.6-compatible backport of ocramius/package-versions. It ships a Composer plugin that bakes installed package versions into a generated PackageVersions\Versions class at install/update time, so lookups are a static array read — no composer.lock parsing, no filesystem IO at runtime. Treat it as a legacy-era tool: it is only worth reaching for on Laravel 5.x codebases pinned to PHP 5.6/7.0, not on modern Laravel where Composer\InstalledVersions (Composer 2) already covers this.
Install and first verification
composer require samsonasik/package-versions
composer dump-autoload # regenerates the baked-in version map
The plugin installs itself, hooks Composer's install/update lifecycle, and rewrites the Versions class in vendor/. If Versions::getVersion() throws right after composer require, a dump-autoload usually fixes it.
Where to look first
vendor/samsonasik/package-versions/src/ — candidates should inspect the hardcoded map to understand what is (and isn't) included.composer.json / composer.lock — confirm the plugin is actually installed and which packages are in scope (dev requirements behave differently with --no-dev).getVersion() returns 1.0.0@<sha1>, getShortVersion() returns 1.0.0, getMajorVersion() returns 1.First use case
The natural day-one use is a build/version surface — a /health or /about endpoint, a log context entry, or a footer tag that reports the app and a couple of framework packages:
Route::get('/health', function () {
return response()->json([
'laravel' => \PackageVersions\Versions::getShortVersion('laravel/framework'),
'app_env' => app()->environment(),
'commit' => strstr(\PackageVersions\Versions::getVersion('laravel/framework'), '@', false),
]);
});
An assessment candidate should get from composer require to a working endpoint in minutes — the interesting signal is whether they notice the @hash suffix and choose getShortVersion() for display and getVersion() for cache keys.
Wrap the static class behind a service. Versions is a static utility with no interface, which makes it untestable and unswappable in isolation. The pattern to look for:
// app/Support/PackageVersionResolver.php
namespace App\Support;
class PackageVersionResolver
{
public function short(string $package, string $fallback = 'unknown'): string
{
try {
return \PackageVersions\Versions::getShortVersion($package);
} catch (\Exception $e) {
return $fallback;
}
}
public function major(string $package, $fallback = null)
{
try {
return \PackageVersions\Versions::getMajorVersion($package);
} catch (\Exception $e) {
return $fallback;
}
}
}
Bind it in a service provider, inject it where needed, and tests can mock the resolver instead of the static class. This also centralises the "package missing" fallback so a --no-dev deploy doesn't blow up an endpoint.
Config-driven package list. Rather than hardcoding package names across Blade views and controllers:
// config/versions.php
return [
'reported' => [
'laravel/framework',
'samsonasik/package-versions',
],
];
Then a Blade directive or view composer iterates the list. This is the difference between a one-off hack and something that survives a dependency bump.
Cache-busting and asset versioning. getVersion() returning version@reference is purpose-built for cache keys — the reference changes whenever the lock file changes:
$key = 'pkg:' . \PackageVersions\Versions::getVersion('vendor/library') . ':report';
Cache::remember($key, 3600, fn () => $this->buildReport());
Good candidates distinguish this from getShortVersion(): a tagged release 1.4.2 will not bust a cache when you redeploy a dev-master build, but the full 1.4.2@abc123 will.
Feature/compatibility branching. getMajorVersion() is the intended hook for "this dependency changed its API" logic:
if ($this->versions->major('spatie/laravel-permission') >= 5) {
return $this->newPermissionPath();
}
This is the anti-pattern to flag as well: branching on installed dependency versions at runtime is a code smell in application code. It is acceptable in a package's own compatibility shim or a migration window, not as a permanent strategy.
Artisan command for on-call. A php artisan app:versions (or enriching the existing about output) that dumps the reported packages plus PHP and Laravel versions is the highest-value daily use: one command an operator can paste into a ticket.
Log context. Push the app's own version plus a couple of key dependencies into a Monolog processor so every log line carries the deployed shape. This is the pattern most likely to appear in day-to-day work.
It is a Composer plugin, not just a library. On Composer 2 the plugin must be explicitly allowed (allow-plugins in composer.json), or installs fail with a prompt/error. Candidates on a 2016-era stack may not hit this, but anyone testing the package today will.
The data is baked at install time — it does not track reality. Editing vendor/ by hand, or a deploy that copies a prebuilt vendor directory, leaves the map stale. If the reported version looks wrong, composer dump-autoload is the first thing to try. The upside is the point of the package: zero runtime IO, so no lock-file read on every request.
Unknown packages throw. getVersion() raises an exception (an OutOfBoundsException subtype / PackageNotFoundException depending on version) for packages it doesn't know about. The root project itself is generally not in the map either — for the app's own version, read composer.json or a build-stamped constant, not this package. Always wrap with a fallback in anything user-facing.
getVersion() is not a clean version string. 1.0.0@0beec7... is the contract; the part after @ is the source reference, and for dev installs the whole string may look like dev-master@abc123. Anyone echoing it into a UI without getShortVersion() will ship an ugly footer.
No dev packages in production. composer install --no-dev produces a map without require-dev entries, so an endpoint reporting a dev dependency will 500 in production but pass locally. The resolver's fallback parameter exists precisely for this asymmetry.
Class collision with ocramius/package-versions. The backport deliberately occupies the same PackageVersions\Versions namespace and class name. Both cannot be installed in one project — if a transitive dependency pulls the original, you get an autoload conflict or silently the wrong implementation. composer why ocramius/package-versions is the diagnostic.
Static means untestable. Versions::getVersion() cannot be mocked in a PHPUnit test without the resolver wrapper shown above. If a candidate's code calls it directly in a controller and then writes tests that "pass locally," ask how those tests behave on a machine with a different composer.lock.
CI and environment drift. Two CI boxes with different lock references will report different getVersion() values. Never assert on the full hash in tests; assert on getShortVersion() or that the value is non-empty.
Version pinning in assertions. Treat reported versions as observability data, not configuration. If application behaviour depends on them, you have moved dependency resolution from composer.json into runtime code — the classic thing to catch in review.
Migration path. On PHP 7.1+/Composer 2, prefer Composer\InstalledVersions::getPrettyVersion() / getVersionRanges(), or ocramius/package-versions v2 (later composer/package-versions-deprecated). The backport has not been maintained since 2016, so any new Laravel work should plan the exit rather than deepen the integration.
How can I help you explore Laravel packages today?