index/src/Version.php

246 lines
7.9 KiB
PHP

<?php
// Version.php
// Created: 2021-04-29
// Updated: 2023-01-01
namespace Index;
use InvalidArgumentException;
use Stringable;
/**
* A Semantic Versioning implementation. Following version 2.0.0 of the specification.
*
* @see https://semver.org/spec/v2.0.0.html
*/
class Version implements Stringable, IComparable, IEquatable {
private int $major;
private int $minor;
private int $patch;
private array $prerelease;
private array $build;
private ?string $versionString = null;
private static ?Version $empty = null;
/**
* Constructor for Version.
*
* @param int $major A positive integer indicating the major version.
* @param int $minor A positive integer indicating the minor version.
* @param int $patch A positive integer indicating the patch version.
* @param string $prerelease A dot separated string indicating prerelease information.
* @param string $build A dot separated string indicating build information.
* @throws InvalidArgumentException A negative integer was provided.
* @return Version
*/
public function __construct(int $major, int $minor = 0, int $patch = 0, string $prerelease = '', string $build = '') {
if($major < 0 || $minor < 0 || $patch < 0)
throw new InvalidArgumentException('$major, $minor and $patch should be positive integers.');
$this->major = $major;
$this->minor = $minor;
$this->patch = $patch;
$this->prerelease = empty($prerelease) ? [] : explode('.', $prerelease);
$this->build = empty($build) ? [] : explode('.', $build);
}
/**
* Gets the value of the major version component.
*
* @return int Major version component.
*/
public function getMajor(): int {
return $this->major;
}
/**
* Gets the value of the minor version component.
*
* @return int Minor version component.
*/
public function getMinor(): int {
return $this->minor;
}
/**
* Gets the value of the patch version component.
*
* @return int Patch version component.
*/
public function getPatch(): int {
return $this->patch;
}
/**
* Gets the split value of the prerelease component.
*
* @return array array containing the prerelease parts.
*/
public function getPrerelease(): array {
return $this->prerelease;
}
/**
* Gets the split value of the build component.
*
* @return array array contains the build parts.
*/
public function getBuild(): array {
return $this->build;
}
/**
* Tests if this Version instance represents a prerelease according to the SemVer spec.
*
* @return bool true if the version indicates a prerelease, false if not.
*/
public function isPrerelease(): bool {
return $this->major < 1 || !empty($this->prerelease);
}
/**
* Creates a new instance of Version with the major component incremented.
*
* This will implicitly drop prerelease and build info and reset the minor and patch component.
*
* @return Version the newly created instance of Version.
*/
public function incrementMajor(): Version {
return new Version($this->major + 1, 0, 0);
}
/**
* Creates a new instance of Version with the minor component incremented.
*
* This will implicitly drop the prerelease and build info and reset the patch component.
*
* @return Version the newly created instance of Version.
*/
public function incrementMinor(): Version {
return new Version($this->major, $this->minor + 1, 0);
}
/**
* Creates a new instance of Version with the patch component incremented.
*
* This will implicitly drop the prerelease and build info.
*
* @return Version the newly created instance of Version.
*/
public function incrementPatch(): Version {
return new Version($this->major, $this->minor, $this->patch + 1);
}
/**
* Checks this instance of Version with another for equality.
*
* The build component is ignored, as per the SemVer spec.
*
* @param mixed $other Another instance of Version.
* @return bool true if the instances are equal, false if not.
*/
public function equals(mixed $other): bool {
return $other instanceof Version
&& $other->major === $this->major
&& $other->minor === $this->minor
&& $other->patch === $this->patch
&& XArray::sequenceEquals($this->prerelease, $other->prerelease);
}
/**
* Compares this instance of Version with another.
*
* The build component is ignored and prerelease component is compared as per the SemVer spec.
*
* @param mixed $other Another instance of Version.
*/
public function compare(mixed $other): int {
if(!($other instanceof Version))
return PHP_INT_MIN;
$diff = $this->major <=> $other->major;
if($diff) return $diff;
$diff = $this->minor <=> $other->minor;
if($diff) return $diff;
$diff = $this->patch <=> $other->patch;
if($diff) return $diff;
$tpi = XArray::extractIterator($this->prerelease);
$opi = XArray::extractIterator($other->prerelease);
$tpi->rewind();
$opi->rewind();
$valid = $tpi->valid();
$diff = $opi->valid() <=> $valid;
if($diff) return $diff;
while($valid) {
$diff = $opi->current()->compare($tpi->current());
if($diff) return $diff;
$tpi->next();
$opi->next();
$valid = $tpi->valid();
$diff = $opi->valid() <=> $valid;
if($diff) return $diff;
}
return 0;
}
public function __toString(): string {
if($this->versionString === null) {
$string = $this->major . '.' . $this->minor . '.' . $this->patch;
if(!empty($this->prerelease))
$string .= '-' . implode('.', $this->prerelease);
if(!empty($this->build))
$string .= '+' . implode('.', $this->build);
$this->versionString = $string;
}
return $this->versionString;
}
/**
* Returns an empty Version instance.
*
* @return Version Instance of Version with all values set to 0.
*/
public static function empty(): Version {
if(self::$empty === null)
self::$empty = new Version(0);
return self::$empty;
}
/**
* Parses a version string.
*
* Parses a version string with a regex adapted from SemVer's documentation. Modified to allow an optional v prefix.
*
* @see https://semver.org/spec/v2.0.0.html#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
* @param string $versionString String containing a version number.
* @throws InvalidArgumentException Provided string does not represent a valid version.
* @return Version An instance of Version representing the provided version string.
*/
public static function parse(string $versionString): Version {
// Regex adapted from https://semver.org/spec/v2.0.0.html#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
if(!preg_match('#^v?(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P<build>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$#', $versionString, $matches))
throw new InvalidArgumentException('$versionString is not a valid version string.');
return new Version(
(int)$matches['major'],
(int)$matches['minor'],
(int)$matches['patch'],
$matches['prerelease'] ?? '',
$matches['build'] ?? ''
);
}
}