diff --git a/.github/workflows/integrate.yaml b/.github/workflows/integrate.yaml
index d708c58..5730c68 100644
--- a/.github/workflows/integrate.yaml
+++ b/.github/workflows/integrate.yaml
@@ -59,7 +59,7 @@ jobs:
dependencies: "${{ matrix.dependencies }}"
- name: "Run backward-compatibility analysis with roave/backward-compatibility-check"
- run: "vendor/bin/roave-backward-compatibility-check --ansi --format=github-actions --from=64ced12"
+ run: "vendor/bin/roave-backward-compatibility-check --ansi --format=github-actions --from=6e3389d"
code-coverage:
name: "Code Coverage"
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 102c8d7..3c8c65f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,4 +8,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
For a full diff see [`64ced12...main`][64ced12...main].
+### Added
+
+- Added `Version` as a value object ([#1]), by [@localheinz]
+
[64ced12...main]: https://github.com/ergebnis/version/compare/64ced12...main
+
+[#1]: https://github.com/ergebnis/version/pull/1
+
+[@localheinz]: https://github.com/localheinz
diff --git a/Makefile b/Makefile
index f760fb9..c59444b 100644
--- a/Makefile
+++ b/Makefile
@@ -3,7 +3,7 @@ it: refactoring coding-standards security-analysis static-code-analysis tests ##
.PHONY: backward-compatibility-analysis
backward-compatibility-analysis: vendor ## Runs a backward-compatibility analysis with roave/backward-compatibility-check
- vendor/bin/roave-backward-compatibility-check --from=64ced12
+ vendor/bin/roave-backward-compatibility-check --from=6e3389d
.PHONY: code-coverage
code-coverage: vendor ## Collects coverage from running unit tests with phpunit/phpunit
diff --git a/README.md b/README.md
index c435ac5..ce4a59d 100644
--- a/README.md
+++ b/README.md
@@ -26,7 +26,34 @@ composer require ergebnis/version
## Usage
-💡 This is a great place for showing a few usage examples!
+### Create a `Version` from a `string`
+
+```php
+equals($two); // true
+$one->equals($three); // false
+```
## Changelog
diff --git a/psalm-baseline.xml b/psalm-baseline.xml
index eb79571..cc319ab 100644
--- a/psalm-baseline.xml
+++ b/psalm-baseline.xml
@@ -1,2 +1,9 @@
-
+
+
+
+ provideInvalidValue
+ provideValidValue
+
+
+
diff --git a/src/Example.php b/src/Exception/InvalidVersion.php
similarity index 58%
rename from src/Example.php
rename to src/Exception/InvalidVersion.php
index 9c9aca6..80806c1 100644
--- a/src/Example.php
+++ b/src/Exception/InvalidVersion.php
@@ -11,21 +11,15 @@
* @see https://github.com/ergebnis/version
*/
-namespace Ergebnis\Version;
+namespace Ergebnis\Version\Exception;
-final class Example
+final class InvalidVersion extends \InvalidArgumentException
{
- private function __construct(private readonly string $value)
- {
- }
-
public static function fromString(string $value): self
{
- return new self($value);
- }
-
- public function toString(): string
- {
- return $this->value;
+ return new self(\sprintf(
+ 'Value "%s" does not appear to be valid.',
+ $value,
+ ));
}
}
diff --git a/src/Version.php b/src/Version.php
new file mode 100644
index 0000000..b5ee541
--- /dev/null
+++ b/src/Version.php
@@ -0,0 +1,49 @@
+0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)(?:-(?P(?: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[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/';
+
+ private function __construct(private readonly string $value)
+ {
+ }
+
+ /**
+ * @throws Exception\InvalidVersion
+ */
+ public static function fromString(string $value): self
+ {
+ if (1 !== \preg_match(self::REGEX, $value)) {
+ throw Exception\InvalidVersion::fromString($value);
+ }
+
+ return new self($value);
+ }
+
+ public function toString(): string
+ {
+ return $this->value;
+ }
+
+ public function equals(self $other): bool
+ {
+ return $this->value === $other->value;
+ }
+}
diff --git a/test/Unit/ExampleTest.php b/test/Unit/ExampleTest.php
deleted file mode 100644
index bf498ab..0000000
--- a/test/Unit/ExampleTest.php
+++ /dev/null
@@ -1,33 +0,0 @@
-sentence();
-
- $example = Example::fromString($value);
-
- self::assertSame($value, $example->toString());
- }
-}
diff --git a/test/Unit/Exception/InvalidVersionTest.php b/test/Unit/Exception/InvalidVersionTest.php
new file mode 100644
index 0000000..40ed5b2
--- /dev/null
+++ b/test/Unit/Exception/InvalidVersionTest.php
@@ -0,0 +1,38 @@
+word();
+
+ $exception = Exception\InvalidVersion::fromString($value);
+
+ $message = \sprintf(
+ 'Value "%s" does not appear to be valid.',
+ $value,
+ );
+
+ self::assertSame($message, $exception->getMessage());
+ }
+}
diff --git a/test/Unit/VersionTest.php b/test/Unit/VersionTest.php
new file mode 100644
index 0000000..f1467d3
--- /dev/null
+++ b/test/Unit/VersionTest.php
@@ -0,0 +1,171 @@
+expectException(Exception\InvalidVersion::class);
+
+ Version::fromString($value);
+ }
+
+ /**
+ * @see https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
+ * @see https://regex101.com/r/Ly7O1x/3/
+ *
+ * @return \Generator
+ */
+ public static function provideInvalidValue(): \Generator
+ {
+ $values = [
+ '1',
+ '1.2',
+ '1.2.3-0123',
+ '1.2.3-0123.0123',
+ '1.1.2+.123',
+ '+invalid',
+ '-invalid',
+ '-invalid+invalid',
+ '-invalid.01',
+ 'alpha',
+ 'alpha.beta',
+ 'alpha.beta.1',
+ 'alpha.1',
+ 'alpha+beta',
+ 'alpha_beta',
+ 'alpha.',
+ 'alpha..',
+ 'beta',
+ '1.0.0-alpha_beta',
+ '-alpha.',
+ '1.0.0-alpha..',
+ '1.0.0-alpha..1',
+ '1.0.0-alpha...1',
+ '1.0.0-alpha....1',
+ '1.0.0-alpha.....1',
+ '1.0.0-alpha......1',
+ '1.0.0-alpha.......1',
+ '01.1.1',
+ '1.01.1',
+ '1.1.01',
+ '1.2',
+ '1.2.3.DEV',
+ '1.2-SNAPSHOT',
+ '1.2.31.2.3----RC-SNAPSHOT.12.09.1--..12+788',
+ '1.2-RC-SNAPSHOT',
+ '-1.0.3-gamma+b7718',
+ '+justmeta',
+ '9.8.7+meta+meta',
+ '9.8.7-whatever+meta+meta',
+ '99999999999999999999999.999999999999999999.99999999999999999----RC-SNAPSHOT.12.09.1--------------------------------..12',
+ ];
+
+ foreach ($values as $value) {
+ yield $value => [
+ $value,
+ ];
+ }
+ }
+
+ #[Framework\Attributes\DataProvider('provideValidValue')]
+ public function testFromStringReturnsVersion(string $value): void
+ {
+ $version = Version::fromString($value);
+
+ self::assertSame($value, $version->toString());
+ }
+
+ /**
+ * @see https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
+ * @see https://regex101.com/r/Ly7O1x/3/
+ *
+ * @return \Generator
+ */
+ public static function provideValidValue(): \Generator
+ {
+ $values = [
+ '0.0.4',
+ '1.2.3',
+ '10.20.30',
+ '1.1.2-prerelease+meta',
+ '1.1.2+meta',
+ '1.1.2+meta-valid',
+ '1.0.0-alpha',
+ '1.0.0-beta',
+ '1.0.0-alpha.beta',
+ '1.0.0-alpha.beta.1',
+ '1.0.0-alpha.1',
+ '1.0.0-alpha0.valid',
+ '1.0.0-alpha.0valid',
+ '1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay',
+ '1.0.0-rc.1+build.1',
+ '2.0.0-rc.1+build.123',
+ '1.2.3-beta',
+ '10.2.3-DEV-SNAPSHOT',
+ '1.2.3-SNAPSHOT-123',
+ '1.0.0',
+ '2.0.0',
+ '1.1.7',
+ '2.0.0+build.1848',
+ '2.0.1-alpha.1227',
+ '1.0.0-alpha+beta',
+ '1.2.3----RC-SNAPSHOT.12.9.1--.12+788',
+ '1.2.3----R-S.12.9.1--.12+meta',
+ '1.2.3----RC-SNAPSHOT.12.9.1--.12',
+ '1.0.0+0.build.1-rc.10000aaa-kk-0.1',
+ '99999999999999999999999.999999999999999999.99999999999999999',
+ '1.0.0-0A.is.legal',
+ ];
+
+ foreach ($values as $value) {
+ yield $value => [
+ $value,
+ ];
+ }
+ }
+
+ public function testEqualsReturnsFalseWhenValuesAreDifferent(): void
+ {
+ $faker = self::faker()->unique();
+
+ $one = Version::fromString($faker->regexify('(0|[1-9]+)\.(0|[1-9]+)\.(0|[1-9]+)'));
+ $two = Version::fromString($faker->regexify('(0|[1-9]+)\.(0|[1-9]+)\.(0|[1-9]+)'));
+
+ self::assertFalse($one->equals($two));
+ }
+
+ public function testEqualsReturnsTrueWhenValueIsSame(): void
+ {
+ $value = self::faker()->regexify('(0|[1-9]+)\.(0|[1-9]+)\.(0|[1-9]+)');
+
+ $one = Version::fromString($value);
+ $two = Version::fromString($value);
+
+ self::assertTrue($one->equals($two));
+ }
+}