<?php
declare(strict_types=1);
namespace Dhii\Versions;
use Dhii\Package\Version\VersionInterface;
use DomainException;
use Exception;
use RangeException;
use RuntimeException;
use Stringable;
/**
* A value object containing information about a SemVer-compliant version.
*/
class Version implements VersionInterface
{
/**
* @var int
*/
protected $major;
/**
* @var int
*/
protected $minor;
/**
* @var int
*/
protected $patch;
/**
* @var string[]
*/
protected $preRelease;
/**
* @var string[]
*/
protected $build;
/**
* @param int $major The major version number. See {@see getMajor()}.
* @param int $minor The minor version number See {@see getMinor()}.
* @param int $patch The patch version number. See {@see getPatch()}.
* @param array<string|Stringable> $preRelease A list of pre-release identifiers. See {@see getPreRelease()}.
* @param array<string|Stringable> $build A list of build identifiers. See {@see getBuild()}.
*
* @throws RangeException If an identifier is malformed
* @throws Exception If problem creating.
*/
public function __construct(
int $major,
int $minor,
int $patch,
array $preRelease,
array $build
) {
$this->major = $major;
$this->minor = $minor;
$this->patch = $patch;
$this->preRelease = $this->normalizePreRelease($preRelease);
$this->build = $this->normalizeBuild($build);
}
/**
* @inheritDoc
*/
public function getMajor(): int
{
return $this->major;
}
/**
* @inheritDoc
*/
public function getMinor(): int
{
return $this->minor;
}
/**
* @inheritDoc
*/
public function getPatch(): int
{
return $this->patch;
}
/**
* @inheritDoc
*/
public function getPreRelease(): array
{
return $this->preRelease;
}
/**
* @inheritDoc
*/
public function getBuild(): array
{
return $this->build;
}
/**
* Normalizes an identifier.
*
* @param string $identifier The identifier to normalize.
* Must be a non-empty alphanumeric+hyphen string.
*
* @return string An identifier with all disallowed characters removed.
*
* @throws DomainException If identifier is malformed
* @throws Exception If problem normalizing.
*/
protected function normalizeIdentifier(string $identifier): string
{
$origIdentifier = $identifier;
$identifier = $this->replace('![^\d\w-]!', '', $identifier);
if (!strlen($identifier)) {
throw new DomainException(sprintf('Identifier "%1$s" normalized to "%2$s" is empty', $origIdentifier, $identifier));
}
return $identifier;
}
/**
* Normalizes a series of pre-release identifiers.
*
* Will remove all illegal characters.
*
* @param iterable|array<string|Stringable> $preRelease The series of identifiers to normalize.
* Each is a non-empty alphanumeric+hyphen string.
* If numeric, leading zeroes are not allowed.
*
* @return string[] A series of normalized pre-release identifiers.
*
* @throws RangeException If could not normalize.
* @throws Exception If problem normalizing.
*/
protected function normalizePreRelease(iterable $preRelease): array
{
$normalized = [];
foreach ($preRelease as $idx => $identifier) {
$identifier = (string) $identifier;
try {
$identifier = $this->normalizeIdentifier($identifier);
} catch (DomainException $e) {
throw new RangeException(sprintf('Pre-release identifier #%1$d "%2$s" cannot be normalized', $idx, $identifier), 0, $e);
}
if (is_numeric($identifier)) {
$identifier = (string) intval($identifier);
}
$normalized[] = $identifier;
}
return $normalized;
}
/**
* Normalizes a series of build identifiers.
*
* Will remove all illegal characters.
*
* @param iterable|array<string|Stringable> $build The series of identifiers to normalize.
* Each is a non-empty alphanumeric+hyphen string.
*
* @return string[] A series of normalized build identifiers.
*
* @throws RangeException If could not normalize.
* @throws Exception If problem normalizing.
*/
protected function normalizeBuild(iterable $build): array
{
$normalized = [];
foreach ($build as $idx => $identifier) {
$identifier = (string) $identifier;
try {
$identifier = $this->normalizeIdentifier($identifier);
} catch (DomainException $e) {
throw new RangeException(sprintf('Build identifier #%1$d "%2$s" cannot be normalized', $idx, $identifier), 0, $e);
}
$normalized[] = $identifier;
}
return $normalized;
}
/**
* Replaces occurrences of $pattern in $subject.
*
* @param string $pattern The pattern to use for replacing.
* @param string $replacement The replacement.
* @param string $subject The subject.
*
* @return string The result of replacement.
*
* @throws Exception If problem replacing.
*/
protected function replace(string $pattern, string $replacement, string $subject): string
{
$result = preg_replace($pattern, $replacement, $subject);
if ($result === null) {
$code = preg_last_error();
$code = $code ? $code : 0;
$message = preg_last_error_msg();
$message = !empty($message) ? $message : 'Could not replace';
throw new RuntimeException($message, $code);
}
return $result;
}
/**
* @inheritDoc
*/
public function __toString()
{
$version = "{$this->major}.{$this->minor}.{$this->patch}";
if (count($this->preRelease)) {
$version .= '-' . implode('.', $this->preRelease);
}
if (count($this->build)) {
$version .= '+' . implode('.', $this->build);
}
return $version;
}
}