parent
7e1f044d72
commit
a85d7d5fc6
@ -1,3 +1,3 @@
|
||||
# XXX
|
||||
# PhpSermVer
|
||||
|
||||
XXX
|
||||
Class representing a [semantic version](https://semver.org/).
|
@ -0,0 +1,396 @@
|
||||
<?php
|
||||
|
||||
namespace jrosset\SemVer;
|
||||
|
||||
use DirectoryIterator;
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
use SplFileInfo;
|
||||
|
||||
/**
|
||||
* A semantic version
|
||||
*
|
||||
* @link https://semver.org/ The semantic versionning (2.0.0)
|
||||
*/
|
||||
final class Version {
|
||||
/**
|
||||
* The separator between version number
|
||||
*/
|
||||
private const SEPARATOR_VERSION = '.';
|
||||
/**
|
||||
* The separator for pre-release
|
||||
*/
|
||||
private const SEPARATOR_PRE_RELEASE = '-';
|
||||
/**
|
||||
* The separator for build metadata
|
||||
*/
|
||||
private const SEPARATOR_BUILD_METADATA = '+';
|
||||
|
||||
/**
|
||||
* @var string The regular expression's pattern for the semantic version parsing
|
||||
*
|
||||
* @link https://regex101.com/r/gDKcBH/1
|
||||
*/
|
||||
private static string $constRegexPattern = '';
|
||||
|
||||
/**
|
||||
* @var int The major version
|
||||
*/
|
||||
private int $major;
|
||||
/**
|
||||
* @var int The minor version
|
||||
*/
|
||||
private int $minor;
|
||||
/**
|
||||
* @var int The patch version
|
||||
*/
|
||||
private int $patch;
|
||||
/**
|
||||
* @var VersionExtraPart The pre-release
|
||||
*/
|
||||
private VersionExtraPart $preRelease;
|
||||
/**
|
||||
* @var VersionExtraPart The build metadata
|
||||
*/
|
||||
private VersionExtraPart $buildMetadata;
|
||||
|
||||
/**
|
||||
* Create a semantic version
|
||||
*
|
||||
* @param int|null $major The major version
|
||||
* @param int|null $minor The minor version
|
||||
* @param int|null $patch The patch version
|
||||
* @param VersionExtraPart|string|null $preRelease The pre-release
|
||||
* @param VersionExtraPart|string|null $buildMetadata The build metadata
|
||||
*
|
||||
* @throws InvalidArgumentException If the semantic version is not valid
|
||||
*/
|
||||
public function __construct (?int $major = null, ?int $minor = null, ?int $patch = null, $preRelease = null, $buildMetadata = null) {
|
||||
$this->setMajor($major);
|
||||
$this->setMinor($minor);
|
||||
$this->setPatch($patch);
|
||||
|
||||
if (!$this->isEmpty()) {
|
||||
$this->setPreRelease($preRelease);
|
||||
$this->setBuildMetadata($buildMetadata);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Export the semantic version to a string
|
||||
*
|
||||
* @return string The semantic version to a string
|
||||
*/
|
||||
public function __toString (): string {
|
||||
if ($this->isEmpty()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$string = $this->getMajor() . self::SEPARATOR_VERSION . $this->getMinor() . self::SEPARATOR_VERSION . $this->getPatch();
|
||||
if (!$this->getPreRelease()->isNull()) {
|
||||
$string .= self::SEPARATOR_PRE_RELEASE . $this->getPreRelease();
|
||||
}
|
||||
if (!$this->getBuildMetadata()->isNull()) {
|
||||
$string .= self::SEPARATOR_BUILD_METADATA . $this->getBuildMetadata();
|
||||
}
|
||||
|
||||
return $string;
|
||||
}
|
||||
/**
|
||||
* Export the semantic version to a <u>short</u> string (only version numbers)
|
||||
*
|
||||
* @param bool $showEmptyPatch Show the patch version if null/0 ?
|
||||
*
|
||||
* @return string The semantic version to a <u>short</u> string
|
||||
*/
|
||||
public function toShortString (bool $showEmptyPatch = false): string {
|
||||
if ($this->isEmpty()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $this->getMajor() . self::SEPARATOR_VERSION . $this->getMinor() . (!$showEmptyPatch && $this->getPatch() === 0 ? '' : self::SEPARATOR_VERSION . $this->getPatch());
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a semantic version from a string
|
||||
*
|
||||
* @param string|null $semanticVersion The string to parse
|
||||
*
|
||||
* @return self The corresponding semantic version
|
||||
*
|
||||
* @throws InvalidArgumentException If the semantic version is not valid
|
||||
*/
|
||||
public static function parse (?string $semanticVersion): self {
|
||||
//region Construction pattern regex
|
||||
if (self::$constRegexPattern === '') {
|
||||
/** @noinspection RegExpSuspiciousBackref */
|
||||
self::$constRegexPattern = /** @lang PhpRegExp */
|
||||
'/'
|
||||
. '(?(DEFINE)(?\'VersionNumber\'0|[1-9][0-9]*))'
|
||||
. '(?(DEFINE)(?\'Identifier\'' . VersionExtraPart::REGEX_IDENTIFIER . '))'
|
||||
. '(?(DEFINE)(?\'ExtraPart\'(?P>Identifier)(?:' . preg_quote(VersionExtraPart::SEPARATOR_IDENTIFIERS, '/') . '(?P>Identifier))*))'
|
||||
. '^v?'
|
||||
. '(?<major>(?P>VersionNumber))(?:' . preg_quote(self::SEPARATOR_VERSION, '/')
|
||||
. '(?<minor>(?P>VersionNumber))(?:' . preg_quote(self::SEPARATOR_VERSION, '/')
|
||||
. '(?<patch>(?P>VersionNumber)))?)?'
|
||||
. '(?:' . preg_quote(self::SEPARATOR_PRE_RELEASE, '/') . '(?<preRelease>(?P>ExtraPart)))?'
|
||||
. '(?:' . preg_quote(self::SEPARATOR_BUILD_METADATA, '/') . '(?<buildMetadata>(?P>ExtraPart)))?'
|
||||
. '$/';
|
||||
}
|
||||
//endregion
|
||||
|
||||
if (preg_match(self::$constRegexPattern, $semanticVersion, $match) !== 1) {
|
||||
$match = [];
|
||||
}
|
||||
return new self (
|
||||
$match['major'] ?? 0,
|
||||
$match['minor'] ?? 0,
|
||||
$match['patch'] ?? 0,
|
||||
$match['preRelease'] ?? new VersionExtraPart(),
|
||||
$match['buildMetadata'] ?? new VersionExtraPart(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare with another semantic version
|
||||
*
|
||||
* @param self $other The other semantic version
|
||||
*
|
||||
* @return int 1 if current version is greater, 0 if they are equal, -1 if current version is lower
|
||||
*/
|
||||
public function compare (self $other): int {
|
||||
if ($this->getMajor() > $other->getMajor()) {
|
||||
return 1;
|
||||
}
|
||||
elseif ($this->getMajor() < $other->getMajor()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if ($this->getMinor() > $other->getMinor()) {
|
||||
return 1;
|
||||
}
|
||||
elseif ($this->getMinor() < $other->getMinor()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if ($this->getPatch() > $other->getPatch()) {
|
||||
return 1;
|
||||
}
|
||||
elseif ($this->getPatch() < $other->getPatch()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (($compare = $this->getPreRelease()->compare($other->getPreRelease())) !== 0) {
|
||||
return $compare;
|
||||
}
|
||||
if (($compare = $this->getBuildMetadata()->compare($other->getBuildMetadata())) !== 0) {
|
||||
return $compare;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the semantic version empty ?
|
||||
*
|
||||
* @return bool Is the semantic version empty ?
|
||||
*/
|
||||
public function isEmpty (): bool {
|
||||
return $this->getMajor() === 0
|
||||
&& $this->getMinor() === 0
|
||||
&& $this->getPatch() === 0;
|
||||
}
|
||||
/**
|
||||
* Alias for {@see Version::isNull()}
|
||||
*/
|
||||
public function isNull (): bool {
|
||||
return $this->isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* The major version
|
||||
*
|
||||
* @param bool $asNull Return null if 0
|
||||
*
|
||||
* @return int|null The major version
|
||||
*/
|
||||
public function getMajor (bool $asNull = false): ?int {
|
||||
return $asNull && $this->major === 0 ? null : $this->major;
|
||||
}
|
||||
/**
|
||||
* Set the major version
|
||||
*
|
||||
* @param int|null $major The new major version
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @throws InvalidArgumentException If the major version is not valid
|
||||
*/
|
||||
public function setMajor (?int $major): self {
|
||||
if ($major < 0) {
|
||||
throw new InvalidArgumentException('The major version is not valid');
|
||||
}
|
||||
|
||||
$this->major = $major ?? 0;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* The minor version
|
||||
*
|
||||
* @param bool $asNull Return null if 0
|
||||
*
|
||||
* @return int|null The minor version
|
||||
*/
|
||||
public function getMinor (bool $asNull = false): ?int {
|
||||
return $asNull && $this->minor === 0 ? null : $this->minor;
|
||||
}
|
||||
/**
|
||||
* Set the minor version
|
||||
*
|
||||
* @param int|null $minor The new minor version
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @throws InvalidArgumentException If the minor version is not valid
|
||||
*/
|
||||
public function setMinor (?int $minor): self {
|
||||
if ($minor < 0) {
|
||||
throw new InvalidArgumentException('The minor version is not valid');
|
||||
}
|
||||
|
||||
$this->minor = $minor ?? 0;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* The patch version
|
||||
*
|
||||
* @param bool $asNull Return null if 0
|
||||
*
|
||||
* @return int|null The patch version
|
||||
*/
|
||||
public function getPatch (bool $asNull = false): ?int {
|
||||
return $asNull && $this->patch === 0 ? null : $this->patch;
|
||||
}
|
||||
/**
|
||||
* Set the patch version
|
||||
*
|
||||
* @param int|null $patch The new patch version
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @throws InvalidArgumentException If the path version is not valid
|
||||
*/
|
||||
public function setPatch (?int $patch): self {
|
||||
if ($patch < 0) {
|
||||
throw new InvalidArgumentException('The path version is not valid');
|
||||
}
|
||||
|
||||
$this->patch = $patch ?? 0;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* The pre-release
|
||||
*
|
||||
* @param bool $asNull Return null if no pre-release
|
||||
*
|
||||
* @return VersionExtraPart|null The pre-release
|
||||
*/
|
||||
public function getPreRelease (bool $asNull = false): ?VersionExtraPart {
|
||||
return $asNull && $this->preRelease->isNull() ? null : $this->preRelease;
|
||||
}
|
||||
/**
|
||||
* Set the pre-release
|
||||
*
|
||||
* @param VersionExtraPart|string|null $preRelease The new pre-release
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @throws InvalidArgumentException If the pre-release is not valid
|
||||
*/
|
||||
public function setPreRelease ($preRelease): self {
|
||||
try {
|
||||
$this->preRelease = is_string($preRelease)
|
||||
? VersionExtraPart::parse($preRelease)
|
||||
: $preRelease ?? new VersionExtraPart();
|
||||
}
|
||||
catch (Exception $exception) {
|
||||
throw new InvalidArgumentException('The pre-release is not valid', 0, $exception);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* The build metadata
|
||||
*
|
||||
* @param bool $asNull Return null if no build metadata
|
||||
*
|
||||
* @return VersionExtraPart|null The build metadata
|
||||
*/
|
||||
public function getBuildMetadata (bool $asNull = false): ?VersionExtraPart {
|
||||
return $asNull && $this->buildMetadata->isNull() ? null : $this->buildMetadata;
|
||||
}
|
||||
/**
|
||||
* Set the build metadata
|
||||
*
|
||||
* @param VersionExtraPart|string|null $buildMetadata The new build metadata
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @throws InvalidArgumentException If the build metadata is not valid
|
||||
*/
|
||||
public function setBuildMetadata ($buildMetadata): self {
|
||||
try {
|
||||
$this->buildMetadata = is_string($buildMetadata)
|
||||
? VersionExtraPart::parse($buildMetadata)
|
||||
: $buildMetadata ?? new VersionExtraPart();
|
||||
}
|
||||
catch (Exception $exception) {
|
||||
throw new InvalidArgumentException('The pre-release is not valid', 0, $exception);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
//region Extraction depuis Git
|
||||
/**
|
||||
* Subdirectory of git for tags
|
||||
*/
|
||||
protected const PATH_GIT_TAGS = '/.git/refs/tags';
|
||||
|
||||
/**
|
||||
* Get last version from git (scan in tags)
|
||||
*
|
||||
* @param string $repositoryRootDir The git repository root directory (without the ".git")
|
||||
*
|
||||
* @return self The last version or an empty semantic version if none
|
||||
*/
|
||||
public static function getGitLastTag (string $repositoryRootDir): self {
|
||||
$versions = [];
|
||||
|
||||
/** @var SplFileInfo $file */
|
||||
$directoryIterator = new DirectoryIterator($repositoryRootDir . self::PATH_GIT_TAGS);
|
||||
foreach ($directoryIterator as $file) {
|
||||
if (!$file->isFile()) {
|
||||
continue;
|
||||
}
|
||||
if (($version = self::parse($file->getFilename()))->isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
$versions[] = $version;
|
||||
}
|
||||
if (count($versions) === 0) {
|
||||
return new self();
|
||||
}
|
||||
|
||||
usort(
|
||||
$versions,
|
||||
function (self $versionNumber1, self $versionNumber2): int {
|
||||
return $versionNumber1->compare($versionNumber2);
|
||||
}
|
||||
);
|
||||
return array_pop($versions);
|
||||
}
|
||||
//endregion
|
||||
}
|
@ -0,0 +1,181 @@
|
||||
<?php
|
||||
|
||||
namespace jrosset\SemVer;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* An extra part to version : pre-release or build metadata
|
||||
*/
|
||||
class VersionExtraPart {
|
||||
/**
|
||||
* The separator between identifiers
|
||||
*/
|
||||
public const SEPARATOR_IDENTIFIERS = '.';
|
||||
/**
|
||||
* The regular expression's pattern for a valid identifier
|
||||
*/
|
||||
public const REGEX_IDENTIFIER = '[1-9a-zA-Z-][0-9a-zA-Z-]*';
|
||||
/**
|
||||
* The regular expression's pattern for a valid identifier
|
||||
*/
|
||||
public const REGEX_IDENTIFIER_PHP = /** @lang PhpRegExp */
|
||||
'/^' . self::REGEX_IDENTIFIER . '$/';
|
||||
|
||||
/**
|
||||
* @var string[] The identifiers list
|
||||
*/
|
||||
private array $identifiers;
|
||||
|
||||
/**
|
||||
* Create an extra part
|
||||
*
|
||||
* @param string[]|null $identifiers The identifiers list
|
||||
*
|
||||
* @throws InvalidArgumentException If one of the identifier is not valid (cf. {@see VersionExtraPart::REGEX_IDENTIFIER_PHP})
|
||||
*/
|
||||
public function __construct (?array $identifiers = null) {
|
||||
$this->setIdentifiers($identifiers);
|
||||
}
|
||||
/**
|
||||
* Export the extra part to a string
|
||||
*
|
||||
* @return string The extra part to a string
|
||||
*/
|
||||
public function __toString (): string {
|
||||
return implode(self::SEPARATOR_IDENTIFIERS, $this->getIdentifiers());
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an extra part from a string
|
||||
*
|
||||
* @param string|null $extraPart The string to parse
|
||||
*
|
||||
* @return self The corresponding extra part
|
||||
*
|
||||
* @throws InvalidArgumentException If one of the identifier is not valid (cf. {@see VersionExtraPart::REGEX_IDENTIFIER_PHP})
|
||||
*/
|
||||
public static function parse (?string $extraPart): self {
|
||||
return new self(explode(self::SEPARATOR_IDENTIFIERS, $extraPart ?? ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the extra part empty ?
|
||||
*
|
||||
* @return bool Is the extra part empty ?
|
||||
*/
|
||||
public function isEmpty (): bool {
|
||||
return count($this->identifiers) === 0;
|
||||
}
|
||||
/**
|
||||
* Alias for {@see VersionExtraPart::isNull()}
|
||||
*/
|
||||
public function isNull (): bool {
|
||||
return $this->isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare with another extra part
|
||||
*
|
||||
* @param VersionExtraPart $other The other extra part
|
||||
*
|
||||
* @return int 1 if greater than, 0 is equal or -1 if lesser than
|
||||
*/
|
||||
public function compare (VersionExtraPart $other): int {
|
||||
$identifiers1 = $this->getIdentifiers();
|
||||
$identifiers2 = $other->getIdentifiers();
|
||||
|
||||
$maxSize = max(count($identifiers1), count($identifiers2));
|
||||
$identifiers1 = array_pad($identifiers1, $maxSize, null);
|
||||
$identifiers2 = array_pad($identifiers2, $maxSize, null);
|
||||
|
||||
for ($iterator = 0; $iterator < $maxSize; $iterator++) {
|
||||
if (($compare = self::compareIdentifier($identifiers1[$iterator], $identifiers2[$iterator])) === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return $compare;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
/**
|
||||
* Compare one identifier with another one
|
||||
*
|
||||
* @param string|null $identifier1 The first identifier
|
||||
* @param string|null $identifier2 The second identifier
|
||||
*
|
||||
* @return int 1 if the first is greater than the second, 0 is they are equals or -1 if the first is lesser than the second
|
||||
*/
|
||||
protected static function compareIdentifier (?string $identifier1, ?string $identifier2): int {
|
||||
//region Check set size (null if missing)
|
||||
if ($identifier1 === null && $identifier2 === null) {
|
||||
return 0;
|
||||
}
|
||||
elseif ($identifier1 !== null && $identifier2 === null) {
|
||||
return 1;
|
||||
}
|
||||
elseif ($identifier1 === null && $identifier2 !== null) {
|
||||
return -1;
|
||||
}
|
||||
//endregion
|
||||
//region Check digit-only vs string
|
||||
if (!self::isOnlyDigits($identifier1) && self::isOnlyDigits($identifier2)) {
|
||||
return 1;
|
||||
}
|
||||
elseif (self::isOnlyDigits($identifier1) && !self::isOnlyDigits($identifier2)) {
|
||||
return -1;
|
||||
}
|
||||
//endregion
|
||||
//region The comparison it-self
|
||||
if ($identifier1 == $identifier2) {
|
||||
return 0;
|
||||
}
|
||||
else {
|
||||
return $identifier1 > $identifier2 ? 1 : -1;
|
||||
}
|
||||
//endregion
|
||||
}
|
||||
/**
|
||||
* Check if a string contains only digits
|
||||
*
|
||||
* @param string $string The string to check
|
||||
*
|
||||
* @return bool Is the string contains only digits ?
|
||||
*/
|
||||
protected static function isOnlyDigits (string $string): bool {
|
||||
return preg_match('/^[0-9]+$/', $string) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* The identifiers list
|
||||
*
|
||||
* @param bool $asNull Return null if empty
|
||||
*
|
||||
* @return string[]|null The identifiers list
|
||||
*/
|
||||
public function getIdentifiers (bool $asNull = false): ?array {
|
||||
return $asNull && $this->isNull() ? null : $this->identifiers;
|
||||
}
|
||||
/**
|
||||
* Set the identifiers list
|
||||
*
|
||||
* @param array|null $identifiers The new identifiers list
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @throws InvalidArgumentException If one of the identifier is not valid (cf. {@see VersionExtraPart::REGEX_IDENTIFIER_PHP})
|
||||
*/
|
||||
protected function setIdentifiers (?array $identifiers): self {
|
||||
if ($identifiers !== null) {
|
||||
foreach ($identifiers as $identifier) {
|
||||
if (preg_match(self::REGEX_IDENTIFIER_PHP, $identifier) !== 1) {
|
||||
throw new InvalidArgumentException('The identifiant is not valid: ' . $identifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->identifiers = $identifiers ?? [];
|
||||
return $this;
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use jrosset\SemVer\Version;
|
||||
|
||||
echo '======= Empty version =======' . PHP_EOL;
|
||||
$version = new Version();
|
||||
var_dump((string)$version, $version->toShortString(), $version);
|
||||
|
||||
echo '======= Version "complète" =======' . PHP_EOL;
|
||||
$version = Version::parse('1.0.0-alpha.1-0+12587-6');
|
||||
var_dump((string)$version, $version->toShortString(), $version);
|
||||
|
||||
echo '======= Last git version =======' . PHP_EOL;
|
||||
$version = Version::getGitLastTag(__DIR__ . '/../');
|
||||
var_dump((string)$version, $version->toShortString(), $version);
|
Loading…
Reference in New Issue