vierbergenlars/php-semver
A PHP implementation of Semantic Versioning: parse, validate, compare, and sort SemVer version strings, and check them against constraints like ^1.2, ~2.0, or ranges. Lightweight, dependency-free, and ideal for package managers, release tooling, and update checks in any PHP project.
Install with Composer — the package is a plain library with no Laravel service provider, facade, config file, or migrations:
composer require vierbergenlars/php-semver
Everything autoloads under the vierbergenlars\Semver namespace. The three entry points you will use 95% of the time are:
Semver\Semver — static helper: satisfies(), satisfiedBy(), sort(), rsort(), maxSatisfying(), validVersion(), validRange()Semver\Version — parsed version object: parse(), getMajor(), getMinor(), getPatch(), getPreRelease(), getBuild(), getVersionString(), incMajor()/incMinor()/incPatch()Semver\Comparator — pairwise comparison: compare(), greaterThan(), lessThan(), equals(), etc.First practical use case: gate a feature or endpoint on a minimum application/client version.
use vierbergenlars\Semver\Semver;
use vierbergenlars\Semver\Comparator;
Semver::satisfies('2.4.1', '^2.0'); // true
Comparator::greaterThanOrEqualTo('2.4.1', '2.3.0'); // true
Semver::validVersion('2.4.1-rc.1'); // true
That is enough to ship a middleware or validation rule in a single sitting — no bootstrapping required.
Range gating from config. Keep the constraint in config/ so ops can change a cutoff without a deploy, and resolve the current version once in the container:
// AppServiceProvider::register()
$this->app->singleton(\App\Support\VersionGate::class, fn () => new \App\Support\VersionGate(
config('app.version', '0.0.0'),
));
// App\Support\VersionGate
final class VersionGate
{
public function __construct(private readonly string $current) {}
public function allows(string $range): bool
{
return Semver::validRange($range) && Semver::satisfies($this->current, $range);
}
}
HTTP middleware for client version headers. Mobile apps and SDKs typically send X-App-Version; compare it against a minimum before hitting controllers:
public function handle(Request $request, Closure $next): Response
{
$client = (string) $request->header('X-App-Version');
$min = (string) config('app.min_client_version');
if (! Semver::validVersion($client) || Comparator::lessThan($client, $min)) {
return response()->json(['error' => 'upgrade_required', 'min' => $min], 426);
}
return $next($request);
}
Note the two-step check: always validate with validVersion() before comparing, because Comparator and satisfies() assume parseable input.
A Laravel validation rule for stored version fields. Plugin manifests, package records, and release tables all benefit from rejecting junk at the boundary:
final class SemanticVersion implements ValidationRule
{
public function __construct(private readonly bool $range = false) {}
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$ok = $this->range
? Semver::validRange((string) $value)
: Semver::validVersion((string) $value);
if (! $ok) {
$fail("The {$attribute} field must be a valid semantic ".($this->range ? 'range.' : 'version.'));
}
}
}
Picking a compatible plugin/driver in an add-on system. This is where satisfiedBy() and maxSatisfying() earn their keep:
$candidates = $plugins->pluck('version')->all();
$compatible = Semver::satisfiedBy($candidates, '^3.1'); // all that fit
$best = Semver::maxSatisfying($candidates, '^3.1'); // highest that fits
$ordered = Semver::rsort($candidates); // newest-first for a changelog UI
$sorted = Semver::sort($candidates); // oldest-first for upgrade paths
Sanitize the input list first (array_filter($candidates, [Semver::class, 'validVersion'])) — one malformed string can abort the whole operation.
Version bumping in admin tooling. Instead of string surgery on "1.4.2":
$next = Version::parse($release->version)->incMinor(); // Version object, 1.5.0
$release->update(['version' => $next->getVersionString()]);
incMajor/incMinor/incPatch return new immutable instances, so it composes cleanly.
Blade/UI feature flags.
@if (app(VersionGate::class)->allows('>=2.3 <3.0'))
<x-new-dashboard />
@endif
Caching hot comparisons. Range checks are cheap but not free, and header middleware runs on every request. Memoize the small set of outcomes you actually care about:
$allowed = Cache::remember(
"gate:{$client}:{$min}",
now()->addHour(),
fn () => Comparator::greaterThanOrEqualTo($client, $min),
);
composer/semver, which is actively maintained and is what Composer itself uses. Keep this package where it already works, but do not make it a foundational dependency.NOASSERTION. GitHub could not detect a license, so verify LICENSE/composer.json yourself before shipping in a commercial product. Do not assume MIT.Semver::satisfies('2.0.0-rc.1', '^2.0.0') is false, matching node-semver semantics. If you want RCs to pass, the range itself must mention a pre-release (e.g. ^2.0.0-rc.1) or you must special-case it. This is the single most common source of "why did my beta build get rejected" tickets.1.0.0+build.5 and 1.0.0+build.9 compare equal; getBuild() is informational only. Do not encode ordering there.x.y.z is required for Version::parse(). "1.2" and "1" typically fail validation — normalize/coerce before parsing if your data is sloppy. Likewise, a leading v (v1.2.3) is not handled loosely; strip it with ltrim($value, 'vV') at the boundary.validVersion() / validRange() before comparing. Comparison on unparsed junk throws rather than returning false, so a try/catch around user-supplied strings is mandatory in middleware and form requests. valid* methods are the safe predicate form.Semver::sort() on a mixed list is a fast way to a 500.vierbergenlars\Semver, not Semver. PSR-4 case matters on Linux production images even when it "works" on macOS. Import the fully qualified class names or you will chase ghost autoload errors.Version is a concrete class without an interface; if you need testability, wrap it behind your own VersionGate-style contract rather than mocking Semver statics. That also lets you swap in composer/semver later without touching call sites.$raw, Semver::validVersion($raw), Version::parse($raw)->getVersionString(), and the range string together. Most surprises are (a) an empty/trimmed header, (b) a pre-release, or (c) a v prefix — in that order.composer.json before upgrading your runtime; an archived package will not gain compatibility with newer PHP minors. Pin it and test the upgrade explicitly.How can I help you explore Laravel packages today?