diff --git a/composer.json b/composer.json index 5d12702f9..1c7964e7a 100644 --- a/composer.json +++ b/composer.json @@ -39,7 +39,7 @@ "pear/archive_tar": "^1.4.9", "pear/pear-core-minimal": "^1.10", "php-http/guzzle7-adapter": "^1.1.0", - "php-opencloud/openstack": "^3.12", + "php-opencloud/openstack": "^3.14", "phpseclib/phpseclib": "^2.0.45", "pimple/pimple": "^3.5.0", "psr/clock": "^1.0", diff --git a/composer.lock b/composer.lock index a7d14e39d..0b42d3ce7 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8ffdafc25d46a458b7b7b603b4afd0a5", + "content-hash": "699fdae2bb3e54eb8ea7d6aeede832ba", "packages": [ { "name": "aws/aws-crt-php", @@ -1570,30 +1570,40 @@ }, { "name": "justinrainbow/json-schema", - "version": "5.3.0", + "version": "6.4.2", "source": { "type": "git", "url": "https://github.com/jsonrainbow/json-schema.git", - "reference": "feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8" + "reference": "ce1fd2d47799bb60668643bc6220f6278a4c1d02" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8", - "reference": "feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/ce1fd2d47799bb60668643bc6220f6278a4c1d02", + "reference": "ce1fd2d47799bb60668643bc6220f6278a4c1d02", "shasum": "" }, "require": { - "php": ">=7.1" + "ext-json": "*", + "marc-mabe/php-enum": "^4.0", + "php": "^7.2 || ^8.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1", + "friendsofphp/php-cs-fixer": "3.3.0", "json-schema/json-schema-test-suite": "1.2.0", - "phpunit/phpunit": "^4.8.35" + "marc-mabe/php-enum-phpstan": "^2.0", + "phpspec/prophecy": "^1.19", + "phpstan/phpstan": "^1.12", + "phpunit/phpunit": "^8.5" }, "bin": [ "bin/validate-json" ], "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.x-dev" + } + }, "autoload": { "psr-4": { "JsonSchema\\": "src/JsonSchema/" @@ -1622,16 +1632,16 @@ } ], "description": "A library to validate a json schema.", - "homepage": "https://github.com/justinrainbow/json-schema", + "homepage": "https://github.com/jsonrainbow/json-schema", "keywords": [ "json", "schema" ], "support": { "issues": "https://github.com/jsonrainbow/json-schema/issues", - "source": "https://github.com/jsonrainbow/json-schema/tree/5.3.0" + "source": "https://github.com/jsonrainbow/json-schema/tree/6.4.2" }, - "time": "2024-07-06T21:00:26+00:00" + "time": "2025-06-03T18:27:04+00:00" }, { "name": "kornrunner/blurhash", @@ -1806,6 +1816,79 @@ ], "time": "2022-12-19T15:00:24+00:00" }, + { + "name": "marc-mabe/php-enum", + "version": "v4.7.1", + "source": { + "type": "git", + "url": "https://github.com/marc-mabe/php-enum.git", + "reference": "7159809e5cfa041dca28e61f7f7ae58063aae8ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/marc-mabe/php-enum/zipball/7159809e5cfa041dca28e61f7f7ae58063aae8ed", + "reference": "7159809e5cfa041dca28e61f7f7ae58063aae8ed", + "shasum": "" + }, + "require": { + "ext-reflection": "*", + "php": "^7.1 | ^8.0" + }, + "require-dev": { + "phpbench/phpbench": "^0.16.10 || ^1.0.4", + "phpstan/phpstan": "^1.3.1", + "phpunit/phpunit": "^7.5.20 | ^8.5.22 | ^9.5.11", + "vimeo/psalm": "^4.17.0 | ^5.26.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-3.x": "3.2-dev", + "dev-master": "4.7-dev" + } + }, + "autoload": { + "psr-4": { + "MabeEnum\\": "src/" + }, + "classmap": [ + "stubs/Stringable.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Marc Bennewitz", + "email": "dev@mabe.berlin", + "homepage": "https://mabe.berlin/", + "role": "Lead" + } + ], + "description": "Simple and fast implementation of enumerations with native PHP", + "homepage": "https://github.com/marc-mabe/php-enum", + "keywords": [ + "enum", + "enum-map", + "enum-set", + "enumeration", + "enumerator", + "enummap", + "enumset", + "map", + "set", + "type", + "type-hint", + "typehint" + ], + "support": { + "issues": "https://github.com/marc-mabe/php-enum/issues", + "source": "https://github.com/marc-mabe/php-enum/tree/v4.7.1" + }, + "time": "2024-11-28T04:54:44+00:00" + }, { "name": "masterminds/html5", "version": "2.9.0", @@ -2685,23 +2768,23 @@ }, { "name": "php-opencloud/openstack", - "version": "v3.12.0", + "version": "v3.14.0", "source": { "type": "git", "url": "https://github.com/php-opencloud/openstack.git", - "reference": "9886899de3a81e95dd6cb352253870fa02abd0f9" + "reference": "b92ea5581ca91779b88f08b1b44e6ca880b34fc3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-opencloud/openstack/zipball/9886899de3a81e95dd6cb352253870fa02abd0f9", - "reference": "9886899de3a81e95dd6cb352253870fa02abd0f9", + "url": "https://api.github.com/repos/php-opencloud/openstack/zipball/b92ea5581ca91779b88f08b1b44e6ca880b34fc3", + "reference": "b92ea5581ca91779b88f08b1b44e6ca880b34fc3", "shasum": "" }, "require": { "guzzlehttp/guzzle": "^7.0", "guzzlehttp/psr7": ">=1.7", "guzzlehttp/uri-template": "^0.2 || ^1.0", - "justinrainbow/json-schema": "^5.2", + "justinrainbow/json-schema": "^5.2 || ^6.0", "php": "^7.2.5 || ^8.0" }, "require-dev": { @@ -2756,9 +2839,9 @@ ], "support": { "issues": "https://github.com/php-opencloud/openstack/issues", - "source": "https://github.com/php-opencloud/openstack/tree/v3.12.0" + "source": "https://github.com/php-opencloud/openstack/tree/v3.14.0" }, - "time": "2025-01-07T07:11:23+00:00" + "time": "2025-05-30T09:26:42+00:00" }, { "name": "phpseclib/phpseclib", @@ -5248,16 +5331,16 @@ }, { "name": "symfony/polyfill-php80", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", - "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", "shasum": "" }, "require": { @@ -5308,7 +5391,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.32.0" }, "funding": [ { @@ -5324,7 +5407,7 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2025-01-02T08:10:11+00:00" }, { "name": "symfony/polyfill-php83", diff --git a/composer/autoload_classmap.php b/composer/autoload_classmap.php index 6544986a1..c197bb1ca 100644 --- a/composer/autoload_classmap.php +++ b/composer/autoload_classmap.php @@ -1839,8 +1839,10 @@ 'JmesPath\\TreeCompiler' => $vendorDir . '/mtdowling/jmespath.php/src/TreeCompiler.php', 'JmesPath\\TreeInterpreter' => $vendorDir . '/mtdowling/jmespath.php/src/TreeInterpreter.php', 'JmesPath\\Utils' => $vendorDir . '/mtdowling/jmespath.php/src/Utils.php', + 'JsonSchema\\ConstraintError' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/ConstraintError.php', 'JsonSchema\\Constraints\\BaseConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/BaseConstraint.php', 'JsonSchema\\Constraints\\CollectionConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/CollectionConstraint.php', + 'JsonSchema\\Constraints\\ConstConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/ConstConstraint.php', 'JsonSchema\\Constraints\\Constraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Constraint.php', 'JsonSchema\\Constraints\\ConstraintInterface' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/ConstraintInterface.php', 'JsonSchema\\Constraints\\EnumConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/EnumConstraint.php', @@ -1856,6 +1858,7 @@ 'JsonSchema\\Constraints\\TypeConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/TypeConstraint.php', 'JsonSchema\\Constraints\\UndefinedConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/UndefinedConstraint.php', 'JsonSchema\\Entity\\JsonPointer' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Entity/JsonPointer.php', + 'JsonSchema\\Enum' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Enum.php', 'JsonSchema\\Exception\\ExceptionInterface' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Exception/ExceptionInterface.php', 'JsonSchema\\Exception\\InvalidArgumentException' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Exception/InvalidArgumentException.php', 'JsonSchema\\Exception\\InvalidConfigException' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Exception/InvalidConfigException.php', @@ -1872,6 +1875,10 @@ 'JsonSchema\\Rfc3339' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Rfc3339.php', 'JsonSchema\\SchemaStorage' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/SchemaStorage.php', 'JsonSchema\\SchemaStorageInterface' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/SchemaStorageInterface.php', + 'JsonSchema\\Tool\\DeepComparer' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Tool/DeepComparer.php', + 'JsonSchema\\Tool\\DeepCopy' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Tool/DeepCopy.php', + 'JsonSchema\\Tool\\Validator\\RelativeReferenceValidator' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Tool/Validator/RelativeReferenceValidator.php', + 'JsonSchema\\Tool\\Validator\\UriValidator' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Tool/Validator/UriValidator.php', 'JsonSchema\\UriResolverInterface' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/UriResolverInterface.php', 'JsonSchema\\UriRetrieverInterface' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/UriRetrieverInterface.php', 'JsonSchema\\Uri\\Retrievers\\AbstractRetriever' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Uri/Retrievers/AbstractRetriever.php', @@ -1899,6 +1906,10 @@ 'Lcobucci\\Clock\\Clock' => $vendorDir . '/lcobucci/clock/src/Clock.php', 'Lcobucci\\Clock\\FrozenClock' => $vendorDir . '/lcobucci/clock/src/FrozenClock.php', 'Lcobucci\\Clock\\SystemClock' => $vendorDir . '/lcobucci/clock/src/SystemClock.php', + 'MabeEnum\\Enum' => $vendorDir . '/marc-mabe/php-enum/src/Enum.php', + 'MabeEnum\\EnumMap' => $vendorDir . '/marc-mabe/php-enum/src/EnumMap.php', + 'MabeEnum\\EnumSerializableTrait' => $vendorDir . '/marc-mabe/php-enum/src/EnumSerializableTrait.php', + 'MabeEnum\\EnumSet' => $vendorDir . '/marc-mabe/php-enum/src/EnumSet.php', 'Masterminds\\HTML5' => $vendorDir . '/masterminds/html5/src/HTML5.php', 'Masterminds\\HTML5\\Elements' => $vendorDir . '/masterminds/html5/src/HTML5/Elements.php', 'Masterminds\\HTML5\\Entities' => $vendorDir . '/masterminds/html5/src/HTML5/Entities.php', diff --git a/composer/autoload_psr4.php b/composer/autoload_psr4.php index 2d33f15f5..a091fdc40 100644 --- a/composer/autoload_psr4.php +++ b/composer/autoload_psr4.php @@ -61,6 +61,7 @@ 'MicrosoftAzure\\Storage\\Common\\' => array($vendorDir . '/microsoft/azure-storage-common/src/Common'), 'MicrosoftAzure\\Storage\\Blob\\' => array($vendorDir . '/microsoft/azure-storage-blob/src/Blob'), 'Masterminds\\' => array($vendorDir . '/masterminds/html5/src'), + 'MabeEnum\\' => array($vendorDir . '/marc-mabe/php-enum/src'), 'Lcobucci\\Clock\\' => array($vendorDir . '/lcobucci/clock/src'), 'Laravel\\SerializableClosure\\' => array($vendorDir . '/laravel/serializable-closure/src'), 'JsonSchema\\' => array($vendorDir . '/justinrainbow/json-schema/src/JsonSchema'), diff --git a/composer/autoload_static.php b/composer/autoload_static.php index 7d391303c..2e8d15339 100644 --- a/composer/autoload_static.php +++ b/composer/autoload_static.php @@ -129,6 +129,7 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'MicrosoftAzure\\Storage\\Common\\' => 30, 'MicrosoftAzure\\Storage\\Blob\\' => 28, 'Masterminds\\' => 12, + 'MabeEnum\\' => 9, ), 'L' => array ( @@ -411,6 +412,10 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 array ( 0 => __DIR__ . '/..' . '/masterminds/html5/src', ), + 'MabeEnum\\' => + array ( + 0 => __DIR__ . '/..' . '/marc-mabe/php-enum/src', + ), 'Lcobucci\\Clock\\' => array ( 0 => __DIR__ . '/..' . '/lcobucci/clock/src', @@ -2367,8 +2372,10 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'JmesPath\\TreeCompiler' => __DIR__ . '/..' . '/mtdowling/jmespath.php/src/TreeCompiler.php', 'JmesPath\\TreeInterpreter' => __DIR__ . '/..' . '/mtdowling/jmespath.php/src/TreeInterpreter.php', 'JmesPath\\Utils' => __DIR__ . '/..' . '/mtdowling/jmespath.php/src/Utils.php', + 'JsonSchema\\ConstraintError' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/ConstraintError.php', 'JsonSchema\\Constraints\\BaseConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/BaseConstraint.php', 'JsonSchema\\Constraints\\CollectionConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/CollectionConstraint.php', + 'JsonSchema\\Constraints\\ConstConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/ConstConstraint.php', 'JsonSchema\\Constraints\\Constraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Constraint.php', 'JsonSchema\\Constraints\\ConstraintInterface' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/ConstraintInterface.php', 'JsonSchema\\Constraints\\EnumConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/EnumConstraint.php', @@ -2384,6 +2391,7 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'JsonSchema\\Constraints\\TypeConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/TypeConstraint.php', 'JsonSchema\\Constraints\\UndefinedConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/UndefinedConstraint.php', 'JsonSchema\\Entity\\JsonPointer' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Entity/JsonPointer.php', + 'JsonSchema\\Enum' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Enum.php', 'JsonSchema\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Exception/ExceptionInterface.php', 'JsonSchema\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Exception/InvalidArgumentException.php', 'JsonSchema\\Exception\\InvalidConfigException' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Exception/InvalidConfigException.php', @@ -2400,6 +2408,10 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'JsonSchema\\Rfc3339' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Rfc3339.php', 'JsonSchema\\SchemaStorage' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/SchemaStorage.php', 'JsonSchema\\SchemaStorageInterface' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/SchemaStorageInterface.php', + 'JsonSchema\\Tool\\DeepComparer' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Tool/DeepComparer.php', + 'JsonSchema\\Tool\\DeepCopy' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Tool/DeepCopy.php', + 'JsonSchema\\Tool\\Validator\\RelativeReferenceValidator' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Tool/Validator/RelativeReferenceValidator.php', + 'JsonSchema\\Tool\\Validator\\UriValidator' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Tool/Validator/UriValidator.php', 'JsonSchema\\UriResolverInterface' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/UriResolverInterface.php', 'JsonSchema\\UriRetrieverInterface' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/UriRetrieverInterface.php', 'JsonSchema\\Uri\\Retrievers\\AbstractRetriever' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Uri/Retrievers/AbstractRetriever.php', @@ -2427,6 +2439,10 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'Lcobucci\\Clock\\Clock' => __DIR__ . '/..' . '/lcobucci/clock/src/Clock.php', 'Lcobucci\\Clock\\FrozenClock' => __DIR__ . '/..' . '/lcobucci/clock/src/FrozenClock.php', 'Lcobucci\\Clock\\SystemClock' => __DIR__ . '/..' . '/lcobucci/clock/src/SystemClock.php', + 'MabeEnum\\Enum' => __DIR__ . '/..' . '/marc-mabe/php-enum/src/Enum.php', + 'MabeEnum\\EnumMap' => __DIR__ . '/..' . '/marc-mabe/php-enum/src/EnumMap.php', + 'MabeEnum\\EnumSerializableTrait' => __DIR__ . '/..' . '/marc-mabe/php-enum/src/EnumSerializableTrait.php', + 'MabeEnum\\EnumSet' => __DIR__ . '/..' . '/marc-mabe/php-enum/src/EnumSet.php', 'Masterminds\\HTML5' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5.php', 'Masterminds\\HTML5\\Elements' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Elements.php', 'Masterminds\\HTML5\\Entities' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Entities.php', diff --git a/composer/installed.json b/composer/installed.json index 450e1cace..b51ce4afe 100644 --- a/composer/installed.json +++ b/composer/installed.json @@ -1627,32 +1627,42 @@ }, { "name": "justinrainbow/json-schema", - "version": "5.3.0", - "version_normalized": "5.3.0.0", + "version": "6.4.2", + "version_normalized": "6.4.2.0", "source": { "type": "git", "url": "https://github.com/jsonrainbow/json-schema.git", - "reference": "feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8" + "reference": "ce1fd2d47799bb60668643bc6220f6278a4c1d02" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8", - "reference": "feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/ce1fd2d47799bb60668643bc6220f6278a4c1d02", + "reference": "ce1fd2d47799bb60668643bc6220f6278a4c1d02", "shasum": "" }, "require": { - "php": ">=7.1" + "ext-json": "*", + "marc-mabe/php-enum": "^4.0", + "php": "^7.2 || ^8.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1", + "friendsofphp/php-cs-fixer": "3.3.0", "json-schema/json-schema-test-suite": "1.2.0", - "phpunit/phpunit": "^4.8.35" + "marc-mabe/php-enum-phpstan": "^2.0", + "phpspec/prophecy": "^1.19", + "phpstan/phpstan": "^1.12", + "phpunit/phpunit": "^8.5" }, - "time": "2024-07-06T21:00:26+00:00", + "time": "2025-06-03T18:27:04+00:00", "bin": [ "bin/validate-json" ], "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.x-dev" + } + }, "installation-source": "dist", "autoload": { "psr-4": { @@ -1682,14 +1692,14 @@ } ], "description": "A library to validate a json schema.", - "homepage": "https://github.com/justinrainbow/json-schema", + "homepage": "https://github.com/jsonrainbow/json-schema", "keywords": [ "json", "schema" ], "support": { "issues": "https://github.com/jsonrainbow/json-schema/issues", - "source": "https://github.com/jsonrainbow/json-schema/tree/5.3.0" + "source": "https://github.com/jsonrainbow/json-schema/tree/6.4.2" }, "install-path": "../justinrainbow/json-schema" }, @@ -1875,6 +1885,82 @@ ], "install-path": "../lcobucci/clock" }, + { + "name": "marc-mabe/php-enum", + "version": "v4.7.1", + "version_normalized": "4.7.1.0", + "source": { + "type": "git", + "url": "https://github.com/marc-mabe/php-enum.git", + "reference": "7159809e5cfa041dca28e61f7f7ae58063aae8ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/marc-mabe/php-enum/zipball/7159809e5cfa041dca28e61f7f7ae58063aae8ed", + "reference": "7159809e5cfa041dca28e61f7f7ae58063aae8ed", + "shasum": "" + }, + "require": { + "ext-reflection": "*", + "php": "^7.1 | ^8.0" + }, + "require-dev": { + "phpbench/phpbench": "^0.16.10 || ^1.0.4", + "phpstan/phpstan": "^1.3.1", + "phpunit/phpunit": "^7.5.20 | ^8.5.22 | ^9.5.11", + "vimeo/psalm": "^4.17.0 | ^5.26.1" + }, + "time": "2024-11-28T04:54:44+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-3.x": "3.2-dev", + "dev-master": "4.7-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "MabeEnum\\": "src/" + }, + "classmap": [ + "stubs/Stringable.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Marc Bennewitz", + "email": "dev@mabe.berlin", + "homepage": "https://mabe.berlin/", + "role": "Lead" + } + ], + "description": "Simple and fast implementation of enumerations with native PHP", + "homepage": "https://github.com/marc-mabe/php-enum", + "keywords": [ + "enum", + "enum-map", + "enum-set", + "enumeration", + "enumerator", + "enummap", + "enumset", + "map", + "set", + "type", + "type-hint", + "typehint" + ], + "support": { + "issues": "https://github.com/marc-mabe/php-enum/issues", + "source": "https://github.com/marc-mabe/php-enum/tree/v4.7.1" + }, + "install-path": "../marc-mabe/php-enum" + }, { "name": "masterminds/html5", "version": "2.9.0", @@ -2799,24 +2885,24 @@ }, { "name": "php-opencloud/openstack", - "version": "v3.12.0", - "version_normalized": "3.12.0.0", + "version": "v3.14.0", + "version_normalized": "3.14.0.0", "source": { "type": "git", "url": "https://github.com/php-opencloud/openstack.git", - "reference": "9886899de3a81e95dd6cb352253870fa02abd0f9" + "reference": "b92ea5581ca91779b88f08b1b44e6ca880b34fc3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-opencloud/openstack/zipball/9886899de3a81e95dd6cb352253870fa02abd0f9", - "reference": "9886899de3a81e95dd6cb352253870fa02abd0f9", + "url": "https://api.github.com/repos/php-opencloud/openstack/zipball/b92ea5581ca91779b88f08b1b44e6ca880b34fc3", + "reference": "b92ea5581ca91779b88f08b1b44e6ca880b34fc3", "shasum": "" }, "require": { "guzzlehttp/guzzle": "^7.0", "guzzlehttp/psr7": ">=1.7", "guzzlehttp/uri-template": "^0.2 || ^1.0", - "justinrainbow/json-schema": "^5.2", + "justinrainbow/json-schema": "^5.2 || ^6.0", "php": "^7.2.5 || ^8.0" }, "require-dev": { @@ -2829,7 +2915,7 @@ "phpunit/phpunit": ">=8.5.23 <9.0", "psr/log": "^1.0" }, - "time": "2025-01-07T07:11:23+00:00", + "time": "2025-05-30T09:26:42+00:00", "type": "library", "extra": { "branch-alias": { @@ -2873,7 +2959,7 @@ ], "support": { "issues": "https://github.com/php-opencloud/openstack/issues", - "source": "https://github.com/php-opencloud/openstack/tree/v3.12.0" + "source": "https://github.com/php-opencloud/openstack/tree/v3.14.0" }, "install-path": "../php-opencloud/openstack" }, @@ -5470,23 +5556,23 @@ }, { "name": "symfony/polyfill-php80", - "version": "v1.31.0", - "version_normalized": "1.31.0.0", + "version": "v1.32.0", + "version_normalized": "1.32.0.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", - "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", "shasum": "" }, "require": { "php": ">=7.2" }, - "time": "2024-09-09T11:45:10+00:00", + "time": "2025-01-02T08:10:11+00:00", "type": "library", "extra": { "thanks": { @@ -5533,7 +5619,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.32.0" }, "funding": [ { diff --git a/composer/installed.php b/composer/installed.php index f4080a9e5..75df019b0 100644 --- a/composer/installed.php +++ b/composer/installed.php @@ -200,9 +200,9 @@ 'dev_requirement' => false, ), 'justinrainbow/json-schema' => array( - 'pretty_version' => '5.3.0', - 'version' => '5.3.0.0', - 'reference' => 'feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8', + 'pretty_version' => '6.4.2', + 'version' => '6.4.2.0', + 'reference' => 'ce1fd2d47799bb60668643bc6220f6278a4c1d02', 'type' => 'library', 'install_path' => __DIR__ . '/../justinrainbow/json-schema', 'aliases' => array(), @@ -235,6 +235,15 @@ 'aliases' => array(), 'dev_requirement' => false, ), + 'marc-mabe/php-enum' => array( + 'pretty_version' => 'v4.7.1', + 'version' => '4.7.1.0', + 'reference' => '7159809e5cfa041dca28e61f7f7ae58063aae8ed', + 'type' => 'library', + 'install_path' => __DIR__ . '/../marc-mabe/php-enum', + 'aliases' => array(), + 'dev_requirement' => false, + ), 'masterminds/html5' => array( 'pretty_version' => '2.9.0', 'version' => '2.9.0.0', @@ -392,9 +401,9 @@ 'dev_requirement' => false, ), 'php-opencloud/openstack' => array( - 'pretty_version' => 'v3.12.0', - 'version' => '3.12.0.0', - 'reference' => '9886899de3a81e95dd6cb352253870fa02abd0f9', + 'pretty_version' => 'v3.14.0', + 'version' => '3.14.0.0', + 'reference' => 'b92ea5581ca91779b88f08b1b44e6ca880b34fc3', 'type' => 'library', 'install_path' => __DIR__ . '/../php-opencloud/openstack', 'aliases' => array(), @@ -776,9 +785,9 @@ 'dev_requirement' => false, ), 'symfony/polyfill-php80' => array( - 'pretty_version' => 'v1.31.0', - 'version' => '1.31.0.0', - 'reference' => '60328e362d4c2c802a54fcbf04f9d3fb892b4cf8', + 'pretty_version' => 'v1.32.0', + 'version' => '1.32.0.0', + 'reference' => '0cc9dd0f17f61d8131e7df6b84bd344899fe2608', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/polyfill-php80', 'aliases' => array(), diff --git a/composer/platform_check.php b/composer/platform_check.php index 4c3a5d68f..2beb14918 100644 --- a/composer/platform_check.php +++ b/composer/platform_check.php @@ -19,8 +19,7 @@ echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL; } } - trigger_error( - 'Composer detected issues in your platform: ' . implode(' ', $issues), - E_USER_ERROR + throw new \RuntimeException( + 'Composer detected issues in your platform: ' . implode(' ', $issues) ); } diff --git a/justinrainbow/json-schema/src/JsonSchema/ConstraintError.php b/justinrainbow/json-schema/src/JsonSchema/ConstraintError.php new file mode 100644 index 000000000..c17cfeff1 --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/ConstraintError.php @@ -0,0 +1,117 @@ +getValue(); + static $messages = [ + self::ADDITIONAL_ITEMS => 'The item %s[%s] is not defined and the definition does not allow additional items', + self::ADDITIONAL_PROPERTIES => 'The property %s is not defined and the definition does not allow additional properties', + self::ALL_OF => 'Failed to match all schemas', + self::ANY_OF => 'Failed to match at least one schema', + self::DEPENDENCIES => '%s depends on %s, which is missing', + self::DISALLOW => 'Disallowed value was matched', + self::DIVISIBLE_BY => 'Is not divisible by %d', + self::ENUM => 'Does not have a value in the enumeration %s', + self::CONSTANT => 'Does not have a value equal to %s', + self::EXCLUSIVE_MINIMUM => 'Must have a minimum value greater than %d', + self::EXCLUSIVE_MAXIMUM => 'Must have a maximum value less than %d', + self::FORMAT_COLOR => 'Invalid color', + self::FORMAT_DATE => 'Invalid date %s, expected format YYYY-MM-DD', + self::FORMAT_DATE_TIME => 'Invalid date-time %s, expected format YYYY-MM-DDThh:mm:ssZ or YYYY-MM-DDThh:mm:ss+hh:mm', + self::FORMAT_DATE_UTC => 'Invalid time %s, expected integer of milliseconds since Epoch', + self::FORMAT_EMAIL => 'Invalid email', + self::FORMAT_HOSTNAME => 'Invalid hostname', + self::FORMAT_IP => 'Invalid IP address', + self::FORMAT_PHONE => 'Invalid phone number', + self::FORMAT_REGEX=> 'Invalid regex format %s', + self::FORMAT_STYLE => 'Invalid style', + self::FORMAT_TIME => 'Invalid time %s, expected format hh:mm:ss', + self::FORMAT_URL => 'Invalid URL format', + self::FORMAT_URL_REF => 'Invalid URL reference format', + self::LENGTH_MAX => 'Must be at most %d characters long', + self::INVALID_SCHEMA => 'Schema is not valid', + self::LENGTH_MIN => 'Must be at least %d characters long', + self::MAX_ITEMS => 'There must be a maximum of %d items in the array, %d found', + self::MAXIMUM => 'Must have a maximum value less than or equal to %d', + self::MIN_ITEMS => 'There must be a minimum of %d items in the array, %d found', + self::MINIMUM => 'Must have a minimum value greater than or equal to %d', + self::MISSING_MAXIMUM => 'Use of exclusiveMaximum requires presence of maximum', + self::MISSING_MINIMUM => 'Use of exclusiveMinimum requires presence of minimum', + /*self::MISSING_ERROR => 'Used for tests; this error is deliberately commented out',*/ + self::MULTIPLE_OF => 'Must be a multiple of %s', + self::NOT => 'Matched a schema which it should not', + self::ONE_OF => 'Failed to match exactly one schema', + self::REQUIRED => 'The property %s is required', + self::REQUIRES => 'The presence of the property %s requires that %s also be present', + self::PATTERN => 'Does not match the regex pattern %s', + self::PREGEX_INVALID => 'The pattern %s is invalid', + self::PROPERTIES_MIN => 'Must contain a minimum of %d properties', + self::PROPERTIES_MAX => 'Must contain no more than %d properties', + self::TYPE => '%s value found, but %s is required', + self::UNIQUE_ITEMS => 'There are no duplicates allowed in the array' + ]; + + if (!isset($messages[$name])) { + throw new InvalidArgumentException('Missing error message for ' . $name); + } + + return $messages[$name]; + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/BaseConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/BaseConstraint.php index 9a4ea1e5a..b1b34c32d 100644 --- a/justinrainbow/json-schema/src/JsonSchema/Constraints/BaseConstraint.php +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/BaseConstraint.php @@ -1,14 +1,11 @@ */ protected $errorMask = Validator::ERROR_NONE; @@ -35,42 +33,46 @@ class BaseConstraint */ protected $factory; - /** - * @param Factory $factory - */ public function __construct(?Factory $factory = null) { $this->factory = $factory ?: new Factory(); } - public function addError(?JsonPointer $path, $message, $constraint = '', ?array $more = null) + public function addError(ConstraintError $constraint, ?JsonPointer $path = null, array $more = []): void { - $error = array( + $message = $constraint->getMessage(); + $name = $constraint->getValue(); + $error = [ 'property' => $this->convertJsonPointerIntoPropertyPath($path ?: new JsonPointer('')), - 'pointer' => ltrim(strval($path ?: new JsonPointer('')), '#'), - 'message' => $message, - 'constraint' => $constraint, + 'pointer' => ltrim((string) ($path ?: new JsonPointer('')), '#'), + 'message' => ucfirst(vsprintf($message, array_map(static function ($val) { + if (is_scalar($val)) { + return is_bool($val) ? var_export($val, true) : $val; + } + + return json_encode($val); + }, array_values($more)))), + 'constraint' => [ + 'name' => $name, + 'params' => $more + ], 'context' => $this->factory->getErrorContext(), - ); + ]; if ($this->factory->getConfig(Constraint::CHECK_MODE_EXCEPTIONS)) { throw new ValidationException(sprintf('Error validating %s: %s', $error['pointer'], $error['message'])); } - if (is_array($more) && count($more) > 0) { - $error += $more; - } - $this->errors[] = $error; $this->errorMask |= $error['context']; } - public function addErrors(array $errors) + public function addErrors(array $errors): void { if ($errors) { $this->errors = array_merge($this->errors, $errors); $errorMask = &$this->errorMask; - array_walk($errors, function ($error) use (&$errorMask) { + array_walk($errors, static function ($error) use (&$errorMask) { if (isset($error['context'])) { $errorMask |= $error['context']; } @@ -78,20 +80,24 @@ public function addErrors(array $errors) } } - public function getErrors($errorContext = Validator::ERROR_ALL) + /** + * @phpstan-param int-mask-of $errorContext + */ + public function getErrors(int $errorContext = Validator::ERROR_ALL): array { if ($errorContext === Validator::ERROR_ALL) { return $this->errors; } - return array_filter($this->errors, function ($error) use ($errorContext) { - if ($errorContext & $error['context']) { - return true; - } + return array_filter($this->errors, static function ($error) use ($errorContext) { + return (bool) ($errorContext & $error['context']); }); } - public function numErrors($errorContext = Validator::ERROR_ALL) + /** + * @phpstan-param int-mask-of $errorContext + */ + public function numErrors(int $errorContext = Validator::ERROR_ALL): int { if ($errorContext === Validator::ERROR_ALL) { return count($this->errors); @@ -100,42 +106,38 @@ public function numErrors($errorContext = Validator::ERROR_ALL) return count($this->getErrors($errorContext)); } - public function isValid() + public function isValid(): bool { return !$this->getErrors(); } /** - * Clears any reported errors. Should be used between + * Clears any reported errors. Should be used between * multiple validation checks. */ - public function reset() + public function reset(): void { - $this->errors = array(); + $this->errors = []; $this->errorMask = Validator::ERROR_NONE; } /** * Get the error mask * - * @return int + * @phpstan-return int-mask-of */ - public function getErrorMask() + public function getErrorMask(): int { return $this->errorMask; } /** * Recursively cast an associative array to an object - * - * @param array $array - * - * @return object */ - public static function arrayToObjectRecursive($array) + public static function arrayToObjectRecursive(array $array): object { $json = json_encode($array); - if (json_last_error() !== \JSON_ERROR_NONE) { + if (json_last_error() !== JSON_ERROR_NONE) { $message = 'Unable to encode schema array as JSON'; if (function_exists('json_last_error_msg')) { $message .= ': ' . json_last_error_msg(); @@ -143,6 +145,26 @@ public static function arrayToObjectRecursive($array) throw new InvalidArgumentException($message); } - return (object) json_decode($json); + return (object) json_decode($json, false); + } + + /** + * Transform a JSON pattern into a PCRE regex + */ + public static function jsonPatternToPhpRegex(string $pattern): string + { + return '~' . str_replace('~', '\\~', $pattern) . '~u'; + } + + protected function convertJsonPointerIntoPropertyPath(JsonPointer $pointer): string + { + $result = array_map( + static function ($path) { + return sprintf(is_numeric($path) ? '[%d]' : '.%s', $path); + }, + $pointer->getPropertyPaths() + ); + + return trim(implode('', $result), '.'); } } diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/CollectionConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/CollectionConstraint.php index a5ddd35b8..e42a6fb8d 100644 --- a/justinrainbow/json-schema/src/JsonSchema/Constraints/CollectionConstraint.php +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/CollectionConstraint.php @@ -1,5 +1,7 @@ minItems) && count($value) < $schema->minItems) { - $this->addError($path, 'There must be a minimum of ' . $schema->minItems . ' items in the array', 'minItems', array('minItems' => $schema->minItems)); + $this->addError(ConstraintError::MIN_ITEMS(), $path, ['minItems' => $schema->minItems, 'found' => count($value)]); } // Verify maxItems if (isset($schema->maxItems) && count($value) > $schema->maxItems) { - $this->addError($path, 'There must be a maximum of ' . $schema->maxItems . ' items in the array', 'maxItems', array('maxItems' => $schema->maxItems)); + $this->addError(ConstraintError::MAX_ITEMS(), $path, ['maxItems' => $schema->maxItems, 'found' => count($value)]); } // Verify uniqueItems if (isset($schema->uniqueItems) && $schema->uniqueItems) { - $unique = $value; - if (is_array($value) && count($value)) { - $unique = array_map(function ($e) { - return var_export($e, true); - }, $value); - } - if (count(array_unique($unique)) != count($value)) { - $this->addError($path, 'There are no duplicates allowed in the array', 'uniqueItems'); + $count = count($value); + for ($x = 0; $x < $count - 1; $x++) { + for ($y = $x + 1; $y < $count; $y++) { + if (DeepComparer::isEqual($value[$x], $value[$y])) { + $this->addError(ConstraintError::UNIQUE_ITEMS(), $path); + break 2; + } + } } } - // Verify items - if (isset($schema->items)) { - $this->validateItems($value, $schema, $path, $i); - } + $this->validateItems($value, $schema, $path, $i); } /** * Validates the items * - * @param array $value - * @param \stdClass $schema - * @param JsonPointer|null $path - * @param string $i + * @param array $value + * @param \stdClass $schema + * @param string $i */ - protected function validateItems(&$value, $schema = null, ?JsonPointer $path = null, $i = null) + protected function validateItems(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void { + if (\is_null($schema) || !isset($schema->items)) { + return; + } + + if ($schema->items === true) { + return; + } + if (is_object($schema->items)) { // just one type definition for the whole array foreach ($value as $k => &$v) { @@ -98,7 +106,14 @@ protected function validateItems(&$value, $schema = null, ?JsonPointer $path = n $this->checkUndefined($v, $schema->additionalItems, $path, $k); } else { $this->addError( - $path, 'The item ' . $i . '[' . $k . '] is not defined and the definition does not allow additional items', 'additionalItems', array('additionalItems' => $schema->additionalItems)); + ConstraintError::ADDITIONAL_ITEMS(), + $path, + [ + 'item' => $i, + 'property' => $k, + 'additionalItems' => $schema->additionalItems + ] + ); } } else { // Should be valid against an empty schema diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/ConstConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/ConstConstraint.php new file mode 100644 index 000000000..b9bb1f48d --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/ConstConstraint.php @@ -0,0 +1,51 @@ + + */ +class ConstConstraint extends Constraint +{ + /** + * {@inheritdoc} + */ + public function check(&$element, $schema = null, ?JsonPointer $path = null, $i = null): void + { + // Only validate const if the attribute exists + if ($element instanceof UndefinedConstraint && (!isset($schema->required) || !$schema->required)) { + return; + } + $const = $schema->const; + + $type = gettype($element); + $constType = gettype($const); + + if ($this->factory->getConfig(self::CHECK_MODE_TYPE_CAST) && $type === 'array' && $constType === 'object') { + if (DeepComparer::isEqual((object) $element, $const)) { + return; + } + } + + if (DeepComparer::isEqual($element, $const)) { + return; + } + + $this->addError(ConstraintError::CONSTANT(), $path, ['const' => $schema->const]); + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Constraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Constraint.php index 5aaa718c2..8e818f0aa 100644 --- a/justinrainbow/json-schema/src/JsonSchema/Constraints/Constraint.php +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Constraint.php @@ -1,71 +1,57 @@ - * @author Bruno Prieto Reis - */ abstract class Constraint extends BaseConstraint implements ConstraintInterface { + /** @var string */ protected $inlineSchemaProperty = '$schema'; - const CHECK_MODE_NONE = 0x00000000; - const CHECK_MODE_NORMAL = 0x00000001; - const CHECK_MODE_TYPE_CAST = 0x00000002; - const CHECK_MODE_COERCE_TYPES = 0x00000004; - const CHECK_MODE_APPLY_DEFAULTS = 0x00000008; - const CHECK_MODE_EXCEPTIONS = 0x00000010; - const CHECK_MODE_DISABLE_FORMAT = 0x00000020; - const CHECK_MODE_ONLY_REQUIRED_DEFAULTS = 0x00000080; - const CHECK_MODE_VALIDATE_SCHEMA = 0x00000100; + public const CHECK_MODE_NONE = 0x00000000; + public const CHECK_MODE_NORMAL = 0x00000001; + public const CHECK_MODE_TYPE_CAST = 0x00000002; + public const CHECK_MODE_COERCE_TYPES = 0x00000004; + public const CHECK_MODE_APPLY_DEFAULTS = 0x00000008; + public const CHECK_MODE_EXCEPTIONS = 0x00000010; + public const CHECK_MODE_DISABLE_FORMAT = 0x00000020; + public const CHECK_MODE_EARLY_COERCE = 0x00000040; + public const CHECK_MODE_ONLY_REQUIRED_DEFAULTS = 0x00000080; + public const CHECK_MODE_VALIDATE_SCHEMA = 0x00000100; /** * Bubble down the path * * @param JsonPointer|null $path Current path * @param mixed $i What to append to the path - * - * @return JsonPointer; */ - protected function incrementPath(?JsonPointer $path, $i) + protected function incrementPath(?JsonPointer $path, $i): JsonPointer { - $path = $path ?: new JsonPointer(''); + $path = $path ?? new JsonPointer(''); if ($i === null || $i === '') { return $path; } - $path = $path->withPropertyPaths( + return $path->withPropertyPaths( array_merge( $path->getPropertyPaths(), - array($i) + [$i] ) ); - - return $path; } /** * Validates an array * - * @param mixed $value - * @param mixed $schema - * @param JsonPointer|null $path - * @param mixed $i + * @param mixed $value + * @param mixed $schema + * @param mixed $i */ - protected function checkArray(&$value, $schema = null, ?JsonPointer $path = null, $i = null) + protected function checkArray(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void { $validator = $this->factory->createInstanceFor('collection'); $validator->check($value, $schema, $path, $i); @@ -76,16 +62,23 @@ protected function checkArray(&$value, $schema = null, ?JsonPointer $path = null /** * Validates an object * - * @param mixed $value - * @param mixed $schema - * @param JsonPointer|null $path - * @param mixed $properties - * @param mixed $additionalProperties - * @param mixed $patternProperties + * @param mixed $value + * @param mixed $schema + * @param mixed $properties + * @param mixed $additionalProperties + * @param mixed $patternProperties + * @param array $appliedDefaults */ - protected function checkObject(&$value, $schema = null, ?JsonPointer $path = null, $properties = null, - $additionalProperties = null, $patternProperties = null, $appliedDefaults = array()) - { + protected function checkObject( + &$value, + $schema = null, + ?JsonPointer $path = null, + $properties = null, + $additionalProperties = null, + $patternProperties = null, + array $appliedDefaults = [] + ): void { + /** @var ObjectConstraint $validator */ $validator = $this->factory->createInstanceFor('object'); $validator->check($value, $schema, $path, $properties, $additionalProperties, $patternProperties, $appliedDefaults); @@ -93,14 +86,13 @@ protected function checkObject(&$value, $schema = null, ?JsonPointer $path = nul } /** - * Validates the type of a property + * Validates the type of the value * - * @param mixed $value - * @param mixed $schema - * @param JsonPointer|null $path - * @param mixed $i + * @param mixed $value + * @param mixed $schema + * @param mixed $i */ - protected function checkType(&$value, $schema = null, ?JsonPointer $path = null, $i = null) + protected function checkType(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void { $validator = $this->factory->createInstanceFor('type'); $validator->check($value, $schema, $path, $i); @@ -111,13 +103,13 @@ protected function checkType(&$value, $schema = null, ?JsonPointer $path = null, /** * Checks a undefined element * - * @param mixed $value - * @param mixed $schema - * @param JsonPointer|null $path - * @param mixed $i + * @param mixed $value + * @param mixed $schema + * @param mixed $i */ - protected function checkUndefined(&$value, $schema = null, ?JsonPointer $path = null, $i = null, $fromDefault = false) + protected function checkUndefined(&$value, $schema = null, ?JsonPointer $path = null, $i = null, bool $fromDefault = false): void { + /** @var UndefinedConstraint $validator */ $validator = $this->factory->createInstanceFor('undefined'); $validator->check($value, $this->factory->getSchemaStorage()->resolveRefSchema($schema), $path, $i, $fromDefault); @@ -128,12 +120,11 @@ protected function checkUndefined(&$value, $schema = null, ?JsonPointer $path = /** * Checks a string element * - * @param mixed $value - * @param mixed $schema - * @param JsonPointer|null $path - * @param mixed $i + * @param mixed $value + * @param mixed $schema + * @param mixed $i */ - protected function checkString($value, $schema = null, ?JsonPointer $path = null, $i = null) + protected function checkString($value, $schema = null, ?JsonPointer $path = null, $i = null): void { $validator = $this->factory->createInstanceFor('string'); $validator->check($value, $schema, $path, $i); @@ -144,12 +135,11 @@ protected function checkString($value, $schema = null, ?JsonPointer $path = null /** * Checks a number element * - * @param mixed $value - * @param mixed $schema - * @param JsonPointer $path - * @param mixed $i + * @param mixed $value + * @param mixed $schema + * @param mixed $i */ - protected function checkNumber($value, $schema = null, ?JsonPointer $path = null, $i = null) + protected function checkNumber($value, $schema = null, ?JsonPointer $path = null, $i = null): void { $validator = $this->factory->createInstanceFor('number'); $validator->check($value, $schema, $path, $i); @@ -160,12 +150,11 @@ protected function checkNumber($value, $schema = null, ?JsonPointer $path = null /** * Checks a enum element * - * @param mixed $value - * @param mixed $schema - * @param JsonPointer|null $path - * @param mixed $i + * @param mixed $value + * @param mixed $schema + * @param mixed $i */ - protected function checkEnum($value, $schema = null, ?JsonPointer $path = null, $i = null) + protected function checkEnum($value, $schema = null, ?JsonPointer $path = null, $i = null): void { $validator = $this->factory->createInstanceFor('enum'); $validator->check($value, $schema, $path, $i); @@ -174,45 +163,40 @@ protected function checkEnum($value, $schema = null, ?JsonPointer $path = null, } /** - * Checks format of an element + * Checks a const element * - * @param mixed $value - * @param mixed $schema - * @param JsonPointer|null $path - * @param mixed $i + * @param mixed $value + * @param mixed $schema + * @param mixed $i */ - protected function checkFormat($value, $schema = null, ?JsonPointer $path = null, $i = null) + protected function checkConst($value, $schema = null, ?JsonPointer $path = null, $i = null): void { - $validator = $this->factory->createInstanceFor('format'); + $validator = $this->factory->createInstanceFor('const'); $validator->check($value, $schema, $path, $i); $this->addErrors($validator->getErrors()); } /** - * Get the type check based on the set check mode. + * Checks format of an element * - * @return TypeCheck\TypeCheckInterface + * @param mixed $value + * @param mixed $schema + * @param mixed $i */ - protected function getTypeCheck() + protected function checkFormat($value, $schema = null, ?JsonPointer $path = null, $i = null): void { - return $this->factory->getTypeCheck(); + $validator = $this->factory->createInstanceFor('format'); + $validator->check($value, $schema, $path, $i); + + $this->addErrors($validator->getErrors()); } /** - * @param JsonPointer $pointer - * - * @return string property path + * Get the type check based on the set check mode. */ - protected function convertJsonPointerIntoPropertyPath(JsonPointer $pointer) + protected function getTypeCheck(): TypeCheck\TypeCheckInterface { - $result = array_map( - function ($path) { - return sprintf(is_numeric($path) ? '[%d]' : '.%s', $path); - }, - $pointer->getPropertyPaths() - ); - - return trim(implode('', $result), '.'); + return $this->factory->getTypeCheck(); } } diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/ConstraintInterface.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/ConstraintInterface.php index a5a235ad4..9e43ecb56 100644 --- a/justinrainbow/json-schema/src/JsonSchema/Constraints/ConstraintInterface.php +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/ConstraintInterface.php @@ -1,5 +1,7 @@ required) || !$schema->required)) { @@ -32,23 +36,24 @@ public function check(&$element, $schema = null, ?JsonPointer $path = null, $i = foreach ($schema->enum as $enum) { $enumType = gettype($enum); - if ($this->factory->getConfig(self::CHECK_MODE_TYPE_CAST) && $type == 'array' && $enumType == 'object') { - if ((object) $element == $enum) { - return; - } + + if ($enumType === 'object' + && $type === 'array' + && $this->factory->getConfig(self::CHECK_MODE_TYPE_CAST) + && DeepComparer::isEqual((object) $element, $enum) + ) { + return; + } + + if (($type === $enumType) && DeepComparer::isEqual($element, $enum)) { + return; } - if ($type === gettype($enum)) { - if ($type == 'object') { - if ($element == $enum) { - return; - } - } elseif ($element === $enum) { - return; - } + if (is_numeric($element) && is_numeric($enum) && DeepComparer::isEqual((float) $element, (float) $enum)) { + return; } } - $this->addError($path, 'Does not have a value in the enumeration ' . json_encode($schema->enum), 'enum', array('enum' => $schema->enum)); + $this->addError(ConstraintError::ENUM(), $path, ['enum' => $schema->enum]); } } diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Factory.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Factory.php index 840854b30..b9220b9d8 100644 --- a/justinrainbow/json-schema/src/JsonSchema/Constraints/Factory.php +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Factory.php @@ -1,5 +1,7 @@ */ private $checkMode = Constraint::CHECK_MODE_NORMAL; /** - * @var TypeCheck\TypeCheckInterface[] + * @var array + * @phpstan-var array, TypeCheck\TypeCheckInterface> */ - private $typeCheck = array(); + private $typeCheck = []; /** * @var int Validation context @@ -49,7 +53,7 @@ class Factory /** * @var array */ - protected $constraintMap = array( + protected $constraintMap = [ 'array' => 'JsonSchema\Constraints\CollectionConstraint', 'collection' => 'JsonSchema\Constraints\CollectionConstraint', 'object' => 'JsonSchema\Constraints\ObjectConstraint', @@ -58,25 +62,24 @@ class Factory 'string' => 'JsonSchema\Constraints\StringConstraint', 'number' => 'JsonSchema\Constraints\NumberConstraint', 'enum' => 'JsonSchema\Constraints\EnumConstraint', + 'const' => 'JsonSchema\Constraints\ConstConstraint', 'format' => 'JsonSchema\Constraints\FormatConstraint', 'schema' => 'JsonSchema\Constraints\SchemaConstraint', 'validator' => 'JsonSchema\Validator' - ); + ]; /** * @var array */ - private $instanceCache = array(); + private $instanceCache = []; /** - * @param SchemaStorage $schemaStorage - * @param UriRetrieverInterface $uriRetriever - * @param int $checkMode + * @phpstan-param int-mask-of $checkMode */ public function __construct( ?SchemaStorageInterface $schemaStorage = null, ?UriRetrieverInterface $uriRetriever = null, - $checkMode = Constraint::CHECK_MODE_NORMAL + int $checkMode = Constraint::CHECK_MODE_NORMAL ) { // set provided config options $this->setConfig($checkMode); @@ -89,8 +92,9 @@ public function __construct( * Set config values * * @param int $checkMode Set checkMode options - does not preserve existing flags + * @phpstan-param int-mask-of $checkMode */ - public function setConfig($checkMode = Constraint::CHECK_MODE_NORMAL) + public function setConfig(int $checkMode = Constraint::CHECK_MODE_NORMAL): void { $this->checkMode = $checkMode; } @@ -98,9 +102,9 @@ public function setConfig($checkMode = Constraint::CHECK_MODE_NORMAL) /** * Enable checkMode flags * - * @param int $options + * @phpstan-param int-mask-of $options */ - public function addConfig($options) + public function addConfig(int $options): void { $this->checkMode |= $options; } @@ -108,9 +112,9 @@ public function addConfig($options) /** * Disable checkMode flags * - * @param int $options + * @phpstan-param int-mask-of $options */ - public function removeConfig($options) + public function removeConfig(int $options): void { $this->checkMode &= ~$options; } @@ -118,11 +122,12 @@ public function removeConfig($options) /** * Get checkMode option * - * @param int $options Options to get, if null then return entire bitmask + * @param int|null $options Options to get, if null then return entire bitmask + * @phpstan-param int-mask-of|null $options Options to get, if null then return entire bitmask * - * @return int + * @phpstan-return int-mask-of */ - public function getConfig($options = null) + public function getConfig(?int $options = null): int { if ($options === null) { return $this->checkMode; @@ -131,20 +136,17 @@ public function getConfig($options = null) return $this->checkMode & $options; } - /** - * @return UriRetrieverInterface - */ - public function getUriRetriever() + public function getUriRetriever(): UriRetrieverInterface { return $this->uriRetriever; } - public function getSchemaStorage() + public function getSchemaStorage(): SchemaStorageInterface { return $this->schemaStorage; } - public function getTypeCheck() + public function getTypeCheck(): TypeCheck\TypeCheckInterface { if (!isset($this->typeCheck[$this->checkMode])) { $this->typeCheck[$this->checkMode] = ($this->checkMode & Constraint::CHECK_MODE_TYPE_CAST) @@ -155,13 +157,7 @@ public function getTypeCheck() return $this->typeCheck[$this->checkMode]; } - /** - * @param string $name - * @param string $class - * - * @return Factory - */ - public function setConstraintClass($name, $class) + public function setConstraintClass(string $name, string $class): Factory { // Ensure class exists if (!class_exists($class)) { @@ -183,7 +179,8 @@ public function setConstraintClass($name, $class) * * @throws InvalidArgumentException if is not possible create the constraint instance * - * @return ConstraintInterface|ObjectConstraint + * @return ConstraintInterface&BaseConstraint + * @phpstan-return ConstraintInterface&BaseConstraint */ public function createInstanceFor($constraintName) { @@ -201,9 +198,9 @@ public function createInstanceFor($constraintName) /** * Get the error context * - * @return string + * @phpstan-return Validator::ERROR_DOCUMENT_VALIDATION|Validator::ERROR_SCHEMA_VALIDATION */ - public function getErrorContext() + public function getErrorContext(): int { return $this->errorContext; } @@ -211,9 +208,9 @@ public function getErrorContext() /** * Set the error context * - * @param string $validationContext + * @phpstan-param Validator::ERROR_DOCUMENT_VALIDATION|Validator::ERROR_SCHEMA_VALIDATION $errorContext */ - public function setErrorContext($errorContext) + public function setErrorContext(int $errorContext): void { $this->errorContext = $errorContext; } diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/FormatConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/FormatConstraint.php index 29843e6c9..9ed4df9dc 100644 --- a/justinrainbow/json-schema/src/JsonSchema/Constraints/FormatConstraint.php +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/FormatConstraint.php @@ -1,5 +1,7 @@ format) || $this->factory->getConfig(self::CHECK_MODE_DISABLE_FORMAT)) { return; @@ -32,113 +37,107 @@ public function check(&$element, $schema = null, ?JsonPointer $path = null, $i = switch ($schema->format) { case 'date': - if (!$date = $this->validateDateTime($element, 'Y-m-d')) { - $this->addError($path, sprintf('Invalid date %s, expected format YYYY-MM-DD', json_encode($element)), 'format', array('format' => $schema->format)); + if (is_string($element) && !$date = $this->validateDateTime($element, 'Y-m-d')) { + $this->addError(ConstraintError::FORMAT_DATE(), $path, [ + 'date' => $element, + 'format' => $schema->format + ] + ); } break; case 'time': - if (!$this->validateDateTime($element, 'H:i:s')) { - $this->addError($path, sprintf('Invalid time %s, expected format hh:mm:ss', json_encode($element)), 'format', array('format' => $schema->format)); + if (is_string($element) && !$this->validateDateTime($element, 'H:i:s')) { + $this->addError(ConstraintError::FORMAT_TIME(), $path, [ + 'time' => json_encode($element), + 'format' => $schema->format, + ] + ); } break; case 'date-time': - if (null === Rfc3339::createFromString($element)) { - $this->addError($path, sprintf('Invalid date-time %s, expected format YYYY-MM-DDThh:mm:ssZ or YYYY-MM-DDThh:mm:ss+hh:mm', json_encode($element)), 'format', array('format' => $schema->format)); + if (is_string($element) && null === Rfc3339::createFromString($element)) { + $this->addError(ConstraintError::FORMAT_DATE_TIME(), $path, [ + 'dateTime' => json_encode($element), + 'format' => $schema->format + ] + ); } break; case 'utc-millisec': if (!$this->validateDateTime($element, 'U')) { - $this->addError($path, sprintf('Invalid time %s, expected integer of milliseconds since Epoch', json_encode($element)), 'format', array('format' => $schema->format)); + $this->addError(ConstraintError::FORMAT_DATE_UTC(), $path, [ + 'value' => $element, + 'format' => $schema->format]); } break; case 'regex': if (!$this->validateRegex($element)) { - $this->addError($path, 'Invalid regex format ' . $element, 'format', array('format' => $schema->format)); + $this->addError(ConstraintError::FORMAT_REGEX(), $path, [ + 'value' => $element, + 'format' => $schema->format + ] + ); } break; case 'color': if (!$this->validateColor($element)) { - $this->addError($path, 'Invalid color', 'format', array('format' => $schema->format)); + $this->addError(ConstraintError::FORMAT_COLOR(), $path, ['format' => $schema->format]); } break; case 'style': if (!$this->validateStyle($element)) { - $this->addError($path, 'Invalid style', 'format', array('format' => $schema->format)); + $this->addError(ConstraintError::FORMAT_STYLE(), $path, ['format' => $schema->format]); } break; case 'phone': if (!$this->validatePhone($element)) { - $this->addError($path, 'Invalid phone number', 'format', array('format' => $schema->format)); + $this->addError(ConstraintError::FORMAT_PHONE(), $path, ['format' => $schema->format]); } break; case 'uri': - if (null === filter_var($element, FILTER_VALIDATE_URL, FILTER_NULL_ON_FAILURE)) { - $this->addError($path, 'Invalid URL format', 'format', array('format' => $schema->format)); + if (is_string($element) && !UriValidator::isValid($element)) { + $this->addError(ConstraintError::FORMAT_URL(), $path, ['format' => $schema->format]); } break; case 'uriref': case 'uri-reference': - if (null === filter_var($element, FILTER_VALIDATE_URL, FILTER_NULL_ON_FAILURE)) { - // FILTER_VALIDATE_URL does not conform to RFC-3986, and cannot handle relative URLs, but - // the json-schema spec uses RFC-3986, so need a bit of hackery to properly validate them. - // See https://tools.ietf.org/html/rfc3986#section-4.2 for additional information. - if (substr($element, 0, 2) === '//') { // network-path reference - $validURL = filter_var('scheme:' . $element, FILTER_VALIDATE_URL, FILTER_NULL_ON_FAILURE); - } elseif (substr($element, 0, 1) === '/') { // absolute-path reference - $validURL = filter_var('scheme://host' . $element, FILTER_VALIDATE_URL, FILTER_NULL_ON_FAILURE); - } elseif (strlen($element)) { // relative-path reference - $pathParts = explode('/', $element, 2); - if (strpos($pathParts[0], ':') !== false) { - $validURL = null; - } else { - $validURL = filter_var('scheme://host/' . $element, FILTER_VALIDATE_URL, FILTER_NULL_ON_FAILURE); - } - } else { - $validURL = null; - } - if ($validURL === null) { - $this->addError($path, 'Invalid URL format', 'format', array('format' => $schema->format)); - } + if (is_string($element) && !(UriValidator::isValid($element) || RelativeReferenceValidator::isValid($element))) { + $this->addError(ConstraintError::FORMAT_URL(), $path, ['format' => $schema->format]); } break; case 'email': - $filterFlags = FILTER_NULL_ON_FAILURE; - if (defined('FILTER_FLAG_EMAIL_UNICODE')) { - // Only available from PHP >= 7.1.0, so ignore it for coverage checks - $filterFlags |= constant('FILTER_FLAG_EMAIL_UNICODE'); // @codeCoverageIgnore - } - if (null === filter_var($element, FILTER_VALIDATE_EMAIL, $filterFlags)) { - $this->addError($path, 'Invalid email', 'format', array('format' => $schema->format)); + if (is_string($element) && null === filter_var($element, FILTER_VALIDATE_EMAIL, FILTER_NULL_ON_FAILURE | FILTER_FLAG_EMAIL_UNICODE)) { + $this->addError(ConstraintError::FORMAT_EMAIL(), $path, ['format' => $schema->format]); } break; case 'ip-address': case 'ipv4': - if (null === filter_var($element, FILTER_VALIDATE_IP, FILTER_NULL_ON_FAILURE | FILTER_FLAG_IPV4)) { - $this->addError($path, 'Invalid IP address', 'format', array('format' => $schema->format)); + if (is_string($element) && null === filter_var($element, FILTER_VALIDATE_IP, FILTER_NULL_ON_FAILURE | FILTER_FLAG_IPV4)) { + $this->addError(ConstraintError::FORMAT_IP(), $path, ['format' => $schema->format]); } break; case 'ipv6': - if (null === filter_var($element, FILTER_VALIDATE_IP, FILTER_NULL_ON_FAILURE | FILTER_FLAG_IPV6)) { - $this->addError($path, 'Invalid IP address', 'format', array('format' => $schema->format)); + if (is_string($element) && null === filter_var($element, FILTER_VALIDATE_IP, FILTER_NULL_ON_FAILURE | FILTER_FLAG_IPV6)) { + $this->addError(ConstraintError::FORMAT_IP(), $path, ['format' => $schema->format]); } break; case 'host-name': case 'hostname': if (!$this->validateHostname($element)) { - $this->addError($path, 'Invalid hostname', 'format', array('format' => $schema->format)); + $this->addError(ConstraintError::FORMAT_HOSTNAME(), $path, ['format' => $schema->format]); } break; @@ -155,7 +154,7 @@ public function check(&$element, $schema = null, ?JsonPointer $path = null, $i = protected function validateDateTime($datetime, $format) { - $dt = \DateTime::createFromFormat($format, $datetime); + $dt = \DateTime::createFromFormat($format, (string) $datetime); if (!$dt) { return false; @@ -165,27 +164,27 @@ protected function validateDateTime($datetime, $format) return true; } - // handles the case where a non-6 digit microsecond datetime is passed - // which will fail the above string comparison because the passed - // $datetime may be '2000-05-01T12:12:12.123Z' but format() will return - // '2000-05-01T12:12:12.123000Z' - if ((strpos('u', $format) !== -1) && (preg_match('/\.\d+Z$/', $datetime))) { - return true; - } - return false; } protected function validateRegex($regex) { - return false !== @preg_match('/' . $regex . '/u', ''); + if (!is_string($regex)) { + return true; + } + + return false !== @preg_match(self::jsonPatternToPhpRegex($regex), ''); } protected function validateColor($color) { - if (in_array(strtolower($color), array('aqua', 'black', 'blue', 'fuchsia', + if (!is_string($color)) { + return true; + } + + if (in_array(strtolower($color), ['aqua', 'black', 'blue', 'fuchsia', 'gray', 'green', 'lime', 'maroon', 'navy', 'olive', 'orange', 'purple', - 'red', 'silver', 'teal', 'white', 'yellow'))) { + 'red', 'silver', 'teal', 'white', 'yellow'])) { return true; } @@ -207,6 +206,10 @@ protected function validatePhone($phone) protected function validateHostname($host) { + if (!is_string($host)) { + return true; + } + $hostnameRegex = '/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/i'; return preg_match($hostnameRegex, $host); diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/NumberConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/NumberConstraint.php index b2ade8e21..e1b2ffc3a 100644 --- a/justinrainbow/json-schema/src/JsonSchema/Constraints/NumberConstraint.php +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/NumberConstraint.php @@ -1,5 +1,7 @@ exclusiveMinimum)) { if (isset($schema->minimum)) { if ($schema->exclusiveMinimum && $element <= $schema->minimum) { - $this->addError($path, 'Must have a minimum value of ' . $schema->minimum, 'exclusiveMinimum', array('minimum' => $schema->minimum)); + $this->addError(ConstraintError::EXCLUSIVE_MINIMUM(), $path, ['minimum' => $schema->minimum]); } elseif ($element < $schema->minimum) { - $this->addError($path, 'Must have a minimum value of ' . $schema->minimum, 'minimum', array('minimum' => $schema->minimum)); + $this->addError(ConstraintError::MINIMUM(), $path, ['minimum' => $schema->minimum]); } } else { - $this->addError($path, 'Use of exclusiveMinimum requires presence of minimum', 'missingMinimum'); + $this->addError(ConstraintError::MISSING_MINIMUM(), $path); } } elseif (isset($schema->minimum) && $element < $schema->minimum) { - $this->addError($path, 'Must have a minimum value of ' . $schema->minimum, 'minimum', array('minimum' => $schema->minimum)); + $this->addError(ConstraintError::MINIMUM(), $path, ['minimum' => $schema->minimum]); } // Verify maximum if (isset($schema->exclusiveMaximum)) { if (isset($schema->maximum)) { if ($schema->exclusiveMaximum && $element >= $schema->maximum) { - $this->addError($path, 'Must have a maximum value of ' . $schema->maximum, 'exclusiveMaximum', array('maximum' => $schema->maximum)); + $this->addError(ConstraintError::EXCLUSIVE_MAXIMUM(), $path, ['maximum' => $schema->maximum]); } elseif ($element > $schema->maximum) { - $this->addError($path, 'Must have a maximum value of ' . $schema->maximum, 'maximum', array('maximum' => $schema->maximum)); + $this->addError(ConstraintError::MAXIMUM(), $path, ['maximum' => $schema->maximum]); } } else { - $this->addError($path, 'Use of exclusiveMaximum requires presence of maximum', 'missingMaximum'); + $this->addError(ConstraintError::MISSING_MAXIMUM(), $path); } } elseif (isset($schema->maximum) && $element > $schema->maximum) { - $this->addError($path, 'Must have a maximum value of ' . $schema->maximum, 'maximum', array('maximum' => $schema->maximum)); + $this->addError(ConstraintError::MAXIMUM(), $path, ['maximum' => $schema->maximum]); } // Verify divisibleBy - Draft v3 if (isset($schema->divisibleBy) && $this->fmod($element, $schema->divisibleBy) != 0) { - $this->addError($path, 'Is not divisible by ' . $schema->divisibleBy, 'divisibleBy', array('divisibleBy' => $schema->divisibleBy)); + $this->addError(ConstraintError::DIVISIBLE_BY(), $path, ['divisibleBy' => $schema->divisibleBy]); } // Verify multipleOf - Draft v4 if (isset($schema->multipleOf) && $this->fmod($element, $schema->multipleOf) != 0) { - $this->addError($path, 'Must be a multiple of ' . $schema->multipleOf, 'multipleOf', array('multipleOf' => $schema->multipleOf)); + $this->addError(ConstraintError::MULTIPLE_OF(), $path, ['multipleOf' => $schema->multipleOf]); } $this->checkFormat($element, $schema, $path, $i); diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/ObjectConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/ObjectConstraint.php index e99c44bcc..c2c614a90 100644 --- a/justinrainbow/json-schema/src/JsonSchema/Constraints/ObjectConstraint.php +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/ObjectConstraint.php @@ -1,42 +1,40 @@ - * @author Bruno Prieto Reis - */ class ObjectConstraint extends Constraint { /** - * @var array List of properties to which a default value has been applied + * @var list List of properties to which a default value has been applied */ - protected $appliedDefaults = array(); + protected $appliedDefaults = []; /** * {@inheritdoc} + * + * @param list $appliedDefaults */ - public function check(&$element, $schema = null, ?JsonPointer $path = null, $properties = null, - $additionalProp = null, $patternProperties = null, $appliedDefaults = array()) - { + public function check( + &$element, + $schema = null, + ?JsonPointer $path = null, + $properties = null, + $additionalProp = null, + $patternProperties = null, + $appliedDefaults = [] + ): void { if ($element instanceof UndefinedConstraint) { return; } $this->appliedDefaults = $appliedDefaults; - $matches = array(); + $matches = []; if ($patternProperties) { // validate the element pattern properties $matches = $this->validatePatternProperties($element, $path, $patternProperties); @@ -53,24 +51,17 @@ public function check(&$element, $schema = null, ?JsonPointer $path = null, $pro public function validatePatternProperties($element, ?JsonPointer $path, $patternProperties) { - $try = array('/', '#', '+', '~', '%'); - $matches = array(); + $matches = []; foreach ($patternProperties as $pregex => $schema) { - $delimiter = '/'; - // Choose delimiter. Necessary for patterns like ^/ , otherwise you get error - foreach ($try as $delimiter) { - if (strpos($pregex, $delimiter) === false) { // safe to use - break; - } - } + $fullRegex = self::jsonPatternToPhpRegex($pregex); // Validate the pattern before using it to test for matches - if (@preg_match($delimiter . $pregex . $delimiter . 'u', '') === false) { - $this->addError($path, 'The pattern "' . $pregex . '" is invalid', 'pregex', array('pregex' => $pregex)); + if (@preg_match($fullRegex, '') === false) { + $this->addError(ConstraintError::PREGEX_INVALID(), $path, ['pregex' => $pregex]); continue; } foreach ($element as $i => $value) { - if (preg_match($delimiter . $pregex . $delimiter . 'u', $i)) { + if (preg_match($fullRegex, $i)) { $matches[] = $i; $this->checkUndefined($value, $schema ?: new \stdClass(), $path, $i, in_array($i, $this->appliedDefaults)); } @@ -100,7 +91,7 @@ public function validateElement($element, $matches, $schema = null, ?JsonPointer // no additional properties allowed if (!in_array($i, $matches) && $additionalProp === false && $this->inlineSchemaProperty !== $i && !$definition) { - $this->addError($path, 'The property ' . $i . ' is not defined and the definition does not allow additional properties', 'additionalProp'); + $this->addError(ConstraintError::ADDITIONAL_PROPERTIES(), $path, ['property' => $i]); } // additional properties defined @@ -115,7 +106,10 @@ public function validateElement($element, $matches, $schema = null, ?JsonPointer // property requires presence of another $require = $this->getProperty($definition, 'requires'); if ($require && !$this->getProperty($element, $require)) { - $this->addError($path, 'The presence of the property ' . $i . ' requires that ' . $require . ' also be present', 'requires'); + $this->addError(ConstraintError::REQUIRES(), $path, [ + 'property' => $i, + 'requiredProperty' => $require + ]); } $property = $this->getProperty($element, $i, $this->factory->createInstanceFor('undefined')); @@ -160,7 +154,7 @@ protected function &getProperty(&$element, $property, $fallback = null) { if (is_array($element) && (isset($element[$property]) || array_key_exists($property, $element)) /*$this->checkMode == self::CHECK_MODE_TYPE_CAST*/) { return $element[$property]; - } elseif (is_object($element) && property_exists($element, $property)) { + } elseif (is_object($element) && property_exists($element, (string) $property)) { return $element->$property; } @@ -176,16 +170,20 @@ protected function &getProperty(&$element, $property, $fallback = null) */ protected function validateMinMaxConstraint($element, $objectDefinition, ?JsonPointer $path = null) { + if (!$this->getTypeCheck()::isObject($element)) { + return; + } + // Verify minimum number of properties - if (isset($objectDefinition->minProperties) && !is_object($objectDefinition->minProperties)) { - if ($this->getTypeCheck()->propertyCount($element) < $objectDefinition->minProperties) { - $this->addError($path, 'Must contain a minimum of ' . $objectDefinition->minProperties . ' properties', 'minProperties', array('minProperties' => $objectDefinition->minProperties)); + if (isset($objectDefinition->minProperties) && is_int($objectDefinition->minProperties)) { + if ($this->getTypeCheck()->propertyCount($element) < max(0, $objectDefinition->minProperties)) { + $this->addError(ConstraintError::PROPERTIES_MIN(), $path, ['minProperties' => $objectDefinition->minProperties]); } } // Verify maximum number of properties - if (isset($objectDefinition->maxProperties) && !is_object($objectDefinition->maxProperties)) { - if ($this->getTypeCheck()->propertyCount($element) > $objectDefinition->maxProperties) { - $this->addError($path, 'Must contain no more than ' . $objectDefinition->maxProperties . ' properties', 'maxProperties', array('maxProperties' => $objectDefinition->maxProperties)); + if (isset($objectDefinition->maxProperties) && is_int($objectDefinition->maxProperties)) { + if ($this->getTypeCheck()->propertyCount($element) > max(0, $objectDefinition->maxProperties)) { + $this->addError(ConstraintError::PROPERTIES_MAX(), $path, ['maxProperties' => $objectDefinition->maxProperties]); } } } diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/SchemaConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/SchemaConstraint.php index 74979eb03..7852e8518 100644 --- a/justinrainbow/json-schema/src/JsonSchema/Constraints/SchemaConstraint.php +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/SchemaConstraint.php @@ -1,5 +1,7 @@ numErrors() > $initialErrorCount) { - $this->addError($path, 'Schema is not valid', 'schema'); + $this->addError(ConstraintError::INVALID_SCHEMA(), $path); } // restore the initial config diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/StringConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/StringConstraint.php index d6189ce0e..7b811614a 100644 --- a/justinrainbow/json-schema/src/JsonSchema/Constraints/StringConstraint.php +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/StringConstraint.php @@ -1,5 +1,7 @@ maxLength) && $this->strlen($element) > $schema->maxLength) { - $this->addError($path, 'Must be at most ' . $schema->maxLength . ' characters long', 'maxLength', array( + $this->addError(ConstraintError::LENGTH_MAX(), $path, [ 'maxLength' => $schema->maxLength, - )); + ]); } //verify minLength if (isset($schema->minLength) && $this->strlen($element) < $schema->minLength) { - $this->addError($path, 'Must be at least ' . $schema->minLength . ' characters long', 'minLength', array( + $this->addError(ConstraintError::LENGTH_MIN(), $path, [ 'minLength' => $schema->minLength, - )); + ]); } // Verify a regex pattern - if (isset($schema->pattern) && !preg_match('#' . str_replace('#', '\\#', $schema->pattern) . '#u', $element)) { - $this->addError($path, 'Does not match the regex pattern ' . $schema->pattern, 'pattern', array( + if (isset($schema->pattern) && !preg_match(self::jsonPatternToPhpRegex($schema->pattern), $element)) { + $this->addError(ConstraintError::PATTERN(), $path, [ 'pattern' => $schema->pattern, - )); + ]); } $this->checkFormat($element, $schema, $path, $i); diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/TypeCheck/LooseTypeCheck.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/TypeCheck/LooseTypeCheck.php index 984288539..8f9f349b7 100644 --- a/justinrainbow/json-schema/src/JsonSchema/Constraints/TypeCheck/LooseTypeCheck.php +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/TypeCheck/LooseTypeCheck.php @@ -1,5 +1,7 @@ 'an integer', 'number' => 'a number', 'boolean' => 'a boolean', @@ -34,25 +37,33 @@ class TypeConstraint extends Constraint 'null' => 'a null', 'any' => null, // validation of 'any' is always true so is not needed in message wording 0 => null, // validation of a false-y value is always true, so not needed as well - ); + ]; /** * {@inheritdoc} */ - public function check(&$value = null, $schema = null, ?JsonPointer $path = null, $i = null) + public function check(&$value = null, $schema = null, ?JsonPointer $path = null, $i = null): void { $type = isset($schema->type) ? $schema->type : null; $isValid = false; - $wording = array(); + $coerce = $this->factory->getConfig(self::CHECK_MODE_COERCE_TYPES); + $earlyCoerce = $this->factory->getConfig(self::CHECK_MODE_EARLY_COERCE); + $wording = []; if (is_array($type)) { - $this->validateTypesArray($value, $type, $wording, $isValid, $path); + $this->validateTypesArray($value, $type, $wording, $isValid, $path, $coerce && $earlyCoerce); + if (!$isValid && $coerce && !$earlyCoerce) { + $this->validateTypesArray($value, $type, $wording, $isValid, $path, true); + } } elseif (is_object($type)) { $this->checkUndefined($value, $type, $path); return; } else { - $isValid = $this->validateType($value, $type); + $isValid = $this->validateType($value, $type, $coerce && $earlyCoerce); + if (!$isValid && $coerce && !$earlyCoerce) { + $isValid = $this->validateType($value, $type, true); + } } if ($isValid === false) { @@ -60,8 +71,10 @@ public function check(&$value = null, $schema = null, ?JsonPointer $path = null, $this->validateTypeNameWording($type); $wording[] = self::$wording[$type]; } - $this->addError($path, ucwords(gettype($value)) . ' value found, but ' . - $this->implodeWith($wording, ', ', 'or') . ' is required', 'type'); + $this->addError(ConstraintError::TYPE(), $path, [ + 'found' => gettype($value), + 'expected' => $this->implodeWith($wording, ', ', 'or') + ]); } } @@ -70,15 +83,21 @@ public function check(&$value = null, $schema = null, ?JsonPointer $path = null, * of $isValid to true, if at least one $type mateches the type of $value or the value * passed as $isValid is already true. * - * @param mixed $value Value to validate - * @param array $type TypeConstraints to check agains - * @param array $validTypesWording An array of wordings of the valid types of the array $type - * @param bool $isValid The current validation value - * @param $path + * @param mixed $value Value to validate + * @param array $type TypeConstraints to check against + * @param array $validTypesWording An array of wordings of the valid types of the array $type + * @param bool $isValid The current validation value + * @param ?JsonPointer $path + * @param bool $coerce */ - protected function validateTypesArray(&$value, array $type, &$validTypesWording, &$isValid, $path) + protected function validateTypesArray(&$value, array $type, &$validTypesWording, &$isValid, $path, $coerce = false) { foreach ($type as $tp) { + // already valid, so no need to waste cycles looping over everything + if ($isValid) { + return; + } + // $tp can be an object, if it's a schema instead of a simple type, validate it // with a new type constraint if (is_object($tp)) { @@ -95,7 +114,7 @@ protected function validateTypesArray(&$value, array $type, &$validTypesWording, $this->validateTypeNameWording($tp); $validTypesWording[] = self::$wording[$tp]; if (!$isValid) { - $isValid = $this->validateType($value, $tp); + $isValid = $this->validateType($value, $tp, $coerce); } } } @@ -119,7 +138,7 @@ protected function implodeWith(array $elements, $delimiter = ', ', $listEnd = fa } $lastElement = array_slice($elements, -1); $firsElements = join($delimiter, array_slice($elements, 0, -1)); - $implodedElements = array_merge(array($firsElements), $lastElement); + $implodedElements = array_merge([$firsElements], $lastElement); return join(" $listEnd ", $implodedElements); } @@ -154,7 +173,7 @@ protected function validateTypeNameWording($type) * * @return bool */ - protected function validateType(&$value, $type) + protected function validateType(&$value, $type, $coerce = false) { //mostly the case for inline schema if (!$type) { @@ -170,11 +189,13 @@ protected function validateType(&$value, $type) } if ('array' === $type) { + if ($coerce) { + $value = $this->toArray($value); + } + return $this->getTypeCheck()->isArray($value); } - $coerce = $this->factory->getConfig(Constraint::CHECK_MODE_COERCE_TYPES); - if ('integer' === $type) { if ($coerce) { $value = $this->toInteger($value); @@ -200,14 +221,18 @@ protected function validateType(&$value, $type) } if ('string' === $type) { - return is_string($value); - } + if ($coerce) { + $value = $this->toString($value); + } - if ('email' === $type) { return is_string($value); } if ('null' === $type) { + if ($coerce) { + $value = $this->toNull($value); + } + return is_null($value); } @@ -217,25 +242,27 @@ protected function validateType(&$value, $type) /** * Converts a value to boolean. For example, "true" becomes true. * - * @param $value The value to convert to boolean + * @param mixed $value The value to convert to boolean * * @return bool|mixed */ protected function toBoolean($value) { - if ($value === 'true') { + if ($value === 1 || $value === 'true') { return true; } - - if ($value === 'false') { + if (is_null($value) || $value === 0 || $value === 'false') { return false; } + if ($this->getTypeCheck()->isArray($value) && count($value) === 1) { + return $this->toBoolean(reset($value)); + } return $value; } /** - * Converts a numeric string to a number. For example, "4" becomes 4. + * Converts a value to a number. For example, "4.5" becomes 4.5. * * @param mixed $value the value to convert to a number * @@ -246,14 +273,91 @@ protected function toNumber($value) if (is_numeric($value)) { return $value + 0; // cast to number } + if (is_bool($value) || is_null($value)) { + return (int) $value; + } + if ($this->getTypeCheck()->isArray($value) && count($value) === 1) { + return $this->toNumber(reset($value)); + } return $value; } + /** + * Converts a value to an integer. For example, "4" becomes 4. + * + * @param mixed $value + * + * @return int|mixed + */ protected function toInteger($value) { - if (is_numeric($value) && (int) $value == $value) { - return (int) $value; // cast to number + $numberValue = $this->toNumber($value); + if (is_numeric($numberValue) && (int) $numberValue == $numberValue) { + return (int) $numberValue; // cast to number + } + + return $value; + } + + /** + * Converts a value to an array containing that value. For example, [4] becomes 4. + * + * @param mixed $value + * + * @return array|mixed + */ + protected function toArray($value) + { + if (is_scalar($value) || is_null($value)) { + return [$value]; + } + + return $value; + } + + /** + * Convert a value to a string representation of that value. For example, null becomes "". + * + * @param mixed $value + * + * @return string|mixed + */ + protected function toString($value) + { + if (is_numeric($value)) { + return "$value"; + } + if ($value === true) { + return 'true'; + } + if ($value === false) { + return 'false'; + } + if (is_null($value)) { + return ''; + } + if ($this->getTypeCheck()->isArray($value) && count($value) === 1) { + return $this->toString(reset($value)); + } + + return $value; + } + + /** + * Convert a value to a null. For example, 0 becomes null. + * + * @param mixed $value + * + * @return null|mixed + */ + protected function toNull($value) + { + if ($value === 0 || $value === false || $value === '') { + return null; + } + if ($this->getTypeCheck()->isArray($value) && count($value) === 1) { + return $this->toNull(reset($value)); } return $value; diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/UndefinedConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/UndefinedConstraint.php index 26e45f785..1ea0a7bc1 100644 --- a/justinrainbow/json-schema/src/JsonSchema/Constraints/UndefinedConstraint.php +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/UndefinedConstraint.php @@ -1,43 +1,34 @@ - * @author Bruno Prieto Reis - */ #[\AllowDynamicProperties] class UndefinedConstraint extends Constraint { /** - * @var array List of properties to which a default value has been applied + * @var list List of properties to which a default value has been applied */ - protected $appliedDefaults = array(); + protected $appliedDefaults = []; /** * {@inheritdoc} - */ - public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null, $fromDefault = false) + * */ + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null, bool $fromDefault = false): void { if (is_null($schema) || !is_object($schema)) { return; } - $path = $this->incrementPath($path ?: new JsonPointer(''), $i); + $path = $this->incrementPath($path, $i); if ($fromDefault) { $path->setFromDefault(); } @@ -68,9 +59,10 @@ public function validateTypes(&$value, $schema, JsonPointer $path, $i = null) } // check object - if (LooseTypeCheck::isObject($value)) { // object processing should always be run on assoc arrays, - // so use LooseTypeCheck here even if CHECK_MODE_TYPE_CAST - // is not set (i.e. don't use $this->getTypeCheck() here). + if (LooseTypeCheck::isObject($value)) { + // object processing should always be run on assoc arrays, + // so use LooseTypeCheck here even if CHECK_MODE_TYPE_CAST + // is not set (i.e. don't use $this->getTypeCheck() here). $this->checkObject( $value, $schema, @@ -96,6 +88,11 @@ public function validateTypes(&$value, $schema, JsonPointer $path, $i = null) if (isset($schema->enum)) { $this->checkEnum($value, $schema, $path, $i); } + + // check const + if (isset($schema->const)) { + $this->checkConst($value, $schema, $path, $i); + } } /** @@ -134,9 +131,10 @@ protected function validateCommonProperties(&$value, $schema, JsonPointer $path, foreach ($schema->required as $required) { if (!$this->getTypeCheck()->propertyExists($value, $required)) { $this->addError( - $this->incrementPath($path ?: new JsonPointer(''), $required), - 'The property ' . $required . ' is required', - 'required' + ConstraintError::REQUIRED(), + $this->incrementPath($path, $required), [ + 'property' => $required + ] ); } } @@ -145,11 +143,7 @@ protected function validateCommonProperties(&$value, $schema, JsonPointer $path, if ($schema->required && $value instanceof self) { $propertyPaths = $path->getPropertyPaths(); $propertyName = end($propertyPaths); - $this->addError( - $path, - 'The property ' . $propertyName . ' is required', - 'required' - ); + $this->addError(ConstraintError::REQUIRED(), $path, ['property' => $propertyName]); } } else { // if the value is both undefined and not required, skip remaining checks @@ -175,7 +169,7 @@ protected function validateCommonProperties(&$value, $schema, JsonPointer $path, // if no new errors were raised it must be a disallowed value if (count($this->getErrors()) == count($initErrors)) { - $this->addError($path, 'Disallowed value was matched', 'disallow'); + $this->addError(ConstraintError::DISALLOW(), $path); } else { $this->errors = $initErrors; } @@ -187,7 +181,7 @@ protected function validateCommonProperties(&$value, $schema, JsonPointer $path, // if no new errors were raised then the instance validated against the "not" schema if (count($this->getErrors()) == count($initErrors)) { - $this->addError($path, 'Matched a schema which it should not', 'not'); + $this->addError(ConstraintError::NOT(), $path); } else { $this->errors = $initErrors; } @@ -238,7 +232,7 @@ private function shouldApplyDefaultValue($requiredOnly, $schema, $name = null, $ * @param mixed $schema * @param JsonPointer $path */ - protected function applyDefaultValues(&$value, $schema, $path) + protected function applyDefaultValues(&$value, $schema, $path): void { // only apply defaults if feature is enabled if (!$this->factory->getConfig(self::CHECK_MODE_APPLY_DEFAULTS)) { @@ -246,7 +240,7 @@ protected function applyDefaultValues(&$value, $schema, $path) } // apply defaults if appropriate - $requiredOnly = $this->factory->getConfig(self::CHECK_MODE_ONLY_REQUIRED_DEFAULTS); + $requiredOnly = (bool) $this->factory->getConfig(self::CHECK_MODE_ONLY_REQUIRED_DEFAULTS); if (isset($schema->properties) && LooseTypeCheck::isObject($value)) { // $value is an object or assoc array, and properties are defined - treat as an object foreach ($schema->properties as $currentProperty => $propertyDefinition) { @@ -266,7 +260,7 @@ protected function applyDefaultValues(&$value, $schema, $path) } } } elseif (isset($schema->items) && LooseTypeCheck::isArray($value)) { - $items = array(); + $items = []; if (LooseTypeCheck::isArray($schema->items)) { $items = $schema->items; } elseif (isset($schema->minItems) && count($value) < $schema->minItems) { @@ -320,19 +314,22 @@ protected function validateOfProperties(&$value, $schema, JsonPointer $path, $i $isValid = $isValid && (count($this->getErrors()) == count($initErrors)); } if (!$isValid) { - $this->addError($path, 'Failed to match all schemas', 'allOf'); + $this->addError(ConstraintError::ALL_OF(), $path); } } if (isset($schema->anyOf)) { $isValid = false; $startErrors = $this->getErrors(); - $caughtException = null; + $coerceOrDefaults = $this->factory->getConfig(self::CHECK_MODE_COERCE_TYPES | self::CHECK_MODE_APPLY_DEFAULTS); + foreach ($schema->anyOf as $anyOf) { $initErrors = $this->getErrors(); try { - $this->checkUndefined($value, $anyOf, $path, $i); - if ($isValid = (count($this->getErrors()) == count($initErrors))) { + $anyOfValue = $coerceOrDefaults ? DeepCopy::copyOf($value) : $value; + $this->checkUndefined($anyOfValue, $anyOf, $path, $i); + if ($isValid = (count($this->getErrors()) === count($initErrors))) { + $value = $anyOfValue; break; } } catch (ValidationException $e) { @@ -340,22 +337,26 @@ protected function validateOfProperties(&$value, $schema, JsonPointer $path, $i } } if (!$isValid) { - $this->addError($path, 'Failed to match at least one schema', 'anyOf'); + $this->addError(ConstraintError::ANY_OF(), $path); } else { $this->errors = $startErrors; } } if (isset($schema->oneOf)) { - $allErrors = array(); - $matchedSchemas = 0; + $allErrors = []; + $matchedSchemas = []; $startErrors = $this->getErrors(); + $coerceOrDefaults = $this->factory->getConfig(self::CHECK_MODE_COERCE_TYPES | self::CHECK_MODE_APPLY_DEFAULTS); + foreach ($schema->oneOf as $oneOf) { try { - $this->errors = array(); - $this->checkUndefined($value, $oneOf, $path, $i); - if (count($this->getErrors()) == 0) { - $matchedSchemas++; + $this->errors = []; + + $oneOfValue = $coerceOrDefaults ? DeepCopy::copyOf($value) : $value; + $this->checkUndefined($oneOfValue, $oneOf, $path, $i); + if (count($this->getErrors()) === 0) { + $matchedSchemas[] = ['schema' => $oneOf, 'value' => $oneOfValue]; } $allErrors = array_merge($allErrors, array_values($this->getErrors())); } catch (ValidationException $e) { @@ -363,11 +364,12 @@ protected function validateOfProperties(&$value, $schema, JsonPointer $path, $i // other schema options in the OneOf field. } } - if ($matchedSchemas !== 1) { + if (count($matchedSchemas) !== 1) { $this->addErrors(array_merge($allErrors, $startErrors)); - $this->addError($path, 'Failed to match exactly one schema', 'oneOf'); + $this->addError(ConstraintError::ONE_OF(), $path); } else { $this->errors = $startErrors; + $value = $matchedSchemas[0]['value']; } } } @@ -387,13 +389,19 @@ protected function validateDependencies($value, $dependencies, JsonPointer $path if (is_string($dependency)) { // Draft 3 string is allowed - e.g. "dependencies": {"bar": "foo"} if (!$this->getTypeCheck()->propertyExists($value, $dependency)) { - $this->addError($path, "$key depends on $dependency and $dependency is missing", 'dependencies'); + $this->addError(ConstraintError::DEPENDENCIES(), $path, [ + 'key' => $key, + 'dependency' => $dependency + ]); } } elseif (is_array($dependency)) { // Draft 4 must be an array - e.g. "dependencies": {"bar": ["foo"]} foreach ($dependency as $d) { if (!$this->getTypeCheck()->propertyExists($value, $d)) { - $this->addError($path, "$key depends on $d and $d is missing", 'dependencies'); + $this->addError(ConstraintError::DEPENDENCIES(), $path, [ + 'key' => $key, + 'dependency' => $dependency + ]); } } } elseif (is_object($dependency)) { diff --git a/justinrainbow/json-schema/src/JsonSchema/Entity/JsonPointer.php b/justinrainbow/json-schema/src/JsonSchema/Entity/JsonPointer.php index fcaf5b8d7..e674fa6f8 100644 --- a/justinrainbow/json-schema/src/JsonSchema/Entity/JsonPointer.php +++ b/justinrainbow/json-schema/src/JsonSchema/Entity/JsonPointer.php @@ -1,5 +1,7 @@ decodePath($path); if (is_string($path) && '' !== $path) { @@ -71,7 +73,7 @@ private function decodePropertyPaths($propertyPathString) private function encodePropertyPaths() { return array_map( - array($this, 'encodePath'), + [$this, 'encodePath'], $this->getPropertyPaths() ); } @@ -83,7 +85,7 @@ private function encodePropertyPaths() */ private function decodePath($path) { - return strtr($path, array('~1' => '/', '~0' => '~', '%25' => '%')); + return strtr($path, ['~1' => '/', '~0' => '~', '%25' => '%']); } /** @@ -93,7 +95,7 @@ private function decodePath($path) */ private function encodePath($path) { - return strtr($path, array('/' => '~1', '~' => '~0', '%' => '%25')); + return strtr($path, ['/' => '~1', '~' => '~0', '%' => '%25']); } /** @@ -120,7 +122,7 @@ public function getPropertyPaths() public function withPropertyPaths(array $propertyPaths) { $new = clone $this; - $new->propertyPaths = $propertyPaths; + $new->propertyPaths = array_map(function ($p): string { return (string) $p; }, $propertyPaths); return $new; } @@ -144,7 +146,7 @@ public function __toString() /** * Mark the value at this path as being set from a schema default */ - public function setFromDefault() + public function setFromDefault(): void { $this->fromDefault = true; } diff --git a/justinrainbow/json-schema/src/JsonSchema/Enum.php b/justinrainbow/json-schema/src/JsonSchema/Enum.php new file mode 100644 index 000000000..ef8cb2856 --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Enum.php @@ -0,0 +1,9 @@ +initialize(); @@ -49,7 +52,7 @@ public function current() /** * {@inheritdoc} */ - public function next() + public function next(): void { $this->initialize(); $this->position++; @@ -58,7 +61,7 @@ public function next() /** * {@inheritdoc} */ - public function key() + public function key(): int { $this->initialize(); @@ -68,7 +71,7 @@ public function key() /** * {@inheritdoc} */ - public function valid() + public function valid(): bool { $this->initialize(); @@ -78,7 +81,7 @@ public function valid() /** * {@inheritdoc} */ - public function rewind() + public function rewind(): void { $this->initialize(); $this->position = 0; @@ -87,7 +90,7 @@ public function rewind() /** * {@inheritdoc} */ - public function count() + public function count(): int { $this->initialize(); @@ -112,7 +115,7 @@ private function initialize() */ private function buildDataFromObject($object) { - $result = array(); + $result = []; $stack = new \SplStack(); $stack->push($object); @@ -141,7 +144,7 @@ private function buildDataFromObject($object) private function getDataFromItem($item) { if (!is_object($item) && !is_array($item)) { - return array(); + return []; } return is_object($item) ? get_object_vars($item) : $item; diff --git a/justinrainbow/json-schema/src/JsonSchema/Rfc3339.php b/justinrainbow/json-schema/src/JsonSchema/Rfc3339.php index adca581a4..3524f681f 100644 --- a/justinrainbow/json-schema/src/JsonSchema/Rfc3339.php +++ b/justinrainbow/json-schema/src/JsonSchema/Rfc3339.php @@ -1,10 +1,12 @@ id == 'http://json-schema.org/draft-04/schema#') { + if ($schema->id === 'http://json-schema.org/draft-04/schema#') { $schema->properties->id->format = 'uri-reference'; - } elseif ($schema->id == 'http://json-schema.org/draft-03/schema#') { + } elseif ($schema->id === 'http://json-schema.org/draft-03/schema#') { $schema->properties->id->format = 'uri-reference'; $schema->properties->{'$ref'}->format = 'uri-reference'; } } + $this->scanForSubschemas($schema, $id); + // resolve references $this->expandRefs($schema, $id); @@ -77,39 +81,44 @@ public function addSchema($id, $schema = null) /** * Recursively resolve all references against the provided base * - * @param mixed $schema - * @param string $base + * @param mixed $schema */ - private function expandRefs(&$schema, $base = null) + private function expandRefs(&$schema, ?string $parentId = null): void { if (!is_object($schema)) { if (is_array($schema)) { foreach ($schema as &$member) { - $this->expandRefs($member, $base); + $this->expandRefs($member, $parentId); } } return; } - if (property_exists($schema, 'id') && is_string($schema->id) && $base != $schema->id) { - $base = $this->uriResolver->resolve($schema->id, $base); - } - if (property_exists($schema, '$ref') && is_string($schema->{'$ref'})) { - $refPointer = new JsonPointer($this->uriResolver->resolve($schema->{'$ref'}, $base)); + $refPointer = new JsonPointer($this->uriResolver->resolve($schema->{'$ref'}, $parentId)); $schema->{'$ref'} = (string) $refPointer; } - foreach ($schema as &$member) { - $this->expandRefs($member, $base); + foreach ($schema as $propertyName => &$member) { + if (in_array($propertyName, ['enum', 'const'])) { + // Enum and const don't allow $ref as a keyword, see https://github.com/json-schema-org/JSON-Schema-Test-Suite/pull/445 + continue; + } + + $childId = $parentId; + if (property_exists($schema, 'id') && is_string($schema->id) && $childId !== $schema->id) { + $childId = $this->uriResolver->resolve($schema->id, $childId); + } + + $this->expandRefs($member, $childId); } } /** * {@inheritdoc} */ - public function getSchema($id) + public function getSchema(string $id) { if (!array_key_exists($id, $this->schemas)) { $this->addSchema($id); @@ -121,7 +130,7 @@ public function getSchema($id) /** * {@inheritdoc} */ - public function resolveRef($ref) + public function resolveRef(string $ref, $resolveStack = []) { $jsonPointer = new JsonPointer($ref); @@ -138,9 +147,9 @@ public function resolveRef($ref) $refSchema = $this->getSchema($fileName); foreach ($jsonPointer->getPropertyPaths() as $path) { if (is_object($refSchema) && property_exists($refSchema, $path)) { - $refSchema = $this->resolveRefSchema($refSchema->{$path}); + $refSchema = $this->resolveRefSchema($refSchema->{$path}, $resolveStack); } elseif (is_array($refSchema) && array_key_exists($path, $refSchema)) { - $refSchema = $this->resolveRefSchema($refSchema[$path]); + $refSchema = $this->resolveRefSchema($refSchema[$path], $resolveStack); } else { throw new UnresolvableJsonPointerException(sprintf( 'File: %s is found, but could not resolve fragment: %s', @@ -156,14 +165,48 @@ public function resolveRef($ref) /** * {@inheritdoc} */ - public function resolveRefSchema($refSchema) + public function resolveRefSchema($refSchema, $resolveStack = []) { if (is_object($refSchema) && property_exists($refSchema, '$ref') && is_string($refSchema->{'$ref'})) { - $newSchema = $this->resolveRef($refSchema->{'$ref'}); - $refSchema = (object) (get_object_vars($refSchema) + get_object_vars($newSchema)); - unset($refSchema->{'$ref'}); + if (in_array($refSchema, $resolveStack, true)) { + throw new UnresolvableJsonPointerException(sprintf( + 'Dereferencing a pointer to %s results in an infinite loop', + $refSchema->{'$ref'} + )); + } + $resolveStack[] = $refSchema; + + return $this->resolveRef($refSchema->{'$ref'}, $resolveStack); } return $refSchema; } + + /** + * @param mixed $schema + */ + private function scanForSubschemas($schema, string $parentId): void + { + if (!$schema instanceof \stdClass && !is_array($schema)) { + return; + } + + foreach ($schema as $propertyName => $potentialSubSchema) { + if (!is_object($potentialSubSchema)) { + continue; + } + + if (property_exists($potentialSubSchema, 'id') && is_string($potentialSubSchema->id) && property_exists($potentialSubSchema, 'type')) { + // Enum and const don't allow id as a keyword, see https://github.com/json-schema-org/JSON-Schema-Test-Suite/pull/471 + if (in_array($propertyName, ['enum', 'const'])) { + continue; + } + + // Found sub schema + $this->addSchema($this->uriResolver->resolve($potentialSubSchema->id, $parentId), $potentialSubSchema); + } + + $this->scanForSubschemas($potentialSubSchema, $parentId); + } + } } diff --git a/justinrainbow/json-schema/src/JsonSchema/SchemaStorageInterface.php b/justinrainbow/json-schema/src/JsonSchema/SchemaStorageInterface.php index ddaf6633e..f625cdd28 100644 --- a/justinrainbow/json-schema/src/JsonSchema/SchemaStorageInterface.php +++ b/justinrainbow/json-schema/src/JsonSchema/SchemaStorageInterface.php @@ -1,5 +1,7 @@ $left + * @param array $right + */ + private static function isArrayEqual(array $left, array $right): bool + { + if (count($left) !== count($right)) { + return false; + } + foreach ($left as $key => $value) { + if (!array_key_exists($key, $right)) { + return false; + } + + if (!self::isEqual($value, $right[$key])) { + return false; + } + } + + return true; + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Tool/DeepCopy.php b/justinrainbow/json-schema/src/JsonSchema/Tool/DeepCopy.php new file mode 100644 index 000000000..8b09156fc --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Tool/DeepCopy.php @@ -0,0 +1,38 @@ + 65535)) { + return false; + } + + // Validate path (reject illegal characters: < > { } | \ ^ `) + if (!empty($matches[6]) && preg_match('/[<>{}|\\\^`]/', $matches[6])) { + return false; + } + + return true; + } + + // If not hierarchical, check non-hierarchical URIs + if (preg_match($nonHierarchicalPattern, $uri, $matches) === 1) { + $scheme = strtolower($matches[1]); // Extract the scheme + + // Special case: `mailto:` must contain a **valid email address** + if ($scheme === 'mailto') { + return preg_match($emailPattern, $matches[2]) === 1; + } + + return true; // Valid non-hierarchical URI + } + + return false; + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Uri/Retrievers/AbstractRetriever.php b/justinrainbow/json-schema/src/JsonSchema/Uri/Retrievers/AbstractRetriever.php index 9ab24b95f..f4ae718aa 100644 --- a/justinrainbow/json-schema/src/JsonSchema/Uri/Retrievers/AbstractRetriever.php +++ b/justinrainbow/json-schema/src/JsonSchema/Uri/Retrievers/AbstractRetriever.php @@ -1,4 +1,7 @@ contentType = self::getContentTypeMatchInHeader($header)) { return true; } diff --git a/justinrainbow/json-schema/src/JsonSchema/Uri/Retrievers/PredefinedArray.php b/justinrainbow/json-schema/src/JsonSchema/Uri/Retrievers/PredefinedArray.php index a663d3417..116930022 100644 --- a/justinrainbow/json-schema/src/JsonSchema/Uri/Retrievers/PredefinedArray.php +++ b/justinrainbow/json-schema/src/JsonSchema/Uri/Retrievers/PredefinedArray.php @@ -1,5 +1,7 @@ $match[2], 'authority' => $match[4], 'path' => $match[5] - ); + ]; } if (7 < count($match)) { $components['query'] = $match[7]; @@ -125,25 +127,37 @@ public function resolve($uri, $baseUri = null) public static function combineRelativePathWithBasePath($relativePath, $basePath) { $relativePath = self::normalizePath($relativePath); - if ($relativePath == '') { + if (!$relativePath) { return $basePath; } - if ($relativePath[0] == '/') { + if ($relativePath[0] === '/') { return $relativePath; } - - $basePathSegments = explode('/', $basePath); - - preg_match('|^/?(\.\./(?:\./)*)*|', $relativePath, $match); - $numLevelUp = strlen($match[0]) /3 + 1; - if ($numLevelUp >= count($basePathSegments)) { + if (!$basePath) { throw new UriResolverException(sprintf("Unable to resolve URI '%s' from base '%s'", $relativePath, $basePath)); } - $basePathSegments = array_slice($basePathSegments, 0, -$numLevelUp); - $path = preg_replace('|^/?(\.\./(\./)*)*|', '', $relativePath); + $dirname = $basePath[strlen($basePath) - 1] === '/' ? $basePath : dirname($basePath); + $combined = rtrim($dirname, '/') . '/' . ltrim($relativePath, '/'); + $combinedSegments = explode('/', $combined); + $collapsedSegments = []; + while ($combinedSegments) { + $segment = array_shift($combinedSegments); + if ($segment === '..') { + if (count($collapsedSegments) <= 1) { + // Do not remove the top level (domain) + // This is not ideal - the domain should not be part of the path here. parse() and generate() + // should handle the "domain" separately, like the schema. + // Then the if-condition here would be `if (!$collapsedSegments) {`. + throw new UriResolverException(sprintf("Unable to resolve URI '%s' from base '%s'", $relativePath, $basePath)); + } + array_pop($collapsedSegments); + } else { + $collapsedSegments[] = $segment; + } + } - return implode('/', $basePathSegments) . '/' . $path; + return implode('/', $collapsedSegments); } /** diff --git a/justinrainbow/json-schema/src/JsonSchema/Uri/UriRetriever.php b/justinrainbow/json-schema/src/JsonSchema/Uri/UriRetriever.php index 41147a2a0..4094cccc3 100644 --- a/justinrainbow/json-schema/src/JsonSchema/Uri/UriRetriever.php +++ b/justinrainbow/json-schema/src/JsonSchema/Uri/UriRetriever.php @@ -1,5 +1,7 @@ 'package://dist/schema/json-schema-draft-$1.json' - ); + ]; /** * @var array A list of endpoints for media type check exclusion */ - protected $allowedInvalidContentTypeEndpoints = array( + protected $allowedInvalidContentTypeEndpoints = [ 'http://json-schema.org/', 'https://json-schema.org/' - ); + ]; /** * @var null|UriRetrieverInterface @@ -50,7 +52,7 @@ class UriRetriever implements BaseUriRetrieverInterface * * @see loadSchema */ - private $schemaCache = array(); + private $schemaCache = []; /** * Adds an endpoint to the media type validation exclusion list @@ -79,12 +81,12 @@ public function confirmMediaType($uriRetriever, $uri) return; } - if (in_array($contentType, array(Validator::SCHEMA_MEDIA_TYPE, 'application/json'))) { + if (in_array($contentType, [Validator::SCHEMA_MEDIA_TYPE, 'application/json'])) { return; } foreach ($this->allowedInvalidContentTypeEndpoints as $endpoint) { - if (strpos($uri, $endpoint) === 0) { + if (!\is_null($uri) && strpos($uri, $endpoint) === 0) { return true; } } @@ -243,13 +245,13 @@ public function parse($uri) { preg_match('|^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?|', $uri, $match); - $components = array(); + $components = []; if (5 < count($match)) { - $components = array( + $components = [ 'scheme' => $match[2], 'authority' => $match[4], 'path' => $match[5] - ); + ]; } if (7 < count($match)) { diff --git a/justinrainbow/json-schema/src/JsonSchema/UriResolverInterface.php b/justinrainbow/json-schema/src/JsonSchema/UriResolverInterface.php index 4eb50c03c..e80e2be73 100644 --- a/justinrainbow/json-schema/src/JsonSchema/UriResolverInterface.php +++ b/justinrainbow/json-schema/src/JsonSchema/UriResolverInterface.php @@ -1,5 +1,7 @@ $checkMode + * @phpstan-return int-mask-of */ - public function validate(&$value, $schema = null, $checkMode = null) + public function validate(&$value, $schema = null, ?int $checkMode = null): int { - // make sure $schema is an object - if (is_array($schema)) { - $schema = self::arrayToObjectRecursive($schema); - } + // reset errors prior to validation + $this->reset(); // set checkMode $initialCheckMode = $this->factory->getConfig(); @@ -50,10 +57,9 @@ public function validate(&$value, $schema = null, $checkMode = null) } // add provided schema to SchemaStorage with internal URI to allow internal $ref resolution - if (is_object($schema) && property_exists($schema, 'id')) { - $schemaURI = $schema->id; - } else { - $schemaURI = SchemaStorage::INTERNAL_PROVIDED_SCHEMA_URI; + $schemaURI = SchemaStorage::INTERNAL_PROVIDED_SCHEMA_URI; + if (LooseTypeCheck::propertyExists($schema, 'id')) { + $schemaURI = LooseTypeCheck::propertyGet($schema, 'id'); } $this->factory->getSchemaStorage()->addSchema($schemaURI, $schema); @@ -72,16 +78,30 @@ public function validate(&$value, $schema = null, $checkMode = null) /** * Alias to validate(), to maintain backwards-compatibility with the previous API + * + * @deprecated since 6.0.0, use Validator::validate() instead, to be removed in 7.0 + * + * @param mixed $value + * @param mixed $schema + * + * @phpstan-return int-mask-of */ - public function check($value, $schema) + public function check($value, $schema): int { return $this->validate($value, $schema); } /** * Alias to validate(), to maintain backwards-compatibility with the previous API + * + * @deprecated since 6.0.0, use Validator::validate() instead, to be removed in 7.0 + * + * @param mixed $value + * @param mixed $schema + * + * @phpstan-return int-mask-of */ - public function coerce(&$value, $schema) + public function coerce(&$value, $schema): int { return $this->validate($value, $schema, Constraint::CHECK_MODE_COERCE_TYPES); } diff --git a/marc-mabe/php-enum/LICENSE.txt b/marc-mabe/php-enum/LICENSE.txt new file mode 100644 index 000000000..02bcecec7 --- /dev/null +++ b/marc-mabe/php-enum/LICENSE.txt @@ -0,0 +1,27 @@ +Copyright (c) 2020, Marc Bennewitz +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of the organisation nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/marc-mabe/php-enum/src/Enum.php b/marc-mabe/php-enum/src/Enum.php new file mode 100644 index 000000000..d602d8bc6 --- /dev/null +++ b/marc-mabe/php-enum/src/Enum.php @@ -0,0 +1,484 @@ + + */ + private $value; + + /** + * The ordinal number of the enumerator + * + * @var null|int + */ + private $ordinal; + + /** + * A map of enumerator names and values by enumeration class + * + * @var array, array>> + */ + private static $constants = []; + + /** + * A List of available enumerator names by enumeration class + * + * @var array, string[]> + */ + private static $names = []; + + /** + * A map of enumerator names and instances by enumeration class + * + * @var array, array> + */ + private static $instances = []; + + /** + * Constructor + * + * @param null|bool|int|float|string|array $value The value of the enumerator + * @param int|null $ordinal The ordinal number of the enumerator + */ + final private function __construct($value, $ordinal = null) + { + $this->value = $value; + $this->ordinal = $ordinal; + } + + /** + * Get the name of the enumerator + * + * @return string + * @see getName() + */ + public function __toString(): string + { + return $this->getName(); + } + + /** + * @throws LogicException Enums are not cloneable + * because instances are implemented as singletons + */ + final public function __clone() + { + throw new LogicException('Enums are not cloneable'); + } + + /** + * @throws LogicException Serialization is not supported by default in this pseudo-enum implementation + * + * @psalm-return never-return + */ + final public function __sleep() + { + throw new LogicException('Serialization is not supported by default in this pseudo-enum implementation'); + } + + /** + * @throws LogicException Serialization is not supported by default in this pseudo-enum implementation + * + * @psalm-return never-return + */ + final public function __wakeup() + { + throw new LogicException('Serialization is not supported by default in this pseudo-enum implementation'); + } + + /** + * Get the value of the enumerator + * + * @return null|bool|int|float|string|array + */ + final public function getValue() + { + return $this->value; + } + + /** + * Get the name of the enumerator + * + * @return string + * + * @phpstan-return string + * @psalm-return non-empty-string + */ + final public function getName() + { + return self::$names[static::class][$this->ordinal ?? $this->getOrdinal()]; + } + + /** + * Get the ordinal number of the enumerator + * + * @return int + */ + final public function getOrdinal() + { + if ($this->ordinal === null) { + $ordinal = 0; + $value = $this->value; + $constants = self::$constants[static::class] ?? static::getConstants(); + foreach ($constants as $constValue) { + if ($value === $constValue) { + break; + } + ++$ordinal; + } + + $this->ordinal = $ordinal; + } + + return $this->ordinal; + } + + /** + * Compare this enumerator against another and check if it's the same. + * + * @param static|null|bool|int|float|string|array $enumerator An enumerator object or value + * @return bool + */ + final public function is($enumerator) + { + return $this === $enumerator || $this->value === $enumerator + + // The following additional conditions are required only because of the issue of serializable singletons + || ($enumerator instanceof static + && \get_class($enumerator) === static::class + && $enumerator->value === $this->value + ); + } + + /** + * Get an enumerator instance of the given enumerator value or instance + * + * @param static|null|bool|int|float|string|array $enumerator An enumerator object or value + * @return static + * @throws InvalidArgumentException On an unknown or invalid value + * @throws LogicException On ambiguous constant values + * + * @psalm-pure + */ + final public static function get($enumerator) + { + if ($enumerator instanceof static) { + if (\get_class($enumerator) !== static::class) { + throw new InvalidArgumentException(sprintf( + 'Invalid value of type %s for enumeration %s', + \get_class($enumerator), + static::class + )); + } + + return $enumerator; + } + + return static::byValue($enumerator); + } + + /** + * Get an enumerator instance by the given value + * + * @param null|bool|int|float|string|array $value Enumerator value + * @return static + * @throws InvalidArgumentException On an unknown or invalid value + * @throws LogicException On ambiguous constant values + * + * @psalm-pure + */ + final public static function byValue($value) + { + /** @var mixed $value */ + + $constants = self::$constants[static::class] ?? static::getConstants(); + + $name = \array_search($value, $constants, true); + if ($name === false) { + throw new InvalidArgumentException(sprintf( + 'Unknown value %s for enumeration %s', + \is_scalar($value) + ? \var_export($value, true) + : 'of type ' . (\is_object($value) ? \get_class($value) : \gettype($value)), + static::class + )); + } + + /** @var static $instance */ + $instance = self::$instances[static::class][$name] + ?? self::$instances[static::class][$name] = new static($constants[$name]); + + return $instance; + } + + /** + * Get an enumerator instance by the given name + * + * @param string $name The name of the enumerator + * @return static + * @throws InvalidArgumentException On an invalid or unknown name + * @throws LogicException On ambiguous values + * + * @psalm-pure + */ + final public static function byName(string $name) + { + if (isset(self::$instances[static::class][$name])) { + /** @var static $instance */ + $instance = self::$instances[static::class][$name]; + return $instance; + } + + $const = static::class . "::{$name}"; + if (!\defined($const)) { + throw new InvalidArgumentException("{$const} not defined"); + } + + assert( + self::noAmbiguousValues(static::getConstants()), + 'Ambiguous enumerator values detected for ' . static::class + ); + + /** @var array|bool|float|int|string|null $value */ + $value = \constant($const); + return self::$instances[static::class][$name] = new static($value); + } + + /** + * Get an enumeration instance by the given ordinal number + * + * @param int $ordinal The ordinal number of the enumerator + * @return static + * @throws InvalidArgumentException On an invalid ordinal number + * @throws LogicException On ambiguous values + * + * @psalm-pure + */ + final public static function byOrdinal(int $ordinal) + { + $constants = self::$constants[static::class] ?? static::getConstants(); + + if (!isset(self::$names[static::class][$ordinal])) { + throw new InvalidArgumentException(\sprintf( + 'Invalid ordinal number %s, must between 0 and %s', + $ordinal, + \count(self::$names[static::class]) - 1 + )); + } + + $name = self::$names[static::class][$ordinal]; + + /** @var static $instance */ + $instance = self::$instances[static::class][$name] + ?? self::$instances[static::class][$name] = new static($constants[$name], $ordinal); + + return $instance; + } + + /** + * Get a list of enumerator instances ordered by ordinal number + * + * @return static[] + * + * @phpstan-return array + * @psalm-return list + * @psalm-pure + */ + final public static function getEnumerators() + { + if (!isset(self::$names[static::class])) { + static::getConstants(); + } + + /** @var callable $byNameFn */ + $byNameFn = [static::class, 'byName']; + return \array_map($byNameFn, self::$names[static::class]); + } + + /** + * Get a list of enumerator values ordered by ordinal number + * + * @return (null|bool|int|float|string|array)[] + * + * @phpstan-return array> + * @psalm-return list + * @psalm-pure + */ + final public static function getValues() + { + return \array_values(self::$constants[static::class] ?? static::getConstants()); + } + + /** + * Get a list of enumerator names ordered by ordinal number + * + * @return string[] + * + * @phpstan-return array + * @psalm-return list + * @psalm-pure + */ + final public static function getNames() + { + if (!isset(self::$names[static::class])) { + static::getConstants(); + } + return self::$names[static::class]; + } + + /** + * Get a list of enumerator ordinal numbers + * + * @return int[] + * + * @phpstan-return array + * @psalm-return list + * @psalm-pure + */ + final public static function getOrdinals() + { + $count = \count(self::$constants[static::class] ?? static::getConstants()); + return $count ? \range(0, $count - 1) : []; + } + + /** + * Get all available constants of the called class + * + * @return (null|bool|int|float|string|array)[] + * @throws LogicException On ambiguous constant values + * + * @phpstan-return array> + * @psalm-return array + * @psalm-pure + */ + final public static function getConstants() + { + if (isset(self::$constants[static::class])) { + return self::$constants[static::class]; + } + + $reflection = new ReflectionClass(static::class); + $constants = []; + + do { + $scopeConstants = []; + // Enumerators must be defined as public class constants + foreach ($reflection->getReflectionConstants() as $reflConstant) { + if ($reflConstant->isPublic()) { + $scopeConstants[ $reflConstant->getName() ] = $reflConstant->getValue(); + } + } + + $constants = $scopeConstants + $constants; + } while (($reflection = $reflection->getParentClass()) && $reflection->name !== __CLASS__); + + /** @var array> $constants */ + + assert( + self::noAmbiguousValues($constants), + 'Ambiguous enumerator values detected for ' . static::class + ); + + self::$names[static::class] = \array_keys($constants); + return self::$constants[static::class] = $constants; + } + + /** + * Test that the given constants does not contain ambiguous values + * @param array> $constants + * @return bool + */ + private static function noAmbiguousValues($constants) + { + foreach ($constants as $value) { + $names = \array_keys($constants, $value, true); + if (\count($names) > 1) { + return false; + } + } + + return true; + } + + /** + * Test if the given enumerator is part of this enumeration + * + * @param static|null|bool|int|float|string|array $enumerator + * @return bool + * + * @psalm-pure + */ + final public static function has($enumerator) + { + if ($enumerator instanceof static) { + return \get_class($enumerator) === static::class; + } + + return static::hasValue($enumerator); + } + + /** + * Test if the given enumerator value is part of this enumeration + * + * @param null|bool|int|float|string|array $value + * @return bool + * + * @psalm-pure + */ + final public static function hasValue($value) + { + return \in_array($value, self::$constants[static::class] ?? static::getConstants(), true); + } + + /** + * Test if the given enumerator name is part of this enumeration + * + * @param string $name + * @return bool + * + * @psalm-pure + */ + final public static function hasName(string $name) + { + return \defined("static::{$name}"); + } + + /** + * Get an enumerator instance by the given name. + * + * This will be called automatically on calling a method + * with the same name of a defined enumerator. + * + * @param string $method The name of the enumerator (called as method) + * @param array $args There should be no arguments + * @return static + * @throws InvalidArgumentException On an invalid or unknown name + * @throws LogicException On ambiguous constant values + * + * @psalm-pure + */ + final public static function __callStatic(string $method, array $args) + { + return static::byName($method); + } +} diff --git a/marc-mabe/php-enum/src/EnumMap.php b/marc-mabe/php-enum/src/EnumMap.php new file mode 100644 index 000000000..eae61bac8 --- /dev/null +++ b/marc-mabe/php-enum/src/EnumMap.php @@ -0,0 +1,393 @@ +). + * + * @template T of Enum + * @implements ArrayAccess + * @implements IteratorAggregate + * + * @copyright 2020, Marc Bennewitz + * @license http://github.com/marc-mabe/php-enum/blob/master/LICENSE.txt New BSD License + * @link http://github.com/marc-mabe/php-enum for the canonical source repository + */ +class EnumMap implements ArrayAccess, Countable, IteratorAggregate +{ + /** + * The classname of the enumeration type + * @var class-string + */ + private $enumeration; + + /** + * Internal map of ordinal number and data value + * @var array + */ + private $map = []; + + /** + * Constructor + * @param class-string $enumeration The classname of the enumeration type + * @param null|iterable, mixed> $map Initialize map + * @throws InvalidArgumentException + */ + public function __construct(string $enumeration, ?iterable $map = null) + { + if (!\is_subclass_of($enumeration, Enum::class)) { + throw new InvalidArgumentException(\sprintf( + '%s can handle subclasses of %s only', + __CLASS__, + Enum::class + )); + } + $this->enumeration = $enumeration; + + if ($map) { + $this->addIterable($map); + } + } + + /** + * Add virtual private property "__pairs" with a list of key-value-pairs + * to the result of var_dump. + * + * This helps debugging as internally the map is using the ordinal number. + * + * @return array + */ + public function __debugInfo(): array { + $dbg = (array)$this; + $dbg["\0" . self::class . "\0__pairs"] = array_map(function ($k, $v) { + return [$k, $v]; + }, $this->getKeys(), $this->getValues()); + return $dbg; + } + + /* write access (mutable) */ + + /** + * Adds the given enumerator (object or value) mapping to the specified data value. + * @param T|null|bool|int|float|string|array $enumerator + * @param mixed $value + * @throws InvalidArgumentException On an invalid given enumerator + * @see offsetSet() + */ + public function add($enumerator, $value): void + { + $ord = ($this->enumeration)::get($enumerator)->getOrdinal(); + $this->map[$ord] = $value; + } + + /** + * Adds the given iterable, mapping enumerators (objects or values) to data values. + * @param iterable, mixed> $map + * @throws InvalidArgumentException On an invalid given enumerator + */ + public function addIterable(iterable $map): void + { + $innerMap = $this->map; + foreach ($map as $enumerator => $value) { + $ord = ($this->enumeration)::get($enumerator)->getOrdinal(); + $innerMap[$ord] = $value; + } + $this->map = $innerMap; + } + + /** + * Removes the given enumerator (object or value) mapping. + * @param T|null|bool|int|float|string|array $enumerator + * @throws InvalidArgumentException On an invalid given enumerator + * @see offsetUnset() + */ + public function remove($enumerator): void + { + $ord = ($this->enumeration)::get($enumerator)->getOrdinal(); + unset($this->map[$ord]); + } + + /** + * Removes the given iterable enumerator (object or value) mappings. + * @param iterable> $enumerators + * @throws InvalidArgumentException On an invalid given enumerator + */ + public function removeIterable(iterable $enumerators): void + { + $map = $this->map; + foreach ($enumerators as $enumerator) { + $ord = ($this->enumeration)::get($enumerator)->getOrdinal(); + unset($map[$ord]); + } + + $this->map = $map; + } + + /* write access (immutable) */ + + /** + * Creates a new map with the given enumerator (object or value) mapping to the specified data value added. + * @param T|null|bool|int|float|string|array $enumerator + * @param mixed $value + * @return static + * @throws InvalidArgumentException On an invalid given enumerator + */ + public function with($enumerator, $value): self + { + $clone = clone $this; + $clone->add($enumerator, $value); + return $clone; + } + + /** + * Creates a new map with the given iterable mapping enumerators (objects or values) to data values added. + * @param iterable, mixed> $map + * @return static + * @throws InvalidArgumentException On an invalid given enumerator + */ + public function withIterable(iterable $map): self + { + $clone = clone $this; + $clone->addIterable($map); + return $clone; + } + + /** + * Create a new map with the given enumerator mapping removed. + * @param T|null|bool|int|float|string|array $enumerator + * @return static + * @throws InvalidArgumentException On an invalid given enumerator + */ + public function without($enumerator): self + { + $clone = clone $this; + $clone->remove($enumerator); + return $clone; + } + + /** + * Creates a new map with the given iterable enumerator (object or value) mappings removed. + * @param iterable> $enumerators + * @return static + * @throws InvalidArgumentException On an invalid given enumerator + */ + public function withoutIterable(iterable $enumerators): self + { + $clone = clone $this; + $clone->removeIterable($enumerators); + return $clone; + } + + /* read access */ + + /** + * Get the classname of the enumeration type. + * @return class-string + */ + public function getEnumeration(): string + { + return $this->enumeration; + } + + /** + * Get the mapped data value of the given enumerator (object or value). + * @param T|null|bool|int|float|string|array $enumerator + * @return mixed + * @throws InvalidArgumentException On an invalid given enumerator + * @throws UnexpectedValueException If the given enumerator does not exist in this map + * @see offsetGet() + */ + public function get($enumerator) + { + $enumerator = ($this->enumeration)::get($enumerator); + $ord = $enumerator->getOrdinal(); + if (!\array_key_exists($ord, $this->map)) { + throw new UnexpectedValueException(sprintf( + 'Enumerator %s could not be found', + \var_export($enumerator->getValue(), true) + )); + } + + return $this->map[$ord]; + } + + /** + * Get a list of enumerator keys. + * @return T[] + * + * @phpstan-return array + * @psalm-return list + */ + public function getKeys(): array + { + /** @var callable $byOrdinalFn */ + $byOrdinalFn = [$this->enumeration, 'byOrdinal']; + + return \array_map($byOrdinalFn, \array_keys($this->map)); + } + + /** + * Get a list of mapped data values. + * @return mixed[] + * + * @phpstan-return array + * @psalm-return list + */ + public function getValues(): array + { + return \array_values($this->map); + } + + /** + * Search for the given data value. + * @param mixed $value + * @param bool $strict Use strict type comparison + * @return T|null The enumerator object of the first matching data value or NULL + */ + public function search($value, bool $strict = false) + { + /** @var false|int $ord */ + $ord = \array_search($value, $this->map, $strict); + if ($ord !== false) { + return ($this->enumeration)::byOrdinal($ord); + } + + return null; + } + + /** + * Test if the given enumerator key (object or value) exists. + * @param T|null|bool|int|float|string|array $enumerator + * @return bool + * @see offsetExists() + */ + public function has($enumerator): bool + { + try { + $ord = ($this->enumeration)::get($enumerator)->getOrdinal(); + return \array_key_exists($ord, $this->map); + } catch (InvalidArgumentException $e) { + // An invalid enumerator can't be contained in this map + return false; + } + } + + /** + * Test if the given enumerator key (object or value) exists. + * @param T|null|bool|int|float|string|array $enumerator + * @return bool + * @see offsetExists() + * @see has() + * @deprecated Will trigger deprecation warning in last 4.x and removed in 5.x + */ + public function contains($enumerator): bool + { + return $this->has($enumerator); + } + + /* ArrayAccess */ + + /** + * Test if the given enumerator key (object or value) exists and is not NULL + * @param T|null|bool|int|float|string|array $enumerator + * @return bool + * @see contains() + */ + public function offsetExists($enumerator): bool + { + try { + return isset($this->map[($this->enumeration)::get($enumerator)->getOrdinal()]); + } catch (InvalidArgumentException $e) { + // An invalid enumerator can't be an offset of this map + return false; + } + } + + /** + * Get the mapped data value of the given enumerator (object or value). + * @param T|null|bool|int|float|string|array $enumerator + * @return mixed The mapped date value of the given enumerator or NULL + * @throws InvalidArgumentException On an invalid given enumerator + * @see get() + */ + #[\ReturnTypeWillChange] + public function offsetGet($enumerator) + { + try { + return $this->get($enumerator); + } catch (UnexpectedValueException $e) { + return null; + } + } + + /** + * Adds the given enumerator (object or value) mapping to the specified data value. + * @param T|null|bool|int|float|string|array $enumerator + * @param mixed $value + * @return void + * @throws InvalidArgumentException On an invalid given enumerator + * @see add() + */ + public function offsetSet($enumerator, $value = null): void + { + $this->add($enumerator, $value); + } + + /** + * Removes the given enumerator (object or value) mapping. + * @param T|null|bool|int|float|string|array $enumerator + * @return void + * @throws InvalidArgumentException On an invalid given enumerator + * @see remove() + */ + public function offsetUnset($enumerator): void + { + $this->remove($enumerator); + } + + /* IteratorAggregate */ + + /** + * Get a new Iterator. + * + * @return Iterator Iterator + */ + public function getIterator(): Iterator + { + $map = $this->map; + foreach ($map as $ordinal => $value) { + yield ($this->enumeration)::byOrdinal($ordinal) => $value; + } + } + + /* Countable */ + + /** + * Count the number of elements + * + * @return int + */ + public function count(): int + { + return \count($this->map); + } + + /** + * Tests if the map is empty + * + * @return bool + */ + public function isEmpty(): bool + { + return empty($this->map); + } +} diff --git a/marc-mabe/php-enum/src/EnumSerializableTrait.php b/marc-mabe/php-enum/src/EnumSerializableTrait.php new file mode 100644 index 000000000..1f54b9d4a --- /dev/null +++ b/marc-mabe/php-enum/src/EnumSerializableTrait.php @@ -0,0 +1,110 @@ + + */ + abstract public function getValue(); + + /** + * Returns an array of data to be serialized. + * This magic method will be called by serialize() in PHP >= 7.4 + * + * @return array> + */ + public function __serialize(): array + { + return ['value' => $this->getValue()]; + } + + /** + * Receives an array of data to be unserialized on a new instance without constructor. + * This magic method will be called in PHP >= 7.4 is the data where serialized with PHP >= 7.4. + * + * @throws RuntimeException On missing, unknown or invalid value + * @throws LogicException On calling this method on an already initialized enumerator + * + * @param array $data + * @return void + */ + public function __unserialize(array $data): void + { + if (!\array_key_exists('value', $data)) { + throw new RuntimeException('Missing array key "value"'); + } + + /** @var mixed $value */ + $value = $data['value']; + $constants = self::getConstants(); + $name = \array_search($value, $constants, true); + if ($name === false) { + $message = \is_scalar($value) + ? 'Unknown value ' . \var_export($value, true) + : 'Invalid value of type ' . (\is_object($value) ? \get_class($value) : \gettype($value)); + throw new RuntimeException($message); + } + + $class = static::class; + $enumerator = $this; + $closure = function () use ($class, $name, $value, $enumerator) { + if ($value !== null && $this->value !== null) { + throw new LogicException('Do not call this directly - please use unserialize($enum) instead'); + } + + $this->value = $value; + + if (!isset(self::$instances[$class][$name])) { + self::$instances[$class][$name] = $enumerator; + } + }; + $closure->bindTo($this, Enum::class)(); + } + + /** + * Serialize the value of the enumeration + * This will be called automatically on `serialize()` if the enumeration implements the `Serializable` interface + * + * @return string + * @deprecated Since PHP 7.4 + */ + public function serialize(): string + { + return \serialize($this->getValue()); + } + + /** + * Unserializes a given serialized value and push it into the current instance + * This will be called automatically on `unserialize()` if the enumeration implements the `Serializable` interface + * @param string $serialized + * @return void + * @throws RuntimeException On an unknown or invalid value + * @throws LogicException On calling this method on an already initialized enumerator + * @deprecated Since PHP 7.4 + */ + public function unserialize($serialized): void + { + $this->__unserialize(['value' => \unserialize($serialized)]); + } +} diff --git a/marc-mabe/php-enum/src/EnumSet.php b/marc-mabe/php-enum/src/EnumSet.php new file mode 100644 index 000000000..42e34b398 --- /dev/null +++ b/marc-mabe/php-enum/src/EnumSet.php @@ -0,0 +1,1193 @@ +) + * based on an integer or binary bitset depending of given enumeration size. + * + * @template T of Enum + * @implements IteratorAggregate + * + * @copyright 2020, Marc Bennewitz + * @license http://github.com/marc-mabe/php-enum/blob/master/LICENSE.txt New BSD License + * @link http://github.com/marc-mabe/php-enum for the canonical source repository + */ +class EnumSet implements IteratorAggregate, Countable +{ + /** + * The classname of the Enumeration + * @var class-string + */ + private $enumeration; + + /** + * Number of enumerators defined in the enumeration + * @var int + */ + private $enumerationCount; + + /** + * Integer or binary (little endian) bitset + * @var int|string + */ + private $bitset = 0; + + /** + * Integer or binary (little endian) empty bitset + * + * @var int|string + */ + private $emptyBitset = 0; + + /**#@+ + * Defines private method names to be called depended of how the bitset type was set too. + * ... Integer or binary bitset. + * ... *Int or *Bin method + * + * @var string + */ + /** @var string */ + private $fnDoGetIterator = 'doGetIteratorInt'; + + /** @var string */ + private $fnDoCount = 'doCountInt'; + + /** @var string */ + private $fnDoGetOrdinals = 'doGetOrdinalsInt'; + + /** @var string */ + private $fnDoGetBit = 'doGetBitInt'; + + /** @var string */ + private $fnDoSetBit = 'doSetBitInt'; + + /** @var string */ + private $fnDoUnsetBit = 'doUnsetBitInt'; + + /** @var string */ + private $fnDoGetBinaryBitsetLe = 'doGetBinaryBitsetLeInt'; + + /** @var string */ + private $fnDoSetBinaryBitsetLe = 'doSetBinaryBitsetLeInt'; + /**#@-*/ + + /** + * Constructor + * + * @param class-string $enumeration The classname of the enumeration + * @param iterable>|null $enumerators iterable list of enumerators initializing the set + * @throws InvalidArgumentException + */ + public function __construct(string $enumeration, ?iterable $enumerators = null) + { + if (!\is_subclass_of($enumeration, Enum::class)) { + throw new InvalidArgumentException(\sprintf( + '%s can handle subclasses of %s only', + __METHOD__, + Enum::class + )); + } + + $this->enumeration = $enumeration; + $this->enumerationCount = \count($enumeration::getConstants()); + + // By default the bitset is initialized as integer bitset + // in case the enumeration has more enumerators then integer bits + // we will switch this into a binary bitset + if ($this->enumerationCount > \PHP_INT_SIZE * 8) { + // init binary bitset with zeros + $this->bitset = $this->emptyBitset = \str_repeat("\0", (int)\ceil($this->enumerationCount / 8)); + + // switch internal binary bitset functions + $this->fnDoGetIterator = 'doGetIteratorBin'; + $this->fnDoCount = 'doCountBin'; + $this->fnDoGetOrdinals = 'doGetOrdinalsBin'; + $this->fnDoGetBit = 'doGetBitBin'; + $this->fnDoSetBit = 'doSetBitBin'; + $this->fnDoUnsetBit = 'doUnsetBitBin'; + $this->fnDoGetBinaryBitsetLe = 'doGetBinaryBitsetLeBin'; + $this->fnDoSetBinaryBitsetLe = 'doSetBinaryBitsetLeBin'; + } + + if ($enumerators !== null) { + foreach ($enumerators as $enumerator) { + $this->{$this->fnDoSetBit}($enumeration::get($enumerator)->getOrdinal()); + } + } + } + + /** + * Add virtual private property "__enumerators" with a list of enumerator values set + * to the result of var_dump. + * + * This helps debugging as internally the enumerators of this EnumSet gets stored + * as either integer or binary bit-array. + * + * @return array + */ + public function __debugInfo() { + $dbg = (array)$this; + $dbg["\0" . self::class . "\0__enumerators"] = $this->getValues(); + return $dbg; + } + + /** + * Get the classname of the enumeration + * @return class-string + */ + public function getEnumeration(): string + { + return $this->enumeration; + } + + /* write access (mutable) */ + + /** + * Adds an enumerator object or value + * @param T|null|bool|int|float|string|array $enumerator Enumerator object or value + * @return void + * @throws InvalidArgumentException On an invalid given enumerator + */ + public function add($enumerator): void + { + $this->{$this->fnDoSetBit}(($this->enumeration)::get($enumerator)->getOrdinal()); + } + + /** + * Adds all enumerator objects or values of the given iterable + * @param iterable> $enumerators Iterable list of enumerator objects or values + * @return void + * @throws InvalidArgumentException On an invalid given enumerator + */ + public function addIterable(iterable $enumerators): void + { + $bitset = $this->bitset; + + try { + foreach ($enumerators as $enumerator) { + $this->{$this->fnDoSetBit}(($this->enumeration)::get($enumerator)->getOrdinal()); + } + } catch (\Throwable $e) { + // reset all changes until error happened + $this->bitset = $bitset; + throw $e; + } + } + + /** + * Removes the given enumerator object or value + * @param T|null|bool|int|float|string|array $enumerator Enumerator object or value + * @return void + * @throws InvalidArgumentException On an invalid given enumerator + */ + public function remove($enumerator): void + { + $this->{$this->fnDoUnsetBit}(($this->enumeration)::get($enumerator)->getOrdinal()); + } + + /** + * Adds an enumerator object or value + * @param T|null|bool|int|float|string|array $enumerator Enumerator object or value + * @return void + * @throws InvalidArgumentException On an invalid given enumerator + * @see add() + * @see with() + * @deprecated Will trigger deprecation warning in last 4.x and removed in 5.x + */ + public function attach($enumerator): void + { + $this->add($enumerator); + } + + /** + * Removes the given enumerator object or value + * @param T|null|bool|int|float|string|array $enumerator Enumerator object or value + * @return void + * @throws InvalidArgumentException On an invalid given enumerator + * @see remove() + * @see without() + * @deprecated Will trigger deprecation warning in last 4.x and removed in 5.x + */ + public function detach($enumerator): void + { + $this->remove($enumerator); + } + + /** + * Removes all enumerator objects or values of the given iterable + * @param iterable> $enumerators Iterable list of enumerator objects or values + * @return void + * @throws InvalidArgumentException On an invalid given enumerator + */ + public function removeIterable(iterable $enumerators): void + { + $bitset = $this->bitset; + + try { + foreach ($enumerators as $enumerator) { + $this->{$this->fnDoUnsetBit}(($this->enumeration)::get($enumerator)->getOrdinal()); + } + } catch (\Throwable $e) { + // reset all changes until error happened + $this->bitset = $bitset; + throw $e; + } + } + + /** + * Modify this set from both this and other (this | other) + * + * @param EnumSet $other EnumSet of the same enumeration to produce the union + * @return void + * @throws InvalidArgumentException If $other doesn't match the enumeration + */ + public function setUnion(EnumSet $other): void + { + if ($this->enumeration !== $other->enumeration) { + throw new InvalidArgumentException(\sprintf( + 'Other should be of the same enumeration as this %s', + $this->enumeration + )); + } + + $this->bitset = $this->bitset | $other->bitset; + } + + /** + * Modify this set with enumerators common to both this and other (this & other) + * + * @param EnumSet $other EnumSet of the same enumeration to produce the intersect + * @return void + * @throws InvalidArgumentException If $other doesn't match the enumeration + */ + public function setIntersect(EnumSet $other): void + { + if ($this->enumeration !== $other->enumeration) { + throw new InvalidArgumentException(\sprintf( + 'Other should be of the same enumeration as this %s', + $this->enumeration + )); + } + + $this->bitset = $this->bitset & $other->bitset; + } + + /** + * Modify this set with enumerators in this but not in other (this - other) + * + * @param EnumSet $other EnumSet of the same enumeration to produce the diff + * @return void + * @throws InvalidArgumentException If $other doesn't match the enumeration + */ + public function setDiff(EnumSet $other): void + { + if ($this->enumeration !== $other->enumeration) { + throw new InvalidArgumentException(\sprintf( + 'Other should be of the same enumeration as this %s', + $this->enumeration + )); + } + + $this->bitset = $this->bitset & ~$other->bitset; + } + + /** + * Modify this set with enumerators in either this and other but not in both (this ^ other) + * + * @param EnumSet $other EnumSet of the same enumeration to produce the symmetric difference + * @return void + * @throws InvalidArgumentException If $other doesn't match the enumeration + */ + public function setSymDiff(EnumSet $other): void + { + if ($this->enumeration !== $other->enumeration) { + throw new InvalidArgumentException(\sprintf( + 'Other should be of the same enumeration as this %s', + $this->enumeration + )); + } + + $this->bitset = $this->bitset ^ $other->bitset; + } + + /** + * Set the given binary bitset in little-endian order + * + * @param string $bitset + * @return void + * @throws InvalidArgumentException On out-of-range bits given as input bitset + * @uses doSetBinaryBitsetLeBin() + * @uses doSetBinaryBitsetLeInt() + */ + public function setBinaryBitsetLe(string $bitset): void + { + $this->{$this->fnDoSetBinaryBitsetLe}($bitset); + } + + /** + * Set binary bitset in little-endian order + * + * @param string $bitset + * @return void + * @throws InvalidArgumentException On out-of-range bits given as input bitset + * @see setBinaryBitsetLeBin() + * @see doSetBinaryBitsetLeInt() + */ + private function doSetBinaryBitsetLeBin($bitset): void + { + /** @var string $thisBitset */ + $thisBitset = $this->bitset; + + $size = \strlen($thisBitset); + $sizeIn = \strlen($bitset); + + if ($sizeIn < $size) { + // add "\0" if the given bitset is not long enough + $bitset .= \str_repeat("\0", $size - $sizeIn); + } elseif ($sizeIn > $size) { + if (\ltrim(\substr($bitset, $size), "\0") !== '') { + throw new InvalidArgumentException('out-of-range bits detected'); + } + $bitset = \substr($bitset, 0, $size); + } + + // truncate out-of-range bits of last byte + $lastByteMaxOrd = $this->enumerationCount % 8; + if ($lastByteMaxOrd !== 0) { + $lastByte = $bitset[-1]; + $lastByteExpected = \chr((1 << $lastByteMaxOrd) - 1) & $lastByte; + if ($lastByte !== $lastByteExpected) { + throw new InvalidArgumentException('out-of-range bits detected'); + } + + $this->bitset = \substr($bitset, 0, -1) . $lastByteExpected; + } + + $this->bitset = $bitset; + } + + /** + * Set binary bitset in little-endian order + * + * @param string $bitset + * @return void + * @throws InvalidArgumentException On out-of-range bits given as input bitset + * @see setBinaryBitsetLeBin() + * @see doSetBinaryBitsetLeBin() + */ + private function doSetBinaryBitsetLeInt($bitset): void + { + $len = \strlen($bitset); + $int = 0; + for ($i = 0; $i < $len; ++$i) { + $ord = \ord($bitset[$i]); + + if ($ord && $i > \PHP_INT_SIZE - 1) { + throw new InvalidArgumentException('out-of-range bits detected'); + } + + $int |= $ord << (8 * $i); + } + + if ($int & (~0 << $this->enumerationCount)) { + throw new InvalidArgumentException('out-of-range bits detected'); + } + + $this->bitset = $int; + } + + /** + * Set the given binary bitset in big-endian order + * + * @param string $bitset + * @return void + * @throws InvalidArgumentException On out-of-range bits given as input bitset + */ + public function setBinaryBitsetBe(string $bitset): void + { + $this->{$this->fnDoSetBinaryBitsetLe}(\strrev($bitset)); + } + + /** + * Set a bit at the given ordinal number + * + * @param int $ordinal Ordinal number of bit to set + * @param bool $bit The bit to set + * @return void + * @throws InvalidArgumentException If the given ordinal number is out-of-range + * @uses doSetBitBin() + * @uses doSetBitInt() + * @uses doUnsetBitBin() + * @uses doUnsetBitInt() + */ + public function setBit(int $ordinal, bool $bit): void + { + if ($ordinal < 0 || $ordinal > $this->enumerationCount) { + throw new InvalidArgumentException("Ordinal number must be between 0 and {$this->enumerationCount}"); + } + + if ($bit) { + $this->{$this->fnDoSetBit}($ordinal); + } else { + $this->{$this->fnDoUnsetBit}($ordinal); + } + } + + /** + * Set a bit at the given ordinal number. + * + * This is the binary bitset implementation. + * + * @param int $ordinal Ordinal number of bit to set + * @return void + * @see setBit() + * @see doSetBitInt() + */ + private function doSetBitBin($ordinal): void + { + /** @var string $thisBitset */ + $thisBitset = $this->bitset; + + $byte = (int) ($ordinal / 8); + $thisBitset[$byte] = $thisBitset[$byte] | \chr(1 << ($ordinal % 8)); + + $this->bitset = $thisBitset; + } + + /** + * Set a bit at the given ordinal number. + * + * This is the binary bitset implementation. + * + * @param int $ordinal Ordinal number of bit to set + * @return void + * @see setBit() + * @see doSetBitBin() + */ + private function doSetBitInt($ordinal): void + { + /** @var int $thisBitset */ + $thisBitset = $this->bitset; + + $this->bitset = $thisBitset | (1 << $ordinal); + } + + /** + * Unset a bit at the given ordinal number. + * + * This is the binary bitset implementation. + * + * @param int $ordinal Ordinal number of bit to unset + * @return void + * @see setBit() + * @see doUnsetBitInt() + */ + private function doUnsetBitBin($ordinal): void + { + /** @var string $thisBitset */ + $thisBitset = $this->bitset; + + $byte = (int) ($ordinal / 8); + $thisBitset[$byte] = $thisBitset[$byte] & \chr(~(1 << ($ordinal % 8))); + + $this->bitset = $thisBitset; + } + + /** + * Unset a bit at the given ordinal number. + * + * This is the integer bitset implementation. + * + * @param int $ordinal Ordinal number of bit to unset + * @return void + * @see setBit() + * @see doUnsetBitBin() + */ + private function doUnsetBitInt($ordinal): void + { + /** @var int $thisBitset */ + $thisBitset = $this->bitset; + + $this->bitset = $thisBitset & ~(1 << $ordinal); + } + + /* write access (immutable) */ + + /** + * Creates a new set with the given enumerator object or value added + * @param T|null|bool|int|float|string|array $enumerator Enumerator object or value + * @return static + * @throws InvalidArgumentException On an invalid given enumerator + */ + public function with($enumerator): self + { + $clone = clone $this; + $clone->{$this->fnDoSetBit}(($this->enumeration)::get($enumerator)->getOrdinal()); + return $clone; + } + + /** + * Creates a new set with the given enumeration objects or values added + * @param iterable> $enumerators Iterable list of enumerator objects or values + * @return static + * @throws InvalidArgumentException On an invalid given enumerator + */ + public function withIterable(iterable $enumerators): self + { + $clone = clone $this; + foreach ($enumerators as $enumerator) { + $clone->{$this->fnDoSetBit}(($this->enumeration)::get($enumerator)->getOrdinal()); + } + return $clone; + } + + /** + * Create a new set with the given enumerator object or value removed + * @param T|null|bool|int|float|string|array $enumerator Enumerator object or value + * @return static + * @throws InvalidArgumentException On an invalid given enumerator + */ + public function without($enumerator): self + { + $clone = clone $this; + $clone->{$this->fnDoUnsetBit}(($this->enumeration)::get($enumerator)->getOrdinal()); + return $clone; + } + + /** + * Creates a new set with the given enumeration objects or values removed + * @param iterable> $enumerators Iterable list of enumerator objects or values + * @return static + * @throws InvalidArgumentException On an invalid given enumerator + */ + public function withoutIterable(iterable $enumerators): self + { + $clone = clone $this; + foreach ($enumerators as $enumerator) { + $clone->{$this->fnDoUnsetBit}(($this->enumeration)::get($enumerator)->getOrdinal()); + } + return $clone; + } + + /** + * Create a new set with enumerators from both this and other (this | other) + * + * @param EnumSet $other EnumSet of the same enumeration to produce the union + * @return static + * @throws InvalidArgumentException If $other doesn't match the enumeration + */ + public function withUnion(EnumSet $other): self + { + $clone = clone $this; + $clone->setUnion($other); + return $clone; + } + + /** + * Create a new set with enumerators from both this and other (this | other) + * + * @param EnumSet $other EnumSet of the same enumeration to produce the union + * @return static + * @throws InvalidArgumentException If $other doesn't match the enumeration + * @see withUnion() + * @see setUnion() + * @deprecated Will trigger deprecation warning in last 4.x and removed in 5.x + */ + public function union(EnumSet $other): self + { + return $this->withUnion($other); + } + + /** + * Create a new set with enumerators common to both this and other (this & other) + * + * @param EnumSet $other EnumSet of the same enumeration to produce the intersect + * @return static + * @throws InvalidArgumentException If $other doesn't match the enumeration + */ + public function withIntersect(EnumSet $other): self + { + $clone = clone $this; + $clone->setIntersect($other); + return $clone; + } + + /** + * Create a new set with enumerators common to both this and other (this & other) + * + * @param EnumSet $other EnumSet of the same enumeration to produce the intersect + * @return static + * @throws InvalidArgumentException If $other doesn't match the enumeration + * @see withIntersect() + * @see setIntersect() + * @deprecated Will trigger deprecation warning in last 4.x and removed in 5.x + */ + public function intersect(EnumSet $other): self + { + return $this->withIntersect($other); + } + + /** + * Create a new set with enumerators in this but not in other (this - other) + * + * @param EnumSet $other EnumSet of the same enumeration to produce the diff + * @return static + * @throws InvalidArgumentException If $other doesn't match the enumeration + */ + public function withDiff(EnumSet $other): self + { + $clone = clone $this; + $clone->setDiff($other); + return $clone; + } + + /** + * Create a new set with enumerators in this but not in other (this - other) + * + * @param EnumSet $other EnumSet of the same enumeration to produce the diff + * @return static + * @throws InvalidArgumentException If $other doesn't match the enumeration + * @see withDiff() + * @see setDiff() + * @deprecated Will trigger deprecation warning in last 4.x and removed in 5.x + */ + public function diff(EnumSet $other): self + { + return $this->withDiff($other); + } + + /** + * Create a new set with enumerators in either this and other but not in both (this ^ other) + * + * @param EnumSet $other EnumSet of the same enumeration to produce the symmetric difference + * @return static + * @throws InvalidArgumentException If $other doesn't match the enumeration + */ + public function withSymDiff(EnumSet $other): self + { + $clone = clone $this; + $clone->setSymDiff($other); + return $clone; + } + + /** + * Create a new set with enumerators in either this and other but not in both (this ^ other) + * + * @param EnumSet $other EnumSet of the same enumeration to produce the symmetric difference + * @return static + * @throws InvalidArgumentException If $other doesn't match the enumeration + * @see withSymDiff() + * @see setSymDiff() + * @deprecated Will trigger deprecation warning in last 4.x and removed in 5.x + */ + public function symDiff(EnumSet $other): self + { + return $this->withSymDiff($other); + } + + /** + * Create a new set with the given binary bitset in little-endian order + * + * @param string $bitset + * @return static + * @throws InvalidArgumentException On out-of-range bits given as input bitset + * @uses doSetBinaryBitsetLeBin() + * @uses doSetBinaryBitsetLeInt() + */ + public function withBinaryBitsetLe(string $bitset): self + { + $clone = clone $this; + $clone->{$this->fnDoSetBinaryBitsetLe}($bitset); + return $clone; + } + + /** + * Create a new set with the given binary bitset in big-endian order + * + * @param string $bitset + * @return static + * @throws InvalidArgumentException On out-of-range bits given as input bitset + */ + public function withBinaryBitsetBe(string $bitset): self + { + $clone = $this; + $clone->{$this->fnDoSetBinaryBitsetLe}(\strrev($bitset)); + return $clone; + } + + /** + * Create a new set with the bit at the given ordinal number set + * + * @param int $ordinal Ordinal number of bit to set + * @param bool $bit The bit to set + * @return static + * @throws InvalidArgumentException If the given ordinal number is out-of-range + * @uses doSetBitBin() + * @uses doSetBitInt() + * @uses doUnsetBitBin() + * @uses doUnsetBitInt() + */ + public function withBit(int $ordinal, bool $bit): self + { + $clone = clone $this; + $clone->setBit($ordinal, $bit); + return $clone; + } + + /* read access */ + + /** + * Test if the given enumerator exists + * @param T|null|bool|int|float|string|array $enumerator + * @return bool + */ + public function has($enumerator): bool + { + return $this->{$this->fnDoGetBit}(($this->enumeration)::get($enumerator)->getOrdinal()); + } + + /** + * Test if the given enumerator exists + * @param T|null|bool|int|float|string|array $enumerator + * @return bool + * @see has() + * @deprecated Will trigger deprecation warning in last 4.x and removed in 5.x + */ + public function contains($enumerator): bool + { + return $this->has($enumerator); + } + + /* IteratorAggregate */ + + /** + * Get a new iterator + * @return Iterator + * @uses doGetIteratorInt() + * @uses doGetIteratorBin() + */ + public function getIterator(): Iterator + { + return $this->{$this->fnDoGetIterator}(); + } + + /** + * Get a new Iterator. + * + * This is the binary bitset implementation. + * + * @return Iterator + * @see getIterator() + * @see goGetIteratorInt() + */ + private function doGetIteratorBin() + { + /** @var string $bitset */ + $bitset = $this->bitset; + $byteLen = \strlen($bitset); + for ($bytePos = 0; $bytePos < $byteLen; ++$bytePos) { + if ($bitset[$bytePos] === "\0") { + // fast skip null byte + continue; + } + + $ord = \ord($bitset[$bytePos]); + for ($bitPos = 0; $bitPos < 8; ++$bitPos) { + if ($ord & (1 << $bitPos)) { + $ordinal = $bytePos * 8 + $bitPos; + yield $ordinal => ($this->enumeration)::byOrdinal($ordinal); + } + } + } + } + + /** + * Get a new Iterator. + * + * This is the integer bitset implementation. + * + * @return Iterator + * @see getIterator() + * @see doGetIteratorBin() + */ + private function doGetIteratorInt() + { + /** @var int $bitset */ + $bitset = $this->bitset; + $count = $this->enumerationCount; + for ($ordinal = 0; $ordinal < $count; ++$ordinal) { + if ($bitset & (1 << $ordinal)) { + yield $ordinal => ($this->enumeration)::byOrdinal($ordinal); + } + } + } + + /* Countable */ + + /** + * Count the number of elements + * + * @return int + * @uses doCountBin() + * @uses doCountInt() + */ + public function count(): int + { + return $this->{$this->fnDoCount}(); + } + + /** + * Count the number of elements. + * + * This is the binary bitset implementation. + * + * @return int + * @see count() + * @see doCountInt() + */ + private function doCountBin() + { + /** @var string $bitset */ + $bitset = $this->bitset; + $count = 0; + $byteLen = \strlen($bitset); + for ($bytePos = 0; $bytePos < $byteLen; ++$bytePos) { + if ($bitset[$bytePos] === "\0") { + // fast skip null byte + continue; + } + + $ord = \ord($bitset[$bytePos]); + if ($ord & 0b00000001) ++$count; + if ($ord & 0b00000010) ++$count; + if ($ord & 0b00000100) ++$count; + if ($ord & 0b00001000) ++$count; + if ($ord & 0b00010000) ++$count; + if ($ord & 0b00100000) ++$count; + if ($ord & 0b01000000) ++$count; + if ($ord & 0b10000000) ++$count; + } + return $count; + } + + /** + * Count the number of elements. + * + * This is the integer bitset implementation. + * + * @return int + * @see count() + * @see doCountBin() + */ + private function doCountInt() + { + /** @var int $bitset */ + $bitset = $this->bitset; + $count = 0; + + // PHP does not support right shift unsigned + if ($bitset < 0) { + $count = 1; + $bitset = $bitset & \PHP_INT_MAX; + } + + // iterate byte by byte and count set bits + $phpIntBitSize = \PHP_INT_SIZE * 8; + for ($bitPos = 0; $bitPos < $phpIntBitSize; $bitPos += 8) { + $bitChk = 0xff << $bitPos; + $byte = $bitset & $bitChk; + if ($byte) { + $byte = $byte >> $bitPos; + if ($byte & 0b00000001) ++$count; + if ($byte & 0b00000010) ++$count; + if ($byte & 0b00000100) ++$count; + if ($byte & 0b00001000) ++$count; + if ($byte & 0b00010000) ++$count; + if ($byte & 0b00100000) ++$count; + if ($byte & 0b01000000) ++$count; + if ($byte & 0b10000000) ++$count; + } + + if ($bitset <= $bitChk) { + break; + } + } + + return $count; + } + + /** + * Check if this EnumSet is the same as other + * @param EnumSet $other + * @return bool + */ + public function isEqual(EnumSet $other): bool + { + return $this->enumeration === $other->enumeration + && $this->bitset === $other->bitset; + } + + /** + * Check if this EnumSet is a subset of other + * @param EnumSet $other + * @return bool + */ + public function isSubset(EnumSet $other): bool + { + return $this->enumeration === $other->enumeration + && ($this->bitset & $other->bitset) === $this->bitset; + } + + /** + * Check if this EnumSet is a superset of other + * @param EnumSet $other + * @return bool + */ + public function isSuperset(EnumSet $other): bool + { + return $this->enumeration === $other->enumeration + && ($this->bitset | $other->bitset) === $this->bitset; + } + + /** + * Tests if the set is empty + * + * @return bool + */ + public function isEmpty(): bool + { + return $this->bitset === $this->emptyBitset; + } + + /** + * Get ordinal numbers of the defined enumerators as array + * @return array + * @uses doGetOrdinalsBin() + * @uses doGetOrdinalsInt() + */ + public function getOrdinals(): array + { + return $this->{$this->fnDoGetOrdinals}(); + } + + /** + * Get ordinal numbers of the defined enumerators as array. + * + * This is the binary bitset implementation. + * + * @return array + * @see getOrdinals() + * @see goGetOrdinalsInt() + */ + private function doGetOrdinalsBin() + { + /** @var string $bitset */ + $bitset = $this->bitset; + $ordinals = []; + $byteLen = \strlen($bitset); + for ($bytePos = 0; $bytePos < $byteLen; ++$bytePos) { + if ($bitset[$bytePos] === "\0") { + // fast skip null byte + continue; + } + + $ord = \ord($bitset[$bytePos]); + for ($bitPos = 0; $bitPos < 8; ++$bitPos) { + if ($ord & (1 << $bitPos)) { + $ordinals[] = $bytePos * 8 + $bitPos; + } + } + } + return $ordinals; + } + + /** + * Get ordinal numbers of the defined enumerators as array. + * + * This is the integer bitset implementation. + * + * @return array + * @see getOrdinals() + * @see doGetOrdinalsBin() + */ + private function doGetOrdinalsInt() + { + /** @var int $bitset */ + $bitset = $this->bitset; + $ordinals = []; + $count = $this->enumerationCount; + for ($ordinal = 0; $ordinal < $count; ++$ordinal) { + if ($bitset & (1 << $ordinal)) { + $ordinals[] = $ordinal; + } + } + return $ordinals; + } + + /** + * Get values of the defined enumerators as array + * @return (null|bool|int|float|string|array)[] + * + * @phpstan-return array> + * @psalm-return list + */ + public function getValues(): array + { + $enumeration = $this->enumeration; + $values = []; + foreach ($this->getOrdinals() as $ord) { + $values[] = $enumeration::byOrdinal($ord)->getValue(); + } + return $values; + } + + /** + * Get names of the defined enumerators as array + * @return string[] + * + * @phpstan-return array + * @psalm-return list + */ + public function getNames(): array + { + $enumeration = $this->enumeration; + $names = []; + foreach ($this->getOrdinals() as $ord) { + $names[] = $enumeration::byOrdinal($ord)->getName(); + } + return $names; + } + + /** + * Get the defined enumerators as array + * @return Enum[] + * + * @phpstan-return array + * @psalm-return list + */ + public function getEnumerators(): array + { + $enumeration = $this->enumeration; + $enumerators = []; + foreach ($this->getOrdinals() as $ord) { + $enumerators[] = $enumeration::byOrdinal($ord); + } + return $enumerators; + } + + /** + * Get binary bitset in little-endian order + * + * @return string + * @uses doGetBinaryBitsetLeBin() + * @uses doGetBinaryBitsetLeInt() + */ + public function getBinaryBitsetLe(): string + { + return $this->{$this->fnDoGetBinaryBitsetLe}(); + } + + /** + * Get binary bitset in little-endian order. + * + * This is the binary bitset implementation. + * + * @return string + * @see getBinaryBitsetLe() + * @see doGetBinaryBitsetLeInt() + */ + private function doGetBinaryBitsetLeBin() + { + /** @var string $bitset */ + $bitset = $this->bitset; + + return $bitset; + } + + /** + * Get binary bitset in little-endian order. + * + * This is the integer bitset implementation. + * + * @return string + * @see getBinaryBitsetLe() + * @see doGetBinaryBitsetLeBin() + */ + private function doGetBinaryBitsetLeInt() + { + $bin = \pack(\PHP_INT_SIZE === 8 ? 'P' : 'V', $this->bitset); + return \substr($bin, 0, (int)\ceil($this->enumerationCount / 8)); + } + + /** + * Get binary bitset in big-endian order + * + * @return string + */ + public function getBinaryBitsetBe(): string + { + return \strrev($this->getBinaryBitsetLe()); + } + + /** + * Get a bit at the given ordinal number + * + * @param int $ordinal Ordinal number of bit to get + * @return bool + * @throws InvalidArgumentException If the given ordinal number is out-of-range + * @uses doGetBitBin() + * @uses doGetBitInt() + */ + public function getBit(int $ordinal): bool + { + if ($ordinal < 0 || $ordinal > $this->enumerationCount) { + throw new InvalidArgumentException("Ordinal number must be between 0 and {$this->enumerationCount}"); + } + + return $this->{$this->fnDoGetBit}($ordinal); + } + + /** + * Get a bit at the given ordinal number. + * + * This is the binary bitset implementation. + * + * @param int $ordinal Ordinal number of bit to get + * @return bool + * @see getBit() + * @see doGetBitInt() + */ + private function doGetBitBin($ordinal) + { + /** @var string $bitset */ + $bitset = $this->bitset; + + return (\ord($bitset[(int) ($ordinal / 8)]) & 1 << ($ordinal % 8)) !== 0; + } + + /** + * Get a bit at the given ordinal number. + * + * This is the integer bitset implementation. + * + * @param int $ordinal Ordinal number of bit to get + * @return bool + * @see getBit() + * @see doGetBitBin() + */ + private function doGetBitInt($ordinal) + { + /** @var int $bitset */ + $bitset = $this->bitset; + + return (bool)($bitset & (1 << $ordinal)); + } +} diff --git a/marc-mabe/php-enum/stubs/Stringable.php b/marc-mabe/php-enum/stubs/Stringable.php new file mode 100644 index 000000000..77e037cb5 --- /dev/null +++ b/marc-mabe/php-enum/stubs/Stringable.php @@ -0,0 +1,11 @@ +getPath(), $userValues); - if (array_key_exists('requestOptions', $userValues)) { + if (isset($userValues['requestOptions'])) { $options += $userValues['requestOptions']; + + // headers are always created in options, merge them + if (isset($userValues['requestOptions']['headers'])) { + $options['headers'] = array_merge($options['headers'], $userValues['requestOptions']['headers']); + } } $options['openstack.skip_auth'] = $operation->getSkipAuth(); diff --git a/symfony/polyfill-php80/PhpToken.php b/symfony/polyfill-php80/PhpToken.php index fe6e69105..cd78c4ccc 100644 --- a/symfony/polyfill-php80/PhpToken.php +++ b/symfony/polyfill-php80/PhpToken.php @@ -29,7 +29,7 @@ class PhpToken implements \Stringable public $text; /** - * @var int + * @var -1|positive-int */ public $line; @@ -38,6 +38,9 @@ class PhpToken implements \Stringable */ public $pos; + /** + * @param -1|positive-int $line + */ public function __construct(int $id, string $text, int $line = -1, int $position = -1) { $this->id = $id; @@ -80,7 +83,7 @@ public function __toString(): string } /** - * @return static[] + * @return list */ public static function tokenize(string $code, int $flags = 0): array {