├── .gitattributes
├── .github
├── dependabot.yml
└── workflows
│ ├── ci.yml
│ └── docs-publish.yml
├── .gitignore
├── .php-cs-fixer.php
├── LICENSE
├── README.md
├── composer.json
├── phpdoc.dist.xml
├── phpstan.neon
├── phpunit.xml
├── sonar-project.properties
└── src
├── Constraints
├── Condition.php
├── Constraint.php
├── Op.php
├── Range.php
├── VersionComparator.php
└── VersionDescriptor.php
├── Inc.php
├── Patterns.php
├── PreRelease.php
├── SemverException.php
├── Traits
├── Comparable.php
├── Copyable.php
├── Iterator.php
├── NextProducer.php
├── PrimitiveComparable.php
├── Singles.php
├── Sortable.php
└── Validator.php
└── Version.php
/.gitattributes:
--------------------------------------------------------------------------------
1 | /tests export-ignore
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | schedule:
6 | interval: "weekly"
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: PHP Semver CI
2 |
3 | on:
4 | push:
5 | branches: [ '*' ]
6 | paths-ignore:
7 | - '**.md'
8 | pull_request:
9 | branches: [ master ]
10 |
11 | jobs:
12 | test:
13 | runs-on: ubuntu-latest
14 | strategy:
15 | matrix:
16 | php-versions: [ '8.1', '8.2', '8.3' ]
17 | steps:
18 | - uses: actions/checkout@v4
19 |
20 | - name: Setup PHP
21 | uses: shivammathur/setup-php@v2
22 | with:
23 | php-version: ${{ matrix.php-versions }}
24 |
25 | - name: Validate composer.json and composer.lock
26 | run: composer validate --strict
27 |
28 | - name: Cache Composer packages
29 | id: composer-cache
30 | uses: actions/cache@v4
31 | with:
32 | path: vendor
33 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
34 | restore-keys: |
35 | ${{ runner.os }}-php-
36 |
37 | - name: Install dependencies
38 | run: |
39 | composer install --prefer-dist --no-progress --no-suggest
40 |
41 | - name: Execute tests
42 | run: vendor/bin/phpunit
43 |
44 | analysis:
45 | needs: [test]
46 | runs-on: ubuntu-latest
47 | steps:
48 | - uses: actions/checkout@v4
49 | with:
50 | fetch-depth: 0
51 |
52 | - name: Setup PHP
53 | uses: shivammathur/setup-php@v2
54 | with:
55 | php-version: '8.1'
56 | extensions: xdebug
57 |
58 | - name: Validate composer.json and composer.lock
59 | run: composer validate --strict
60 |
61 | - name: Cache Composer packages
62 | id: composer-cache
63 | uses: actions/cache@v4
64 | with:
65 | path: vendor
66 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
67 | restore-keys: |
68 | ${{ runner.os }}-php-
69 |
70 | - name: Install dependencies
71 | run: |
72 | composer install --prefer-dist --no-progress --no-suggest
73 |
74 | - name: Execute coverage
75 | run: vendor/bin/phpunit --coverage-clover=coverage.xml --log-junit=tests.xml
76 | env:
77 | XDEBUG_MODE: coverage
78 |
79 | - name: SonarCloud Scan
80 | uses: SonarSource/sonarcloud-github-action@master
81 | env:
82 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
83 |
84 | php-cs-fixer:
85 | runs-on: ubuntu-latest
86 | steps:
87 | - uses: actions/checkout@v4
88 |
89 | - name: Setup PHP
90 | uses: shivammathur/setup-php@v2
91 | with:
92 | php-version: '8.1'
93 | ini-values: 'memory_limit=-1'
94 |
95 | - name: Validate composer.json and composer.lock
96 | run: composer validate --strict
97 |
98 | - name: Cache Composer packages
99 | id: composer-cache
100 | uses: actions/cache@v4
101 | with:
102 | path: vendor
103 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
104 | restore-keys: |
105 | ${{ runner.os }}-php-
106 |
107 | - name: Install dependencies
108 | run: |
109 | composer install --prefer-dist --no-progress --no-suggest
110 |
111 | - name: Execute Code Style Check
112 | run: vendor/bin/php-cs-fixer fix --ansi --diff --dry-run
113 |
114 | phpstan:
115 | runs-on: ubuntu-latest
116 | steps:
117 | - uses: actions/checkout@v4
118 |
119 | - name: Setup PHP
120 | uses: shivammathur/setup-php@v2
121 | with:
122 | php-version: '8.1'
123 | ini-values: 'memory_limit=-1'
124 |
125 | - name: Validate composer.json and composer.lock
126 | run: composer validate --strict
127 |
128 | - name: Cache Composer packages
129 | id: composer-cache
130 | uses: actions/cache@v4
131 | with:
132 | path: vendor
133 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
134 | restore-keys: |
135 | ${{ runner.os }}-php-
136 |
137 | - name: Install dependencies
138 | run: |
139 | composer install --prefer-dist --no-progress --no-suggest
140 |
141 | - name: Execute Static Analysis
142 | run: vendor/bin/phpstan analyse --ansi
--------------------------------------------------------------------------------
/.github/workflows/docs-publish.yml:
--------------------------------------------------------------------------------
1 | name: Docs Publish
2 |
3 | on:
4 | workflow_dispatch:
5 |
6 | jobs:
7 | deploy-gh-pages:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/checkout@v4
11 |
12 | - name: Generate Docs
13 | run: docker run --rm -v "$(pwd):/data" "phpdoc/phpdoc:3"
14 |
15 | - name: Deploy docs to GitHub Pages
16 | uses: JamesIves/github-pages-deploy-action@v4.7.3
17 | with:
18 | branch: gh-pages
19 | folder: docs
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor
2 | composer.lock
3 | .idea/
4 | .phpdoc
5 | *.cache
6 | composer.phar
7 | docs/
8 | .DS_Store
--------------------------------------------------------------------------------
/.php-cs-fixer.php:
--------------------------------------------------------------------------------
1 | in(__DIR__.'/src')
5 | ->in(__DIR__.'/tests')
6 | ;
7 |
8 | $config = (new PhpCsFixer\Config())
9 | ->setRules([
10 | '@PhpCsFixer' => true,
11 | '@PSR2' => true,
12 | 'php_unit_internal_class' => false,
13 | 'php_unit_test_class_requires_covers' => false,
14 | 'global_namespace_import' => [
15 | 'import_classes' => true,
16 | 'import_constants' => true,
17 | 'import_functions' => false,
18 | ],
19 | ])
20 | ->setUsingCache(true)
21 | ->setFinder($finder)
22 | ;
23 |
24 | return $config;
25 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Peter Csajtai
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # php-semver
2 | [](https://github.com/z4kn4fein/php-semver/actions/workflows/ci.yml)
3 | [](https://packagist.org/packages/z4kn4fein/php-semver)
4 | [](https://packagist.org/packages/z4kn4fein/php-semver)
5 | [](https://sonarcloud.io/project/overview?id=z4kn4fein_php-semver)
6 | [](https://sonarcloud.io/project/overview?id=z4kn4fein_php-semver)
7 |
8 | Semantic Versioning library for PHP. It implements the full [semantic version 2.0.0](https://semver.org/spec/v2.0.0.html) specification and
9 | provides ability to **parse**, **compare**, and **increment** semantic versions along with validation against **constraints**.
10 |
11 | ## Requirements
12 | | Version | PHP Version |
13 | |---------------|-------------|
14 | | `>=1.0, <1.2` | >=5.5 |
15 | | `>=1.2, <3.0` | >=7.1 |
16 | | `>=3.0` | >=8.1 |
17 |
18 |
19 | ## Install with [Composer](https://getcomposer.org/)
20 | ```shell
21 | composer require z4kn4fein/php-semver
22 | ```
23 |
24 | ## Usage
25 | The following options are supported to construct a `Version`:
26 | 1. Building part by part with `Version::create()`.
27 |
28 | ```php
29 | Version::create(3, 5, 2, "alpha", "build");
30 | ```
31 |
32 | 2. Parsing from a string with `Version::parse()` or `Version::parseOrNull()`.
33 |
34 | ```php
35 | Version::parse("3.5.2-alpha+build");
36 | ```
37 |
38 | The following information is accessible on a constructed `Version` object:
39 | ```php
40 | getMajor(); // 2
47 | echo $version->getMinor(); // 5
48 | echo $version->getPatch(); // 6
49 | echo $version->getPreRelease(); // alpha.12
50 | echo $version->getBuildMeta(); // build.34
51 | echo $version->isPreRelease(); // true
52 | echo $version->isStable(); // false
53 | echo $version->withoutSuffixes(); // 2.5.6
54 | echo $version; // 2.5.6-alpha.12+build.34
55 | ```
56 |
57 | ### Strict vs. Loose Parsing
58 | By default, the version parser considers partial versions like `1.0` and versions starting with the `v` prefix invalid.
59 | This behaviour can be turned off by setting the `strict` parameter to `false`.
60 | ```php
61 | echo Version::parse("v2.3-alpha"); // exception
62 | echo Version::parse("2.1"); // exception
63 | echo Version::parse("v3"); // exception
64 |
65 | echo Version::parse("v2.3-alpha", false); // 2.3.0-alpha
66 | echo Version::parse("2.1", false); // 2.1.0
67 | echo Version::parse("v3", false); // 3.0.0
68 | ```
69 |
70 | ## Compare
71 | It is possible to compare two `Version` objects with the following comparison methods.
72 | ```php
73 | isLessThan(Version::parse("2.3.1")); // false
99 | echo $version->isLessThanOrEqual(Version::parse("2.5.6-alpha.15")); // true
100 | echo $version->isGreaterThan(Version::parse("2.5.6")); // false
101 | echo $version->isLessThanOrEqual(Version::parse("2.5.6-alpha.12")); // true
102 | echo $version->isEqual(Version::parse("2.5.6-alpha.12+build.56")); // true
103 | echo $version->isNotEqual(Version::parse("2.2.4")); // true
104 | ```
105 |
106 | ### Sort
107 |
108 | `Version::sort()` and `Version::sortString()` are available to sort an array of versions.
109 | ```php
110 | =1.2.0`.
204 | The condition `>=1.2.0` would be met by any version that greater than or equal to `1.2.0`.
205 |
206 | Supported comparison operators:
207 | - `=` Equal (equivalent to no operator: `1.2.0` means `=1.2.0`)
208 | - `!=` Not equal
209 | - `<` Less than
210 | - `<=` Less than or equal
211 | - `>` Greater than
212 | - `>=` Greater than or equal
213 |
214 | Conditions can be joined together with whitespace, representing the `AND` logical operator between them.
215 | The `OR` operator can be expressed with `||` or `|` between condition sets.
216 |
217 | For example, the constraint `>=1.2.0 <3.0.0 || >4.0.0` translates to: *Only those versions are allowed that are either greater than or
218 | equal to `1.2.0` {**AND**} less than `3.0.0` {**OR**} greater than `4.0.0`*.
219 |
220 | We can notice that the first part of the previous constraint (`>=1.2.0 <3.0.0`) is a simple semantic version range.
221 | There are more ways to express version ranges; the following section will go through all the available options.
222 |
223 | ### Range Conditions
224 | There are particular range indicators which are sugars for more extended range expressions.
225 |
226 | - **X-Range**: The `x`, `X`, and `*` characters can be used as a wildcard for the numeric parts of a version.
227 | - `1.2.x` translates to `>=1.2.0 <1.3.0-0`
228 | - `1.x` translates to `>=1.0.0 <2.0.0-0`
229 | - `*` translates to `>=0.0.0`
230 |
231 | In partial version expressions, the missing numbers are treated as wildcards.
232 | - `1.2` means `1.2.x` which finally translates to `>=1.2.0 <1.3.0-0`
233 | - `1` means `1.x` or `1.x.x` which finally translates to `>=1.0.0 <2.0.0-0`
234 |
235 | - **Hyphen Range**: Describes an inclusive version range. Wildcards are evaluated and taken into account in the final range.
236 | - `1.0.0 - 1.2.0` translates to `>=1.0.0 <=1.2.0`
237 | - `1.1 - 1.4.0` means `>=(>=1.1.0 <1.2.0-0) <=1.4.0` which finally translates to `>=1.1.0 <=1.4.0`
238 | - `1.1.0 - 2` means `>=1.1.0 <=(>=2.0.0 <3.0.0-0)` which finally translates to `>=1.1.0 <3.0.0-0`
239 |
240 | - **Tilde Range (`~`)**: Describes a patch level range when the minor version is specified or a minor level range when it's not.
241 | - `~1.0.1` translates to `>=1.0.1 <1.1.0-0`
242 | - `~1.0` translates to `>=1.0.0 <1.1.0-0`
243 | - `~1` translates to `>=1.0.0 <2.0.0-0`
244 | - `~1.0.0-alpha.1` translates to `>=1.0.1-alpha.1 <1.1.0-0`
245 |
246 | - **Caret Range (`^`)**: Describes a range with regard to the most left non-zero part of the version.
247 | - `^1.1.2` translates to `>=1.1.2 <2.0.0-0`
248 | - `^0.1.2` translates to `>=0.1.2 <0.2.0-0`
249 | - `^0.0.2` translates to `>=0.0.2 <0.0.3-0`
250 | - `^1.2` translates to `>=1.2.0 <2.0.0-0`
251 | - `^1` translates to `>=1.0.0 <2.0.0-0`
252 | - `^0.1.2-alpha.1` translates to `>=0.1.2-alpha.1 <0.2.0-0`
253 |
254 | ### Validation
255 | Let's see how we can determine whether a version satisfies a constraint or not.
256 | ```php
257 | =1.2.0");
263 | $version = Version::parse("1.2.1");
264 |
265 | echo $version->isSatisfying($constraint); // true
266 | echo $constraint->isSatisfiedBy($version); // true
267 |
268 | // Or using the static satisfies() method with strings:
269 | echo Version::satisfies("1.2.1", ">=1.2.0"); // true
270 | ```
271 |
272 | ## Increment
273 | `Version` objects can produce incremented versions of themselves with the `getNext{Major|Minor|Patch|PreRelease}Version` methods.
274 | These methods can be used to determine the next version in order incremented by the according part.
275 | `Version` objects are **immutable**, so each incrementing function creates a new `Version`.
276 |
277 | This example shows how the incrementation works on a stable version:
278 | ```php
279 | getNextMajorVersion(); // 2.0.0
287 | echo $stableVersion->getNextMinorVersion(); // 1.1.0
288 | echo $stableVersion->getNextPatchVersion(); // 1.0.1
289 | echo $stableVersion->getNextPreReleaseVersion(); // 1.0.1-0
290 |
291 | // or with the inc() method:
292 | echo $stableVersion->inc(Inc::MAJOR); // 2.0.0
293 | echo $stableVersion->inc(Inc::MINOR); // 1.1.0
294 | echo $stableVersion->inc(Inc::PATCH); // 1.0.1
295 | echo $stableVersion->inc(Inc::PRE_RELEASE); // 1.0.1-0
296 | ```
297 |
298 | In case of an unstable version:
299 | ```php
300 | getNextMajorVersion(); // 2.0.0
308 | echo $unstableVersion->getNextMinorVersion(); // 1.1.0
309 | echo $unstableVersion->getNextPatchVersion(); // 1.0.0
310 | echo $unstableVersion->getNextPreReleaseVersion(); // 1.0.0-alpha.3
311 |
312 | // or with the inc() method:
313 | echo $unstableVersion->inc(Inc::MAJOR); // 2.0.0
314 | echo $unstableVersion->inc(Inc::MINOR); // 1.1.0
315 | echo $unstableVersion->inc(Inc::PATCH); // 1.0.0
316 | echo $unstableVersion->inc(Inc::PRE_RELEASE); // 1.0.0-alpha.3
317 | ```
318 |
319 | Each incrementing function provides the option to set a pre-release identity on the incremented version.
320 | ```php
321 | getNextMajorVersion("beta"); // 2.0.0-beta
329 | echo $version->getNextMinorVersion(""); // 1.1.0-0
330 | echo $version->getNextPatchVersion("alpha"); // 1.0.1-alpha
331 | echo $version->getNextPreReleaseVersion("alpha"); // 1.0.0-alpha.2
332 |
333 | // or with the inc() method:
334 | echo $version->inc(Inc::MAJOR, "beta"); // 2.0.0-beta
335 | echo $version->inc(Inc::MINOR, ""); // 1.1.0-0
336 | echo $version->inc(Inc::PATCH, "alpha"); // 1.0.1-alpha
337 | echo $version->inc(Inc::PRE_RELEASE, "alpha"); // 1.0.0-alpha.2
338 | ```
339 |
340 | ## Copy
341 | It's possible to make a copy of a particular version with the `copy()` method.
342 | It allows altering the copied version's properties with optional parameters.
343 | ```php
344 | $version = Version::parse("1.0.0-alpha.2+build.1");
345 |
346 | echo $version->copy(); // 1.0.0-alpha.2+build.1
347 | echo $version->copy(3); // 3.0.0-alpha.2+build.1
348 | echo $version->copy(null, 4); // 1.4.0-alpha.2+build.1
349 | echo $version->copy(null, null, 5); // 1.0.5-alpha.2+build.1
350 | echo $version->copy(null, null, null, "alpha.4"); // 1.0.0-alpha.4+build.1
351 | echo $version->copy(null, null, null, null, "build.3"); // 1.0.0-alpha.2+build.3
352 | echo $version->copy(3, 4, 5); // 3.4.5-alpha.2+build.1
353 | ```
354 | > [!NOTE]\
355 | > Without setting any optional parameter, the `copy()` method will produce an exact copy of the original version.
356 |
357 | ## Invalid version handling
358 | When the version or constraint parsing fails due to an invalid format, the library throws a specific `SemverException`.
359 | > [!NOTE]\
360 | > The `Version::parseOrNull()` and `Constraint::parseOrNull()` methods can be used for exception-less conversions as they return `null` when the parsing fails.
361 |
362 | ## Contact & Support
363 | - Create an [issue](https://github.com/z4kn4fein/php-semver/issues) for bug reports and feature requests.
364 | - Start a [discussion](https://github.com/z4kn4fein/php-semver/discussions) for your questions and ideas.
365 | - Add a ⭐️ to support the project!
366 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "z4kn4fein/php-semver",
3 | "type": "library",
4 | "description": "Semantic Versioning library for PHP. It implements the full semantic version 2.0.0 specification and provides ability to parse, compare, and increment semantic versions along with validation against constraints.",
5 | "minimum-stability": "stable",
6 | "keywords": ["semantic", "versioning", "semver", "version", "validation", "comparison"],
7 | "homepage": "https://github.com/z4kn4fein/php-semver",
8 | "license": "MIT",
9 | "authors": [
10 | {
11 | "name": "Peter Csajtai",
12 | "email": "peter.csajtai@outlook.com"
13 | }
14 | ],
15 | "require": {
16 | "php": ">=8.1"
17 | },
18 | "require-dev": {
19 | "phpunit/phpunit": "^10",
20 | "phpstan/phpstan": "^1.0",
21 | "friendsofphp/php-cs-fixer": "^3.0"
22 | },
23 | "autoload": {
24 | "psr-4": {
25 | "z4kn4fein\\SemVer\\": "src/"
26 | }
27 | },
28 | "autoload-dev": {
29 | "psr-4": {
30 | "z4kn4fein\\SemVer\\Tests\\": "tests/"
31 | }
32 | }
33 | }
--------------------------------------------------------------------------------
/phpdoc.dist.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 | PHP Semver
8 |
9 |
10 |
11 |
12 |
13 |
14 | src
15 |
16 |
17 | php
18 |
19 | public
20 | z4kn4fein
21 |
22 |
23 |
--------------------------------------------------------------------------------
/phpstan.neon:
--------------------------------------------------------------------------------
1 | parameters:
2 | level: 8
3 | paths:
4 | - src
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | tests
6 |
7 |
8 |
9 |
10 | src
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/sonar-project.properties:
--------------------------------------------------------------------------------
1 | sonar.projectKey=z4kn4fein_php-semver
2 | sonar.organization=z4kn4fein
3 | sonar.sources=src
4 | sonar.tests=tests
5 |
6 | sonar.php.tests.reportPath=tests.xml
7 | sonar.php.coverage.reportPaths=coverage.xml
--------------------------------------------------------------------------------
/src/Constraints/Condition.php:
--------------------------------------------------------------------------------
1 | operator = $operator;
24 | $this->version = $version;
25 | }
26 |
27 | /**
28 | * @return string the string representation of the condition
29 | */
30 | public function __toString(): string
31 | {
32 | return $this->operator.$this->version;
33 | }
34 |
35 | /**
36 | * @throws SemverException
37 | */
38 | public function isSatisfiedBy(Version $version): bool
39 | {
40 | return match ($this->operator) {
41 | Op::EQUAL => $version->isEqual($this->version),
42 | Op::NOT_EQUAL => !$version->isEqual($this->version),
43 | Op::LESS_THAN => $version->isLessThan($this->version),
44 | Op::LESS_THAN_OR_EQUAL, Op::LESS_THAN_OR_EQUAL2 => $version->isLessThanOrEqual($this->version),
45 | Op::GREATER_THAN => $version->isGreaterThan($this->version),
46 | Op::GREATER_THAN_OR_EQUAL, Op::GREATER_THAN_OR_EQUAL2 => $version->isGreaterThanOrEqual($this->version),
47 | default => throw new SemverException(sprintf('Invalid operator in condition %s', $this)),
48 | };
49 | }
50 |
51 | /**
52 | * @throws SemverException
53 | */
54 | public function opposite(): string
55 | {
56 | return match ($this->operator) {
57 | Op::EQUAL => Op::NOT_EQUAL.$this->version,
58 | Op::NOT_EQUAL => Op::EQUAL.$this->version,
59 | Op::LESS_THAN => Op::GREATER_THAN_OR_EQUAL.$this->version,
60 | Op::LESS_THAN_OR_EQUAL, Op::LESS_THAN_OR_EQUAL2 => Op::GREATER_THAN.$this->version,
61 | Op::GREATER_THAN => Op::LESS_THAN_OR_EQUAL.$this->version,
62 | Op::GREATER_THAN_OR_EQUAL, Op::GREATER_THAN_OR_EQUAL2 => Op::LESS_THAN.$this->version,
63 | default => throw new SemverException(sprintf('Invalid operator in condition %s', $this)),
64 | };
65 | }
66 |
67 | public static function greaterThanMin(): Condition
68 | {
69 | return self::single('greaterThanMin', function () {
70 | return new Condition(Op::GREATER_THAN_OR_EQUAL, Version::minVersion());
71 | });
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/Constraints/Constraint.php:
--------------------------------------------------------------------------------
1 | comparators = $comparators;
34 | }
35 |
36 | /**
37 | * @return string the string representation of the constraint
38 | */
39 | public function __toString(): string
40 | {
41 | $result = array_map(function ($comparator) {
42 | return implode(' ', $comparator);
43 | }, $this->comparators);
44 |
45 | return implode(' || ', $result);
46 | }
47 |
48 | /**
49 | * Determines whether this constraint is satisfied by a Version or not.
50 | *
51 | * @param Version $version the version to check
52 | *
53 | * @return bool true when the version satisfies the constraint, otherwise false
54 | */
55 | public function isSatisfiedBy(Version $version): bool
56 | {
57 | return self::any($this->comparators, function (array $comparator) use ($version) {
58 | return self::all($comparator, function (VersionComparator $condition) use ($version) {
59 | return $condition->isSatisfiedBy($version);
60 | });
61 | });
62 | }
63 |
64 | /**
65 | * The default constraint (>=0.0.0).
66 | *
67 | * @return Constraint the default constraint
68 | */
69 | public static function default(): Constraint
70 | {
71 | return self::single('default-constraint', function () {
72 | return new Constraint([[Condition::greaterThanMin()]]);
73 | });
74 | }
75 |
76 | /**
77 | * Parses a new constraint from the given string.
78 | *
79 | * @param string $constraintString the string to parse
80 | *
81 | * @return null|Constraint the parsed constraint, or null if the parse fails
82 | */
83 | public static function parseOrNull(string $constraintString): ?Constraint
84 | {
85 | try {
86 | return self::parse($constraintString);
87 | } catch (Exception) {
88 | return null;
89 | }
90 | }
91 |
92 | /**
93 | * Parses a new constraint from the given string.
94 | *
95 | * @param string $constraintString the string to parse
96 | *
97 | * @return Constraint the parsed constraint
98 | *
99 | * @throws SemverException when the constraint string is invalid
100 | */
101 | public static function parse(string $constraintString): Constraint
102 | {
103 | $constraintString = trim($constraintString);
104 | if (empty($constraintString)) {
105 | return self::default();
106 | }
107 |
108 | $orParts = explode('|', $constraintString);
109 | $orParts = array_filter($orParts);
110 |
111 | $comps = array_map(function ($comparator) use ($constraintString) {
112 | $result = [];
113 | $escaped = preg_replace_callback(
114 | Patterns::HYPHEN_CONDITION_REGEX,
115 | function ($matches) use (&$result) {
116 | $result[] = self::hyphenToComparator($matches);
117 |
118 | return '';
119 | },
120 | $comparator
121 | );
122 |
123 | if (empty($escaped)) {
124 | return $result;
125 | }
126 |
127 | $escaped = trim($escaped);
128 | if (!empty($escaped) && !preg_match(Patterns::VALID_OPERATOR_CONDITION_REGEX, $escaped)) {
129 | throw new SemverException(sprintf('Invalid constraint: %s', $constraintString));
130 | }
131 |
132 | self::ensure(
133 | (bool) preg_match_all(
134 | Patterns::OPERATOR_CONDITION_REGEX,
135 | $escaped,
136 | $matches,
137 | PREG_SET_ORDER
138 | ),
139 | sprintf('Invalid constraint: %s', $constraintString)
140 | );
141 |
142 | foreach ($matches as $match) {
143 | $result[] = self::operatorToComparator($match);
144 | }
145 |
146 | return $result;
147 | }, $orParts);
148 |
149 | self::ensure(self::any($comps, function (array $comparator) {
150 | return !empty($comparator);
151 | }), sprintf('Invalid constraint: %s', $constraintString));
152 |
153 | return new Constraint($comps);
154 | }
155 |
156 | /**
157 | * @param string[] $matches
158 | *
159 | * @throws SemverException
160 | */
161 | private static function hyphenToComparator(array $matches): VersionComparator
162 | {
163 | $startDescriptor = new VersionDescriptor(
164 | $matches[1],
165 | isset($matches[2]) && '' !== $matches[2] ? $matches[2] : null,
166 | isset($matches[3]) && '' !== $matches[3] ? $matches[3] : null,
167 | isset($matches[4]) && '' !== $matches[4] ? $matches[4] : null,
168 | isset($matches[5]) && '' !== $matches[5] ? $matches[5] : null
169 | );
170 | $endDescriptor = new VersionDescriptor(
171 | $matches[6],
172 | isset($matches[7]) && '' !== $matches[7] ? $matches[7] : null,
173 | isset($matches[8]) && '' !== $matches[8] ? $matches[8] : null,
174 | isset($matches[9]) && '' !== $matches[9] ? $matches[9] : null,
175 | isset($matches[10]) && '' !== $matches[10] ? $matches[10] : null
176 | );
177 |
178 | return new Range(
179 | $startDescriptor->toComparator(Op::GREATER_THAN_OR_EQUAL),
180 | $endDescriptor->toComparator(Op::LESS_THAN_OR_EQUAL),
181 | Op::EQUAL
182 | );
183 | }
184 |
185 | /**
186 | * @param string[] $matches
187 | *
188 | * @throws SemverException
189 | */
190 | private static function operatorToComparator(array $matches): VersionComparator
191 | {
192 | $operator = isset($matches[1]) && '' !== $matches[1] ? $matches[1] : Op::EQUAL;
193 | $descriptor = new VersionDescriptor(
194 | $matches[2],
195 | isset($matches[3]) && '' !== $matches[3] ? $matches[3] : null,
196 | isset($matches[4]) && '' !== $matches[4] ? $matches[4] : null,
197 | isset($matches[5]) && '' !== $matches[5] ? $matches[5] : null,
198 | isset($matches[6]) && '' !== $matches[6] ? $matches[6] : null
199 | );
200 |
201 | return $descriptor->fromOperator($operator);
202 | }
203 | }
204 |
--------------------------------------------------------------------------------
/src/Constraints/Op.php:
--------------------------------------------------------------------------------
1 | ';
18 | const GREATER_THAN_OR_EQUAL = '>=';
19 | const GREATER_THAN_OR_EQUAL2 = '=>';
20 | }
21 |
--------------------------------------------------------------------------------
/src/Constraints/Range.php:
--------------------------------------------------------------------------------
1 | start = $start;
22 | $this->end = $end;
23 | $this->operator = $operator;
24 | }
25 |
26 | /**
27 | * @return string the string representation of the range
28 | */
29 | public function __toString(): string
30 | {
31 | return $this->toStringByOp($this->operator);
32 | }
33 |
34 | /**
35 | * @throws SemverException
36 | */
37 | public function isSatisfiedBy(Version $version): bool
38 | {
39 | return match ($this->operator) {
40 | Op::EQUAL => $this->start->isSatisfiedBy($version) && $this->end->isSatisfiedBy($version),
41 | Op::NOT_EQUAL => !$this->start->isSatisfiedBy($version) || !$this->end->isSatisfiedBy($version),
42 | Op::LESS_THAN => !$this->start->isSatisfiedBy($version) && $this->end->isSatisfiedBy($version),
43 | Op::LESS_THAN_OR_EQUAL, Op::LESS_THAN_OR_EQUAL2 => $this->end->isSatisfiedBy($version),
44 | Op::GREATER_THAN => $this->start->isSatisfiedBy($version) && !$this->end->isSatisfiedBy($version),
45 | Op::GREATER_THAN_OR_EQUAL, Op::GREATER_THAN_OR_EQUAL2 => $this->start->isSatisfiedBy($version),
46 | default => throw new SemverException(sprintf('Invalid operator in range %s', $this)),
47 | };
48 | }
49 |
50 | /**
51 | * @throws SemverException
52 | */
53 | public function opposite(): string
54 | {
55 | return match ($this->operator) {
56 | Op::EQUAL => $this->toStringByOp(Op::NOT_EQUAL),
57 | Op::NOT_EQUAL => $this->toStringByOp(Op::EQUAL),
58 | Op::LESS_THAN => $this->toStringByOp(Op::GREATER_THAN_OR_EQUAL),
59 | Op::LESS_THAN_OR_EQUAL, Op::LESS_THAN_OR_EQUAL2 => $this->toStringByOp(Op::GREATER_THAN),
60 | Op::GREATER_THAN => $this->toStringByOp(Op::LESS_THAN_OR_EQUAL),
61 | Op::GREATER_THAN_OR_EQUAL, Op::GREATER_THAN_OR_EQUAL2 => $this->toStringByOp(Op::LESS_THAN),
62 | default => throw new SemverException(sprintf('Invalid operator in range %s', $this)),
63 | };
64 | }
65 |
66 | private function toStringByOp(string $op): string
67 | {
68 | return match ($op) {
69 | Op::EQUAL => sprintf('%s %s', (string) $this->start, (string) $this->end),
70 | Op::NOT_EQUAL => sprintf('%s || %s', $this->start->opposite(), $this->end->opposite()),
71 | Op::LESS_THAN => $this->start->opposite(),
72 | Op::LESS_THAN_OR_EQUAL, Op::LESS_THAN_OR_EQUAL2 => (string) $this->end,
73 | Op::GREATER_THAN => $this->end->opposite(),
74 | Op::GREATER_THAN_OR_EQUAL, Op::GREATER_THAN_OR_EQUAL2 => (string) $this->start,
75 | default => '',
76 | };
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/Constraints/VersionComparator.php:
--------------------------------------------------------------------------------
1 | major = $major;
32 | $this->minor = $minor;
33 | $this->patch = $patch;
34 | $this->preRelease = $preRelease;
35 | $this->buildMeta = $buildMeta;
36 |
37 | $this->isMajorWildcard = Patterns::isWildcard($major);
38 | $this->isMinorWildCard = is_null($minor) || Patterns::isWildcard($minor);
39 | $this->isPatchWildCard = is_null($patch) || Patterns::isWildcard($patch);
40 | $this->isWildCard = $this->isMajorWildcard || $this->isMinorWildCard || $this->isPatchWildCard;
41 | }
42 |
43 | /**
44 | * @return string the string representation of the descriptor
45 | */
46 | public function __toString(): string
47 | {
48 | $result = $this->major;
49 | $result .= isset($this->minor) ? '.'.$this->minor : '';
50 | $result .= isset($this->patch) ? '.'.$this->patch : '';
51 | $result .= isset($this->preRelease) ? '-'.$this->preRelease : '';
52 | $result .= isset($this->buildMeta) ? '+'.$this->buildMeta : '';
53 |
54 | return $result;
55 | }
56 |
57 | /**
58 | * @throws SemverException
59 | */
60 | public function getIntMajor(): int
61 | {
62 | self::ensure(is_numeric($this->major), sprintf('Invalid MAJOR number in: %s', (string) $this));
63 |
64 | return intval($this->major);
65 | }
66 |
67 | /**
68 | * @throws SemverException
69 | */
70 | public function getIntMinor(): int
71 | {
72 | self::ensure(is_numeric($this->minor), sprintf('Invalid MINOR number in: %s', (string) $this));
73 |
74 | return intval($this->minor);
75 | }
76 |
77 | /**
78 | * @throws SemverException
79 | */
80 | public function getIntPatch(): int
81 | {
82 | self::ensure(is_numeric($this->patch), sprintf('Invalid PATCH number in: %s', (string) $this));
83 |
84 | return intval($this->patch);
85 | }
86 |
87 | /**
88 | * @throws SemverException
89 | */
90 | public function fromOperator(string $operator): VersionComparator
91 | {
92 | if (in_array($operator, Patterns::COMPARISON_OPERATORS, true) || '' === $operator) {
93 | return $this->toComparator($operator);
94 | }
95 |
96 | if (in_array($operator, Patterns::TILDE_OPERATORS, true)) {
97 | return $this->fromTilde();
98 | }
99 | if (Patterns::CARET_OPERATOR === $operator) {
100 | return $this->fromCaret();
101 | }
102 |
103 | throw new SemverException(sprintf('Invalid constraint operator: %s in %s', $operator, (string) $this));
104 | }
105 |
106 | /**
107 | * @throws SemverException
108 | */
109 | public function toComparator(string $operator = Op::EQUAL): VersionComparator
110 | {
111 | if ($this->isMajorWildcard) {
112 | switch ($operator) {
113 | case Op::GREATER_THAN:
114 | case Op::LESS_THAN:
115 | case Op::NOT_EQUAL:
116 | return new Condition(
117 | Op::LESS_THAN,
118 | Version::minVersion()->copy(null, null, null, '')
119 | );
120 |
121 | default:
122 | return Condition::greaterThanMin();
123 | }
124 | } elseif ($this->isMinorWildCard) {
125 | $version = Version::create($this->getIntMajor(), 0, 0, $this->preRelease, $this->buildMeta);
126 |
127 | return new Range(
128 | new Condition(Op::GREATER_THAN_OR_EQUAL, $version),
129 | new Condition(Op::LESS_THAN, $version->getNextMajorVersion('')),
130 | $operator
131 | );
132 | } elseif ($this->isPatchWildCard) {
133 | $version = Version::create(
134 | $this->getIntMajor(),
135 | $this->getIntMinor(),
136 | 0,
137 | $this->preRelease,
138 | $this->buildMeta
139 | );
140 |
141 | return new Range(
142 | new Condition(Op::GREATER_THAN_OR_EQUAL, $version),
143 | new Condition(Op::LESS_THAN, $version->getNextMinorVersion('')),
144 | $operator
145 | );
146 | } else {
147 | return new Condition($operator, Version::create(
148 | $this->getIntMajor(),
149 | $this->getIntMinor(),
150 | $this->getIntPatch(),
151 | $this->preRelease,
152 | $this->buildMeta
153 | ));
154 | }
155 | }
156 |
157 | /**
158 | * @throws SemverException
159 | */
160 | private function fromTilde(): VersionComparator
161 | {
162 | if ($this->isWildCard) {
163 | return $this->toComparator();
164 | }
165 | $version = Version::create(
166 | $this->getIntMajor(),
167 | $this->getIntMinor(),
168 | $this->getIntPatch(),
169 | $this->preRelease,
170 | $this->buildMeta
171 | );
172 |
173 | return new Range(
174 | new Condition(Op::GREATER_THAN_OR_EQUAL, $version),
175 | new Condition(Op::LESS_THAN, $version->getNextMinorVersion('')),
176 | Op::EQUAL
177 | );
178 | }
179 |
180 | /**
181 | * @throws SemverException
182 | */
183 | private function fromCaret(): VersionComparator
184 | {
185 | if ($this->isMajorWildcard) {
186 | return Condition::greaterThanMin();
187 | }
188 | if ($this->isMinorWildCard) {
189 | return $this->fromMinorCaret();
190 | }
191 | if ($this->isPatchWildCard) {
192 | return $this->fromPatchCaret();
193 | }
194 |
195 | $version = Version::create(
196 | $this->getIntMajor(),
197 | $this->getIntMinor(),
198 | $this->getIntPatch(),
199 | $this->preRelease,
200 | $this->buildMeta
201 | );
202 |
203 | $endVersion = Version::create(0, 0, 1, '');
204 | if ('0' !== $this->major) {
205 | $endVersion = $version->getNextMajorVersion('');
206 | } elseif ('0' !== $this->minor) {
207 | $endVersion = $version->getNextMinorVersion('');
208 | } elseif ('0' !== $this->patch) {
209 | $endVersion = $version->getNextPatchVersion('');
210 | }
211 |
212 | return new Range(
213 | new Condition(Op::GREATER_THAN_OR_EQUAL, $version),
214 | new Condition(Op::LESS_THAN, $endVersion),
215 | Op::EQUAL
216 | );
217 | }
218 |
219 | /**
220 | * @throws SemverException
221 | */
222 | private function fromMinorCaret(): VersionComparator
223 | {
224 | if ('0' === $this->major) {
225 | return new Range(
226 | Condition::greaterThanMin(),
227 | new Condition(Op::LESS_THAN, Version::create(1, 0, 0, '')),
228 | Op::EQUAL
229 | );
230 | }
231 |
232 | return $this->toComparator();
233 | }
234 |
235 | /**
236 | * @throws SemverException
237 | */
238 | private function fromPatchCaret(): VersionComparator
239 | {
240 | if ('0' === $this->major && '0' === $this->minor) {
241 | return new Range(
242 | Condition::greaterThanMin(),
243 | new Condition(Op::LESS_THAN, Version::create(0, 1, 0, '')),
244 | Op::EQUAL
245 | );
246 | }
247 |
248 | if ('0' !== $this->major) {
249 | $version = Version::create($this->getIntMajor(), $this->getIntMinor(), 0);
250 |
251 | return new Range(
252 | new Condition(Op::GREATER_THAN_OR_EQUAL, $version),
253 | new Condition(Op::LESS_THAN, $version->getNextMajorVersion('')),
254 | Op::EQUAL
255 | );
256 | }
257 |
258 | return $this->toComparator();
259 | }
260 | }
261 |
--------------------------------------------------------------------------------
/src/Inc.php:
--------------------------------------------------------------------------------
1 | ..)
25 | const CORE_VERSION = '('.self::NUMERIC.')\.('.self::NUMERIC.')\.('.self::NUMERIC.')';
26 |
27 | // Dot-separated loose numeric identifier pattern. ((.)?(.)?)
28 | const LOOSE_CORE_VERSION = '('.self::NUMERIC.')(?:\.('.self::NUMERIC.'))?(?:\.('.self::NUMERIC.'))?';
29 |
30 | // Numeric or non-numeric pre-release part pattern.
31 | const PRE_RELEASE_PART = '(?:'.self::NUMERIC.'|'.self::NON_NUMERIC.')';
32 |
33 | // Pre-release identifier pattern. A hyphen followed by dot-separated
34 | // numeric or non-numeric pre-release parts.
35 | const PRE_RELEASE = '(?:-('.self::PRE_RELEASE_PART.'(?:\.'.self::PRE_RELEASE_PART.')*))';
36 |
37 | // Build-metadata identifier pattern. A + sign followed by dot-separated
38 | // alphanumeric build-metadata parts.
39 | const BUILD = '(?:\+('.self::ALPHANUMERIC_OR_HYPHEN.'+(?:\.'.self::ALPHANUMERIC_OR_HYPHEN.'+)*))';
40 |
41 | // List of allowed operations in a condition.
42 | const ALLOWED_OPERATORS = '||=|!=|<|<=|=<|>|>=|=>|\^|~>|~';
43 |
44 | // Numeric identifier pattern for parsing conditions.
45 | const X_RANGE_NUMERIC = self::NUMERIC.'|x|X|\*';
46 |
47 | // X-RANGE version: 1.x | 1.2.* | 1.1.X
48 | // phpcs:ignore
49 | const X_RANGE_VERSION = '('.self::X_RANGE_NUMERIC.')(?:\.('.self::X_RANGE_NUMERIC.')(?:\.('.self::X_RANGE_NUMERIC.')(?:'.self::PRE_RELEASE.')?'.self::BUILD.'?)?)?';
50 |
51 | // Pattern that only matches numbers.
52 | const ONLY_NUMBER_REGEX = '/^[0-9]+$/';
53 |
54 | // Pattern that only matches alphanumeric or hyphen characters.
55 | const ONLY_ALPHANUMERIC_OR_HYPHEN_REGEX = '/^'.self::ALPHANUMERIC_OR_HYPHEN.'+$/';
56 |
57 | // Version parsing pattern: 1.2.3-alpha+build
58 | const VERSION_REGEX = '/^'.self::CORE_VERSION.self::PRE_RELEASE.'?'.self::BUILD.'?$/';
59 |
60 | // Prefixed version parsing pattern: v1.2-alpha+build
61 | const LOOSE_VERSION_REGEX = '/^v?'.self::LOOSE_CORE_VERSION.self::PRE_RELEASE.'?'.self::BUILD.'?$/';
62 |
63 | // Operator condition: >=1.2.*
64 | const OPERATOR_CONDITION = '('.self::ALLOWED_OPERATORS.')\s*v?(?:'.self::X_RANGE_VERSION.')';
65 |
66 | // Operator condition: >=1.2.*
67 | const OPERATOR_CONDITION_REGEX = '/'.self::OPERATOR_CONDITION.'/';
68 |
69 | // Operator condition: >=1.2.*
70 | const VALID_OPERATOR_CONDITION_REGEX = '/^(\s*'.self::OPERATOR_CONDITION.'\s*?)+$/';
71 |
72 | // Hyphen range condition: 1.2.* - 2.0.0
73 | // phpcs:ignore
74 | const HYPHEN_CONDITION_REGEX = '/\s*v?(?:'.self::X_RANGE_VERSION.')\s+-\s+v?(?:'.self::X_RANGE_VERSION.')\s*/';
75 |
76 | // Wildcard characters.
77 | const WILDCARDS = ['*', 'x', 'X'];
78 |
79 | // Operators.
80 | const COMPARISON_OPERATORS = ['=', '!=', '>', '>=', '=>', '<', '<=', '=<'];
81 | const TILDE_OPERATORS = ['~>', '~'];
82 | const CARET_OPERATOR = '^';
83 |
84 | /**
85 | * Determines whether a string is a wildcard or not.
86 | *
87 | * @param string $text the string to check
88 | *
89 | * @return bool true when the string is wildcard, otherwise false
90 | */
91 | public static function isWildcard(string $text): bool
92 | {
93 | return in_array($text, self::WILDCARDS, true);
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/PreRelease.php:
--------------------------------------------------------------------------------
1 | preReleaseParts = $preReleaseParts;
31 | }
32 |
33 | /**
34 | * @return string the string representation of the pre-release
35 | */
36 | public function __toString()
37 | {
38 | return implode('.', $this->preReleaseParts);
39 | }
40 |
41 | /**
42 | * @return string the identity of the pre-release tag
43 | */
44 | public function identity(): string
45 | {
46 | return $this->preReleaseParts[0];
47 | }
48 |
49 | /**
50 | * @return PreRelease the incremented pre-release
51 | */
52 | public function increment(): PreRelease
53 | {
54 | $result = $this->copy();
55 | $lastNumericIndex = -1;
56 | foreach ($result->preReleaseParts as $key => $part) {
57 | if (is_numeric($part)) {
58 | $lastNumericIndex = $key;
59 | }
60 | }
61 |
62 | if (-1 != $lastNumericIndex) {
63 | $result->preReleaseParts[$lastNumericIndex]
64 | = (string) (intval($result->preReleaseParts[$lastNumericIndex]) + 1);
65 | } else {
66 | $result->preReleaseParts[] = '0';
67 | }
68 |
69 | return $result;
70 | }
71 |
72 | /**
73 | * The default pre-release tag (-0).
74 | *
75 | * @return PreRelease the default pre-release tag
76 | */
77 | public static function default(): PreRelease
78 | {
79 | return self::single('default-pre-release', function () {
80 | return new PreRelease(['0']);
81 | });
82 | }
83 |
84 | /**
85 | * @param string $preReleaseString the pre-release string
86 | *
87 | * @return PreRelease the parsed pre-release part
88 | *
89 | * @throws SemverException when the given pre-release string is invalid
90 | */
91 | public static function parse(string $preReleaseString): PreRelease
92 | {
93 | $preReleaseString = trim($preReleaseString);
94 | if ('' === $preReleaseString) {
95 | return self::default();
96 | }
97 |
98 | $preRelease = new PreRelease(explode('.', $preReleaseString));
99 | $preRelease->validate();
100 |
101 | return $preRelease;
102 | }
103 |
104 | /**
105 | * @param PreRelease $p1 the left side of the comparison
106 | * @param PreRelease $p2 the right side of the comparison
107 | *
108 | * @return int -1 when $p1 < $p2, 0 when $p1 == $p2, 1 when $p1 > $p2
109 | */
110 | public static function compare(PreRelease $p1, PreRelease $p2): int
111 | {
112 | $v1Size = count($p1->preReleaseParts);
113 | $v2Size = count($p2->preReleaseParts);
114 |
115 | $count = min($v1Size, $v2Size);
116 |
117 | for ($i = 0; $i < $count; ++$i) {
118 | $part = self::comparePart($p1->preReleaseParts[$i], $p2->preReleaseParts[$i]);
119 | if (0 != $part) {
120 | return $part;
121 | }
122 | }
123 |
124 | return self::comparePrimitive($v1Size, $v2Size);
125 | }
126 |
127 | /**
128 | * @return PreRelease the copied pre-release
129 | */
130 | private function copy(): PreRelease
131 | {
132 | return new PreRelease($this->preReleaseParts);
133 | }
134 |
135 | /**
136 | * @throws SemverException when the any part of the tag is invalid
137 | */
138 | private function validate(): void
139 | {
140 | foreach ($this->preReleaseParts as $part) {
141 | if (preg_match(Patterns::ONLY_NUMBER_REGEX, $part) && strlen($part) > 1 && '0' === $part[0]) {
142 | throw new SemverException(sprintf(
143 | "The pre-release part '%s' is numeric but contains a leading zero.",
144 | $part
145 | ));
146 | }
147 |
148 | self::ensure((bool) preg_match(Patterns::ONLY_ALPHANUMERIC_OR_HYPHEN_REGEX, $part), sprintf(
149 | "The pre-release part '%s' contains invalid character.",
150 | $part
151 | ));
152 | }
153 | }
154 |
155 | /**
156 | * @param int|string $a the left side of the comparison
157 | * @param int|string $b the right side of the comparison
158 | *
159 | * @return int -1 when $a < $b, 0 when $a == $b, 1 when $v1 > $b
160 | */
161 | private static function comparePart($a, $b): int
162 | {
163 | if (is_numeric($a) && !is_numeric($b)) {
164 | return -1;
165 | }
166 |
167 | if (!is_numeric($a) && is_numeric($b)) {
168 | return 1;
169 | }
170 |
171 | return is_numeric($a) && is_numeric($b)
172 | ? self::comparePrimitive(intval($a), intval($b))
173 | : self::comparePrimitive($a, $b);
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/src/SemverException.php:
--------------------------------------------------------------------------------
1 | $v, otherwise false
48 | */
49 | public function isGreaterThan(Version $v): bool
50 | {
51 | return self::compare($this, $v) > 0;
52 | }
53 |
54 | /**
55 | * Compares the version with the given one, returns true when the current is greater than the other or equal.
56 | *
57 | * @param Version $v the version to compare
58 | *
59 | * @return bool true when instance >= $v, otherwise false
60 | */
61 | public function isGreaterThanOrEqual(Version $v): bool
62 | {
63 | return self::compare($this, $v) >= 0;
64 | }
65 |
66 | /**
67 | * Compares the version with the given one, returns true when they are equal.
68 | *
69 | * @param Version $v the version to compare
70 | *
71 | * @return bool true when instance == $v, otherwise false
72 | */
73 | public function isEqual(Version $v): bool
74 | {
75 | return 0 === self::compare($this, $v);
76 | }
77 |
78 | /**
79 | * Compares the version with the given one, returns true when they are not equal.
80 | *
81 | * @param Version $v the version to compare
82 | *
83 | * @return bool true when instance != $v, otherwise false
84 | */
85 | public function isNotEqual(Version $v): bool
86 | {
87 | return 0 !== self::compare($this, $v);
88 | }
89 |
90 | /**
91 | * Compares two version strings and returns true when the first is less than the second.
92 | *
93 | * @param string $v1 the left side of the comparison
94 | * @param string $v2 the right side of the comparison
95 | *
96 | * @return bool true when $v1 < $v2, otherwise false
97 | *
98 | * @throws SemverException when the version strings are invalid
99 | */
100 | public static function lessThan(string $v1, string $v2): bool
101 | {
102 | $version1 = self::parse($v1);
103 | $version2 = self::parse($v2);
104 |
105 | return $version1->isLessThan($version2);
106 | }
107 |
108 | /**
109 | * Compares two version strings and returns true when the first is less than the second or equal.
110 | *
111 | * @param string $v1 the left side of the comparison
112 | * @param string $v2 the right side of the comparison
113 | *
114 | * @return bool true when $v1 <= $v2, otherwise false
115 | *
116 | * @throws SemverException when the version strings are invalid
117 | */
118 | public static function lessThanOrEqual(string $v1, string $v2): bool
119 | {
120 | $version1 = self::parse($v1);
121 | $version2 = self::parse($v2);
122 |
123 | return $version1->isLessThanOrEqual($version2);
124 | }
125 |
126 | /**
127 | * Compares two version strings and returns true when the first is greater than the second.
128 | *
129 | * @param string $v1 the left side of the comparison
130 | * @param string $v2 the right side of the comparison
131 | *
132 | * @return bool true when $v1 > $v2, otherwise false
133 | *
134 | * @throws SemverException when the version strings are invalid
135 | */
136 | public static function greaterThan(string $v1, string $v2): bool
137 | {
138 | $version1 = self::parse($v1);
139 | $version2 = self::parse($v2);
140 |
141 | return $version1->isGreaterThan($version2);
142 | }
143 |
144 | /**
145 | * Compares two version strings and returns true when the first is greater than the second or equal.
146 | *
147 | * @param string $v1 the left side of the comparison
148 | * @param string $v2 the right side of the comparison
149 | *
150 | * @return bool true when $v1 >= $v2, otherwise false
151 | *
152 | * @throws SemverException when the version strings are invalid
153 | */
154 | public static function greaterThanOrEqual(string $v1, string $v2): bool
155 | {
156 | $version1 = self::parse($v1);
157 | $version2 = self::parse($v2);
158 |
159 | return $version1->isGreaterThanOrEqual($version2);
160 | }
161 |
162 | /**
163 | * Compares two version strings and returns true when the first and second are equal.
164 | *
165 | * @param string $v1 the left side of the comparison
166 | * @param string $v2 the right side of the comparison
167 | *
168 | * @return bool true when $v1 == $v2, otherwise false
169 | *
170 | * @throws SemverException when the version strings are invalid
171 | */
172 | public static function equal(string $v1, string $v2): bool
173 | {
174 | $version1 = self::parse($v1);
175 | $version2 = self::parse($v2);
176 |
177 | return $version1->isEqual($version2);
178 | }
179 |
180 | /**
181 | * Compares two version strings and returns true when the first and second are not equal.
182 | *
183 | * @param string $v1 the left side of the comparison
184 | * @param string $v2 the right side of the comparison
185 | *
186 | * @return bool true when $v1 != $v2, otherwise false
187 | *
188 | * @throws SemverException when the version strings are invalid
189 | */
190 | public static function notEqual(string $v1, string $v2): bool
191 | {
192 | $version1 = self::parse($v1);
193 | $version2 = self::parse($v2);
194 |
195 | return $version1->isNotEqual($version2);
196 | }
197 |
198 | /**
199 | * Compares two version strings.
200 | *
201 | * @param string $v1 the left side of the comparison
202 | * @param string $v2 the right side of the comparison
203 | *
204 | * @return int -1 when $v1 < $v2, 0 when $v1 == $v2, 1 when $v1 > $v2
205 | *
206 | * @throws SemverException when the version strings are invalid
207 | */
208 | public static function compareString(string $v1, string $v2): int
209 | {
210 | $version1 = self::parse($v1);
211 | $version2 = self::parse($v2);
212 |
213 | return self::compare($version1, $version2);
214 | }
215 |
216 | /**
217 | * Compares two versions.
218 | *
219 | * @param Version $v1 the left side of the comparison
220 | * @param Version $v2 the right side of the comparison
221 | *
222 | * @return int -1 when $v1 < $v2, 0 when $v1 == $v2, 1 when $v1 > $v2
223 | */
224 | public static function compare(Version $v1, Version $v2): int
225 | {
226 | $major = self::comparePrimitive($v1->getMajor(), $v2->getMajor());
227 | if (0 != $major) {
228 | return $major;
229 | }
230 |
231 | $minor = self::comparePrimitive($v1->getMinor(), $v2->getMinor());
232 | if (0 != $minor) {
233 | return $minor;
234 | }
235 |
236 | $patch = self::comparePrimitive($v1->getPatch(), $v2->getPatch());
237 | if (0 != $patch) {
238 | return $patch;
239 | }
240 |
241 | return self::compareByPreRelease($v1, $v2);
242 | }
243 |
244 | /**
245 | * @param Version $v1 the left side of the comparison
246 | * @param Version $v2 the right side of the comparison
247 | *
248 | * @return int -1 when $v1 < $v2, 0 when $v1 == $v2, 1 when $v1 > $v2
249 | */
250 | private static function compareByPreRelease(Version $v1, Version $v2): int
251 | {
252 | if ($v1->isPreRelease() && !$v2->isPreRelease()) {
253 | return -1;
254 | }
255 |
256 | if (!$v1->isPreRelease() && $v2->isPreRelease()) {
257 | return 1;
258 | }
259 |
260 | if (!is_null($v1->preRelease) && !is_null($v2->preRelease)) {
261 | return PreRelease::compare($v1->preRelease, $v2->preRelease);
262 | }
263 |
264 | return 0;
265 | }
266 | }
267 |
--------------------------------------------------------------------------------
/src/Traits/Copyable.php:
--------------------------------------------------------------------------------
1 | major : $major,
37 | null == $minor ? $this->minor : $minor,
38 | null == $patch ? $this->patch : $patch,
39 | null === $preRelease
40 | ? null === $this->preRelease
41 | ? null
42 | : (string) $this->preRelease
43 | : $preRelease,
44 | null === $buildMeta ? $this->buildMeta : $buildMeta
45 | );
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/Traits/Iterator.php:
--------------------------------------------------------------------------------
1 | major + 1,
30 | 0,
31 | 0,
32 | !is_null($preRelease) ? PreRelease::parse($preRelease) : null
33 | );
34 | }
35 |
36 | /**
37 | * Produces the next minor version.
38 | *
39 | * @param null|string $preRelease the pre-release part
40 | *
41 | * @return Version the next minor version
42 | *
43 | * @throws SemverException when the pre-release tag is non-null and invalid
44 | */
45 | public function getNextMinorVersion(?string $preRelease = null): Version
46 | {
47 | return new Version(
48 | $this->major,
49 | $this->minor + 1,
50 | 0,
51 | !is_null($preRelease) ? PreRelease::parse($preRelease) : null
52 | );
53 | }
54 |
55 | /**
56 | * Produces the next patch version.
57 | *
58 | * @param null|string $preRelease the pre-release part
59 | *
60 | * @return Version the next patch version
61 | *
62 | * @throws SemverException when the pre-release tag is non-null and invalid
63 | */
64 | public function getNextPatchVersion(?string $preRelease = null): Version
65 | {
66 | return new Version(
67 | $this->major,
68 | $this->minor,
69 | !$this->isPreRelease() || !is_null($preRelease) ? $this->patch + 1 : $this->patch,
70 | !is_null($preRelease) ? PreRelease::parse($preRelease) : null
71 | );
72 | }
73 |
74 | /**
75 | * Produces the next pre-release version.
76 | *
77 | * @param null|string $preRelease the pre-release part
78 | *
79 | * @return Version the next pre-release version
80 | *
81 | * @throws SemverException when the pre-release tag is non-null and invalid
82 | */
83 | public function getNextPreReleaseVersion(?string $preRelease = null): Version
84 | {
85 | $pre = PreRelease::default();
86 | if (!empty($preRelease)) {
87 | $pre = null != $this->preRelease && $this->preRelease->identity() === $preRelease
88 | ? $this->preRelease->increment()
89 | : PreRelease::parse($preRelease);
90 | } elseif (null != $this->preRelease) {
91 | $pre = $this->preRelease->increment();
92 | }
93 |
94 | return new Version(
95 | $this->major,
96 | $this->minor,
97 | $this->isPreRelease() ? $this->patch : $this->patch + 1,
98 | $pre
99 | );
100 | }
101 |
102 | /**
103 | * Increases the version by its Inc::MAJOR, Inc::MINOR, Inc::PATCH, or Inc::PRE_RELEASE segment.
104 | * Returns a new version while the original remains unchanged.
105 | *
106 | * @param int $by determines by which part the Version should be incremented
107 | * @param null|string $preRelease the optional pre-release part
108 | *
109 | * @return Version the incremented version
110 | *
111 | * @throws SemverException when the pre-release tag is non-null and invalid
112 | */
113 | public function inc(int $by, ?string $preRelease = null): Version
114 | {
115 | return match ($by) {
116 | Inc::MAJOR => $this->getNextMajorVersion($preRelease),
117 | Inc::MINOR => $this->getNextMinorVersion($preRelease),
118 | Inc::PATCH => $this->getNextPatchVersion($preRelease),
119 | Inc::PRE_RELEASE => $this->getNextPreReleaseVersion($preRelease),
120 | default => throw new SemverException('Invalid `by` argument in inc() method'),
121 | };
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/src/Traits/PrimitiveComparable.php:
--------------------------------------------------------------------------------
1 | $b
17 | */
18 | private static function comparePrimitive(int|string $a, int|string $b): int
19 | {
20 | if ($a != $b) {
21 | return $a < $b ? -1 : 1;
22 | }
23 |
24 | return 0;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Traits/Singles.php:
--------------------------------------------------------------------------------
1 | */
13 | private static array $singles = [];
14 |
15 | /**
16 | * This method gets a single instance for a particular key.
17 | *
18 | * @param string $key the key for the instance
19 | * @param callable $factory the factory function used when an instance doesn't exist for the key
20 | *
21 | * @return mixed the instance
22 | */
23 | private static function single(string $key, callable $factory): mixed
24 | {
25 | if (!isset(self::$singles[$key])) {
26 | self::$singles[$key] = $factory();
27 | }
28 |
29 | return self::$singles[$key];
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Traits/Sortable.php:
--------------------------------------------------------------------------------
1 | major = $major;
52 | $this->minor = $minor;
53 | $this->patch = $patch;
54 | $this->preRelease = $preRelease;
55 | $this->buildMeta = $buildMeta;
56 | }
57 |
58 | /**
59 | * @return string the string representation of the version
60 | */
61 | public function __toString(): string
62 | {
63 | $result = implode('.', [$this->major, $this->minor, $this->patch]);
64 | $result .= isset($this->preRelease) ? '-'.$this->preRelease : '';
65 | $result .= isset($this->buildMeta) ? '+'.$this->buildMeta : '';
66 |
67 | return $result;
68 | }
69 |
70 | /**
71 | * Returns the major version number.
72 | *
73 | * @return int the major version number
74 | */
75 | public function getMajor(): int
76 | {
77 | return $this->major;
78 | }
79 |
80 | /**
81 | * Returns the minor version number.
82 | *
83 | * @return int the minor version number
84 | */
85 | public function getMinor(): int
86 | {
87 | return $this->minor;
88 | }
89 |
90 | /**
91 | * Returns the patch version number.
92 | *
93 | * @return int the patch version number
94 | */
95 | public function getPatch(): int
96 | {
97 | return $this->patch;
98 | }
99 |
100 | /**
101 | * Returns the pre-release tag.
102 | *
103 | * @return null|string the pre-release part
104 | */
105 | public function getPreRelease(): ?string
106 | {
107 | return null != $this->preRelease ? (string) $this->preRelease : null;
108 | }
109 |
110 | /**
111 | * Returns the build metadata.
112 | *
113 | * @return null|string the build metadata part
114 | */
115 | public function getBuildMeta(): ?string
116 | {
117 | return $this->buildMeta;
118 | }
119 |
120 | /**
121 | * Returns true when the version has a pre-release tag.
122 | *
123 | * @return bool true when the version is a pre-release version
124 | */
125 | public function isPreRelease(): bool
126 | {
127 | return null != $this->preRelease;
128 | }
129 |
130 | /**
131 | * Determines whether the version is considered stable or not.
132 | * Stable versions have a positive major number and no pre-release identifier.
133 | *
134 | * @return bool true when the version is a stable version
135 | */
136 | public function isStable(): bool
137 | {
138 | return $this->major > 0 && !$this->isPreRelease();
139 | }
140 |
141 | /**
142 | * Produces a copy of the Version without the PRE-RELEASE and BUILD METADATA identities.
143 | *
144 | * @return Version the new version
145 | */
146 | public function withoutSuffixes(): Version
147 | {
148 | return new Version($this->major, $this->minor, $this->patch);
149 | }
150 |
151 | /**
152 | * Determines whether the version satisfies the given Constraint or not.
153 | *
154 | * @param Constraint $constraint the constraint to satisfy
155 | *
156 | * @return bool true when the constraint is satisfied, otherwise false
157 | */
158 | public function isSatisfying(Constraint $constraint): bool
159 | {
160 | return $constraint->isSatisfiedBy($this);
161 | }
162 |
163 | /**
164 | * @return Version The 0.0.0 semantic version.
165 | */
166 | public static function minVersion(): Version
167 | {
168 | return self::single('min', function () {
169 | return new Version(0, 0, 0);
170 | });
171 | }
172 |
173 | /**
174 | * Parses the given string as a Version and returns the result or null
175 | * if the string is not a valid representation of a semantic version.
176 | *
177 | * Strict mode is on by default, which means partial versions (e.g. '1.0' or '1') and versions with 'v' prefix
178 | * are considered invalid. This behaviour can be turned off by setting the strict parameter to false.
179 | *
180 | * @param string $versionString the version string
181 | * @param bool $strict enables or disables strict parsing
182 | *
183 | * @return null|Version the parsed version, or null if the parse fails
184 | */
185 | public static function parseOrNull(string $versionString, bool $strict = true): ?Version
186 | {
187 | try {
188 | return self::parse($versionString, $strict);
189 | } catch (Exception) {
190 | return null;
191 | }
192 | }
193 |
194 | /**
195 | * Parses the given string as a Version and returns the result or throws a SemverException
196 | * if the string is not a valid representation of a semantic version.
197 | *
198 | * Strict mode is on by default, which means partial versions (e.g. '1.0' or '1') and versions with 'v' prefix
199 | * are considered invalid. This behaviour can be turned off by setting the strict parameter to false.
200 | *
201 | * @param string $versionString the version string
202 | * @param bool $strict enables or disables strict parsing
203 | *
204 | * @return Version the parsed version
205 | *
206 | * @throws SemverException when the given version string is invalid
207 | */
208 | public static function parse(string $versionString, bool $strict = true): Version
209 | {
210 | $versionString = trim($versionString);
211 | self::ensure('' !== $versionString, 'versionString cannot be empty.');
212 | self::ensure(
213 | (bool) preg_match(
214 | $strict ? Patterns::VERSION_REGEX : Patterns::LOOSE_VERSION_REGEX,
215 | $versionString,
216 | $matches
217 | ),
218 | sprintf('Invalid version: %s.', $versionString)
219 | );
220 |
221 | $matchedMajor = isset($matches[1]) && '' !== $matches[1];
222 | $matchedMinor = isset($matches[2]) && '' !== $matches[2];
223 | $matchedPatch = isset($matches[3]) && '' !== $matches[3];
224 |
225 | $preRelease = isset($matches[4]) && '' !== $matches[4]
226 | ? PreRelease::parse($matches[4])
227 | : null;
228 | $buildMeta = isset($matches[5]) && '' !== $matches[5]
229 | ? $matches[5]
230 | : null;
231 |
232 | if ($strict && $matchedMajor && $matchedMinor && $matchedPatch) {
233 | return new Version(
234 | intval($matches[1] ?? 0),
235 | intval($matches[2] ?? 0),
236 | intval($matches[3] ?? 0),
237 | $preRelease,
238 | $buildMeta
239 | );
240 | }
241 | if (!$strict && $matchedMajor) {
242 | return new Version(
243 | intval($matches[1] ?? 0),
244 | $matchedMinor ? intval($matches[2] ?? 0) : 0,
245 | $matchedPatch ? intval($matches[3] ?? 0) : 0,
246 | $preRelease,
247 | $buildMeta
248 | );
249 | }
250 |
251 | throw new SemverException(sprintf('Invalid version: %s.', $versionString));
252 | }
253 |
254 | /**
255 | * Constructs a semantic version from the given arguments following the pattern:
256 | * <[major]>.<[minor]>.<[patch]>-<[preRelease]>+<[buildMetadata]>.
257 | *
258 | * @param int $major the major version number
259 | * @param int $minor the minor version number
260 | * @param int $patch the patch version number
261 | * @param null|string $preRelease the pre-release part
262 | * @param null|string $buildMeta the build metadata
263 | *
264 | * @return Version the new version
265 | *
266 | * @throws SemverException when the version parts are invalid
267 | */
268 | public static function create(
269 | int $major,
270 | int $minor,
271 | int $patch,
272 | ?string $preRelease = null,
273 | ?string $buildMeta = null
274 | ): Version {
275 | self::ensure($major >= 0, 'The major number must be >= 0.');
276 | self::ensure($minor >= 0, 'The minor number must be >= 0.');
277 | self::ensure($patch >= 0, 'The patch number must be >= 0.');
278 |
279 | return new Version(
280 | $major,
281 | $minor,
282 | $patch,
283 | null !== $preRelease ? PreRelease::parse($preRelease) : null,
284 | $buildMeta
285 | );
286 | }
287 |
288 | /**
289 | * Determines whether a Version satisfies a Constraint or not.
290 | *
291 | * @param string $versionString the version to check
292 | * @param string $constraintString the constraint to satisfy
293 | *
294 | * @return bool true when the given version satisfies the given constraint, otherwise false
295 | *
296 | * @throws SemverException when the version string or the constraint string is invalid
297 | */
298 | public static function satisfies(string $versionString, string $constraintString): bool
299 | {
300 | $version = self::parse($versionString);
301 | $constraint = Constraint::parse($constraintString);
302 |
303 | return $version->isSatisfying($constraint);
304 | }
305 | }
306 |
--------------------------------------------------------------------------------