├── .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 | [![Build Status](https://github.com/z4kn4fein/php-semver/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/z4kn4fein/php-semver/actions/workflows/ci.yml) 3 | [![Latest Stable Version](https://poser.pugx.org/z4kn4fein/php-semver/version)](https://packagist.org/packages/z4kn4fein/php-semver) 4 | [![Total Downloads](https://poser.pugx.org/z4kn4fein/php-semver/downloads)](https://packagist.org/packages/z4kn4fein/php-semver) 5 | [![Sonar Quality Gate](https://img.shields.io/sonar/quality_gate/z4kn4fein_php-semver?logo=sonarcloud&server=https%3A%2F%2Fsonarcloud.io)](https://sonarcloud.io/project/overview?id=z4kn4fein_php-semver) 6 | [![Sonar Coverage](https://img.shields.io/sonar/coverage/z4kn4fein_php-semver?logo=SonarCloud&server=https%3A%2F%2Fsonarcloud.io)](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 | docs 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 | --------------------------------------------------------------------------------